Skip to content

Commit 41505e0

Browse files
committed
chore(predictions): add attempt count changes and unit tests (#3657)
* chore(predictions): add attempt count changes and unit tests * remove test url * Add coding keys for Challenge object
1 parent afdf3a5 commit 41505e0

File tree

6 files changed

+259
-16
lines changed

6 files changed

+259
-16
lines changed

AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/SPI/AWSPredictionsPlugin+Liveness.swift

+4-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ extension AWSPredictionsPlugin {
1515
withID sessionID: String,
1616
credentialsProvider: AWSCredentialsProvider? = nil,
1717
region: String,
18-
options: FaceLivenessSession.Options,
1918
completion: @escaping (Result<Void, FaceLivenessSessionError>) -> Void
2019
) async throws -> FaceLivenessSession {
2120

@@ -36,8 +35,7 @@ extension AWSPredictionsPlugin {
3635
let session = FaceLivenessSession(
3736
websocket: WebSocketSession(),
3837
signer: signer,
39-
baseURL: url,
40-
options: options
38+
baseURL: url
4139
)
4240

4341
session.onServiceException = { completion(.failure($0)) }
@@ -49,14 +47,14 @@ extension AWSPredictionsPlugin {
4947
extension FaceLivenessSession {
5048
@_spi(PredictionsFaceLiveness)
5149
public struct Options {
52-
public let viewId: String
50+
public let attemptCount: Int
5351
public let preCheckViewEnabled: Bool
5452

5553
public init(
56-
faceLivenessDetectorViewId: String,
54+
attemptCount: Int,
5755
preCheckViewEnabled: Bool
5856
) {
59-
self.viewId = faceLivenessDetectorViewId
57+
self.attemptCount = attemptCount
6058
self.preCheckViewEnabled = preCheckViewEnabled
6159
}
6260
}

AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/Challenge.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99

1010
@_spi(PredictionsFaceLiveness)
11-
public struct Challenge {
11+
public struct Challenge: Codable {
1212
public let version: String
1313
public let type: ChallengeType
1414

@@ -20,6 +20,11 @@ public struct Challenge {
2020
public func queryParameterString() -> String {
2121
return self.type.rawValue + "_" + self.version
2222
}
23+
24+
enum CodingKeys: String, CodingKey {
25+
case version = "Version"
26+
case type = "Type"
27+
}
2328
}
2429

2530
@_spi(PredictionsFaceLiveness)

AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift

+4-8
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public final class FaceLivenessSession: LivenessService {
1818
var serverEventListeners: [LivenessEventKind.Server: (FaceLivenessSession.SessionConfiguration) -> Void] = [:]
1919
var challengeTypeListeners: [LivenessEventKind.Server: (Challenge) -> Void] = [:]
2020
var onComplete: (ServerDisconnection) -> Void = { _ in }
21-
let options: FaceLivenessSession.Options
2221

2322
private let livenessServiceDispatchQueue = DispatchQueue(
2423
label: "com.amazon.aws.amplify.liveness.service",
@@ -28,14 +27,12 @@ public final class FaceLivenessSession: LivenessService {
2827
init(
2928
websocket: WebSocketSession,
3029
signer: SigV4Signer,
31-
baseURL: URL,
32-
options: FaceLivenessSession.Options
30+
baseURL: URL
3331
) {
3432
self.eventStreamEncoder = EventStream.Encoder()
3533
self.eventStreamDecoder = EventStream.Decoder()
3634
self.signer = signer
3735
self.baseURL = baseURL
38-
self.options = options
3936

4037
self.websocket = websocket
4138

@@ -73,14 +70,13 @@ public final class FaceLivenessSession: LivenessService {
7370

7471
public func initializeLivenessStream(withSessionID sessionID: String,
7572
userAgent: String = "",
76-
challenges: [Challenge] = FaceLivenessSession.supportedChallenges) throws {
73+
challenges: [Challenge] = FaceLivenessSession.supportedChallenges,
74+
options: FaceLivenessSession.Options) throws {
7775
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
78-
7976
components?.queryItems = [
8077
URLQueryItem(name: "session-id", value: sessionID),
8178
URLQueryItem(name: "precheck-view-enabled", value: options.preCheckViewEnabled ? "1":"0"),
82-
// TODO: Change this after confirmation
83-
URLQueryItem(name: "attempt-id", value: options.viewId),
79+
URLQueryItem(name: "attempt-count", value: String(options.attemptCount)),
8480
URLQueryItem(name: "challenge-versions",
8581
value: challenges.map({$0.queryParameterString()}).joined(separator: ",")),
8682
URLQueryItem(name: "video-width", value: "480"),

AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSessionRepresentable.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public protocol LivenessService {
2121

2222
func initializeLivenessStream(withSessionID sessionID: String,
2323
userAgent: String,
24-
challenges: [Challenge]) throws
24+
challenges: [Challenge],
25+
options: FaceLivenessSession.Options) throws
2526

2627
func register(
2728
listener: @escaping (FaceLivenessSession.SessionConfiguration) -> Void,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import Amplify
10+
@testable import AWSPredictionsPlugin
11+
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
12+
13+
class LivenessChallengeTests: XCTestCase {
14+
15+
func testFaceMovementChallengeQueryParamterString() {
16+
let challenge = Challenge(version: "1.0.0", type: .faceMovementChallenge)
17+
XCTAssertEqual(challenge.queryParameterString(), "FaceMovementChallenge_1.0.0")
18+
}
19+
20+
func testFaceMovementAndLightChallengeQueryParamterString() {
21+
let challenge = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge)
22+
XCTAssertEqual(challenge.queryParameterString(), "FaceMovementAndLightChallenge_2.0.0")
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import Amplify
10+
@testable import AWSPredictionsPlugin
11+
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
12+
13+
class LivenessDecodingTests: XCTestCase {
14+
15+
// MARK: - ChallengeEvent
16+
/// - Given: A valid json payload depicting a FaceMovementChallenge
17+
/// - When: The payload is decoded
18+
/// - Then: The payload is decoded successfully
19+
func testFacemovementChallengeEventDecodeSuccess() {
20+
let jsonString =
21+
"""
22+
{"Type":"FaceMovementChallenge","Version":"1.0.0"}
23+
"""
24+
25+
do {
26+
let data = jsonString.data(using: .utf8)
27+
guard let data = data else {
28+
XCTFail("Input JSON is invalid")
29+
return
30+
}
31+
let challengeEvent = try JSONDecoder().decode(
32+
ChallengeEvent.self, from: data
33+
)
34+
35+
XCTAssertEqual(challengeEvent.type, ChallengeType.faceMovementChallenge)
36+
XCTAssertEqual(challengeEvent.version, "1.0.0")
37+
} catch {
38+
XCTFail("Decoding failed with error: \(error)")
39+
}
40+
}
41+
42+
/// - Given: A valid json payload depicting a FaceMovementAndLightChallenge
43+
/// - When: The payload is decoded
44+
/// - Then: The payload is decoded successfully
45+
func testFacemovementAndLightChallengeEventDecodeSuccess() {
46+
let jsonString =
47+
"""
48+
{"Type":"FaceMovementAndLightChallenge","Version":"1.0.0"}
49+
"""
50+
51+
do {
52+
let data = jsonString.data(using: .utf8)
53+
guard let data = data else {
54+
XCTFail("Input JSON is invalid")
55+
return
56+
}
57+
let challengeEvent = try JSONDecoder().decode(
58+
ChallengeEvent.self, from: data
59+
)
60+
61+
XCTAssertEqual(challengeEvent.type, ChallengeType.faceMovementAndLightChallenge)
62+
XCTAssertEqual(challengeEvent.version, "1.0.0")
63+
} catch {
64+
XCTFail("Decoding failed with error: \(error)")
65+
}
66+
}
67+
68+
/// - Given: A valid json payload depicting an unknown challenge
69+
/// - When: The payload is decoded
70+
/// - Then: Error is thrown
71+
func testUnknownChallengeEventDecodeFailure() {
72+
let jsonString =
73+
"""
74+
{"Type":"UnknownChallenge","Version":"1.0.0"}
75+
"""
76+
77+
do {
78+
let data = jsonString.data(using: .utf8)
79+
guard let data = data else {
80+
XCTFail("Input JSON is invalid")
81+
return
82+
}
83+
_ = try JSONDecoder().decode(
84+
ChallengeEvent.self, from: data
85+
)
86+
87+
XCTFail("Decoding should fail for unknown challenge")
88+
} catch {
89+
XCTAssertNotNil(error)
90+
}
91+
}
92+
93+
// MARK: - ServerSessionInformationEvent
94+
95+
/// - Given: A valid json payload depicting a ServerSessionInformation
96+
/// containing FaceMovementChallenge
97+
/// - When: The payload is decoded
98+
/// - Then: The payload is decoded successfully
99+
func testFaceMovementChallengeServerSessionInformationEventDecodeSuccess() {
100+
let jsonString =
101+
"""
102+
{\"SessionInformation\":{\"Challenge\":{\"FaceMovementChallenge\":{\"OvalParameters\":{\"Width\":0.1,\"Height\":0.1,\"CenterY\":0.1,\"CenterX\":0.1},\"ChallengeConfig\":{\"BlazeFaceDetectionThreshold\":0.1,\"FaceIouHeightThreshold\":0.1,\"OvalHeightWidthRatio\":0.1,\"OvalIouHeightThreshold\":0.1,\"OvalFitTimeout\":1,\"OvalIouWidthThreshold\":0.1,\"OvalIouThreshold\":0.1,\"FaceDistanceThreshold\":0.1,\"FaceDistanceThresholdMax\":0.1,\"FaceIouWidthThreshold\":0.1,\"FaceDistanceThresholdMin\":0.1}}}}}
103+
"""
104+
105+
do {
106+
let data = jsonString.data(using: .utf8)
107+
guard let data = data else {
108+
XCTFail("Input JSON is invalid")
109+
return
110+
}
111+
let serverSessionInformationEvent = try JSONDecoder().decode(
112+
ServerSessionInformationEvent.self, from: data
113+
)
114+
115+
guard case let .faceMovementChallenge(challenge: recoveredChallenge) =
116+
serverSessionInformationEvent.sessionInformation.challenge.type else {
117+
XCTFail("Cannot decode event from the input JSON")
118+
return
119+
}
120+
121+
XCTAssertEqual(recoveredChallenge.ovalParameters.height, 0.1)
122+
XCTAssertEqual(recoveredChallenge.ovalParameters.width, 0.1)
123+
XCTAssertEqual(recoveredChallenge.ovalParameters.centerX, 0.1)
124+
XCTAssertEqual(recoveredChallenge.ovalParameters.centerY, 0.1)
125+
126+
XCTAssertEqual(recoveredChallenge.challengeConfig.blazeFaceDetectionThreshold, 0.1)
127+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThreshold, 0.1)
128+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThresholdMax, 0.1)
129+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThresholdMin, 0.1)
130+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceIouHeightThreshold, 0.1)
131+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceIouWidthThreshold, 0.1)
132+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalHeightWidthRatio, 0.1)
133+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouHeightThreshold, 0.1)
134+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouThreshold, 0.1)
135+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouWidthThreshold, 0.1)
136+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalFitTimeout, 1)
137+
} catch {
138+
XCTFail("Decoding failed with error: \(error)")
139+
}
140+
}
141+
142+
/// - Given: A valid json payload depicting a ServerSessionInformation
143+
/// containing FaceMovementAndLightChallenge
144+
/// - When: The payload is decoded
145+
/// - Then: The payload is decoded successfully
146+
func testFaceMovementAndLightChallengeServerSessionInformationEventDecodeSuccess() {
147+
let jsonString =
148+
"""
149+
{\"SessionInformation\":{\"Challenge\":{\"FaceMovementAndLightChallenge\":{\"OvalParameters\":{\"Height\":0.1,\"CenterX\":0.1,\"Width\":0.1,\"CenterY\":0.1},\"ColorSequences\":[{\"FreshnessColor\":{\"RGB\":[255,255,255]},\"DownscrollDuration\":0.1,\"FlatDisplayDuration\":0.1}],\"ChallengeConfig\":{\"OvalIouWidthThreshold\":0.1,\"FaceDistanceThreshold\":0.1,\"OvalFitTimeout\":1,\"FaceIouHeightThreshold\":0.1,\"FaceDistanceThresholdMax\":0.1,\"FaceDistanceThresholdMin\":0.1,\"OvalIouHeightThreshold\":0.1,\"FaceIouWidthThreshold\":0.1,\"OvalIouThreshold\":0.1,\"BlazeFaceDetectionThreshold\":0.1,\"OvalHeightWidthRatio\":0.1}}}}}
150+
"""
151+
152+
do {
153+
let data = jsonString.data(using: .utf8)
154+
guard let data = data else {
155+
XCTFail("Input JSON is invalid")
156+
return
157+
}
158+
let serverSessionInformationEvent = try JSONDecoder().decode(
159+
ServerSessionInformationEvent.self, from: data
160+
)
161+
162+
guard case let .faceMovementAndLightChallenge(challenge: recoveredChallenge) =
163+
serverSessionInformationEvent.sessionInformation.challenge.type else {
164+
XCTFail("Cannot decode event from the input JSON")
165+
return
166+
}
167+
168+
XCTAssertEqual(recoveredChallenge.ovalParameters.height, 0.1)
169+
XCTAssertEqual(recoveredChallenge.ovalParameters.width, 0.1)
170+
XCTAssertEqual(recoveredChallenge.ovalParameters.centerX, 0.1)
171+
XCTAssertEqual(recoveredChallenge.ovalParameters.centerY, 0.1)
172+
173+
XCTAssertEqual(recoveredChallenge.challengeConfig.blazeFaceDetectionThreshold, 0.1)
174+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThreshold, 0.1)
175+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThresholdMax, 0.1)
176+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceDistanceThresholdMin, 0.1)
177+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceIouHeightThreshold, 0.1)
178+
XCTAssertEqual(recoveredChallenge.challengeConfig.faceIouWidthThreshold, 0.1)
179+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalHeightWidthRatio, 0.1)
180+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouHeightThreshold, 0.1)
181+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouThreshold, 0.1)
182+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalIouWidthThreshold, 0.1)
183+
XCTAssertEqual(recoveredChallenge.challengeConfig.ovalFitTimeout, 1)
184+
185+
XCTAssertEqual(recoveredChallenge.colorSequences.count, 1)
186+
XCTAssertEqual(recoveredChallenge.colorSequences.first?.downscrollDuration, 0.1)
187+
XCTAssertEqual(recoveredChallenge.colorSequences.first?.flatDisplayDuration, 0.1)
188+
XCTAssertEqual(recoveredChallenge.colorSequences.first?.freshnessColor.rgb, [255,255,255])
189+
} catch {
190+
XCTFail("Decoding failed with error: \(error)")
191+
}
192+
}
193+
194+
/// - Given: A valid json payload depicting a ServerSessionInformation
195+
/// containing unknown challenge
196+
/// - When: The payload is decoded
197+
/// - Then: Error should be thrown
198+
func testUnknownChallengeServerSessionInformationEventDecodeFailure() {
199+
let jsonString =
200+
"""
201+
{\"SessionInformation\":{\"Challenge\":{\"UnknownChallenge\":{\"OvalParameters\":{\"Height\":0.1,\"CenterX\":0.1,\"Width\":0.1,\"CenterY\":0.1},\"ColorSequences\":[{\"FreshnessColor\":{\"RGB\":[255,255,255]},\"DownscrollDuration\":0.1,\"FlatDisplayDuration\":0.1}],\"ChallengeConfig\":{\"OvalIouWidthThreshold\":0.1,\"FaceDistanceThreshold\":0.1,\"OvalFitTimeout\":1,\"FaceIouHeightThreshold\":0.1,\"FaceDistanceThresholdMax\":0.1,\"FaceDistanceThresholdMin\":0.1,\"OvalIouHeightThreshold\":0.1,\"FaceIouWidthThreshold\":0.1,\"OvalIouThreshold\":0.1,\"BlazeFaceDetectionThreshold\":0.1,\"OvalHeightWidthRatio\":0.1}}}}}
202+
"""
203+
204+
do {
205+
let data = jsonString.data(using: .utf8)
206+
guard let data = data else {
207+
XCTFail("Input JSON is invalid")
208+
return
209+
}
210+
let serverSessionInformationEvent = try JSONDecoder().decode(
211+
ServerSessionInformationEvent.self, from: data
212+
)
213+
214+
XCTFail("Decoding should fail for unknown challenge")
215+
} catch {
216+
XCTAssertNotNil(error)
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)