main
swift 206 lines 8.69 KB
Raw
1 import Foundation
2 import XCTest
3
4 @testable import AuthCore
5
6 final class AuthCoreTests: XCTestCase {
7
8 // MARK: - Session
9
10 func testSessionScopeJoinsScopesWithSpaces() {
11 let session = SmbCloudSession(accessToken: "token", scopes: ["openid", "profile", "email"])
12 XCTAssertEqual(session.scope, "openid profile email")
13 XCTAssertEqual(session.authorizationHeaderValue, "Bearer token")
14 }
15
16 func testSessionValidityReflectsExpiryDate() {
17 let future = SmbCloudSession(accessToken: "f", expiresAt: Date().addingTimeInterval(300))
18 let past = SmbCloudSession(accessToken: "p", expiresAt: Date().addingTimeInterval(-300))
19 XCTAssertTrue(future.isValid())
20 XCTAssertFalse(future.isExpired)
21 XCTAssertFalse(past.isValid(leeway: 0))
22 XCTAssertTrue(past.isExpired)
23 }
24
25 func testSessionCodableRoundTripPreservesValues() throws {
26 let original = SmbCloudSession(
27 accessToken: "access-token",
28 refreshToken: "refresh-token",
29 idToken: "id-token",
30 tokenType: "Bearer",
31 expiresAt: Date(timeIntervalSince1970: 1_750_000_000),
32 scopes: ["openid", "profile", "email"]
33 )
34 let data = try JSONEncoder().encode(original)
35 let decoded = try JSONDecoder().decode(SmbCloudSession.self, from: data)
36 XCTAssertEqual(decoded, original)
37 }
38
39 func testSessionFromTokenResponseMapsFields() {
40 let now = Date(timeIntervalSince1970: 1_000_000)
41 let token = OIDC.TokenResponse(
42 accessToken: "at", tokenType: "Bearer", expiresIn: 3600,
43 refreshToken: "rt", scope: "openid profile openid", idToken: "it")
44 let session = SmbCloudSession(tokenResponse: token, now: now)
45 XCTAssertEqual(session.accessToken, "at")
46 XCTAssertEqual(session.refreshToken, "rt")
47 XCTAssertEqual(session.idToken, "it")
48 XCTAssertEqual(session.expiresAt, now.addingTimeInterval(3600))
49 XCTAssertEqual(session.scopes, ["openid", "profile"]) // deduped
50 }
51
52 // MARK: - Credentials store
53
54 func testInMemoryCredentialsStoreRoundTrips() throws {
55 let store = SmbCloudInMemoryCredentialsStore()
56 XCTAssertNil(try store.current())
57
58 try store.store(
59 SmbCloudSession(
60 accessToken: "access-token",
61 expiresAt: Date().addingTimeInterval(300),
62 scopes: ["openid", "openid", " profile "]))
63
64 XCTAssertEqual(try store.current()?.scopes, ["openid", "profile"])
65 XCTAssertTrue(try store.hasValidSession())
66 XCTAssertNotNil(try store.currentValidSession())
67
68 try store.clear()
69 XCTAssertNil(try store.current())
70 }
71
72 func testExpiredSessionIsNotReturnedAsValid() throws {
73 let store = SmbCloudInMemoryCredentialsStore(
74 session: SmbCloudSession(accessToken: "stale", expiresAt: Date().addingTimeInterval(-300)))
75 XCTAssertNotNil(try store.current())
76 XCTAssertNil(try store.currentValidSession())
77 XCTAssertFalse(try store.hasValidSession())
78 }
79
80 // MARK: - Environment
81
82 func testEnvironmentHostsAndProtocols() {
83 XCTAssertEqual(SmbCloudEnvironment.dev.apiProtocol, "http")
84 XCTAssertEqual(SmbCloudEnvironment.dev.apiHost, "localhost:8088")
85 XCTAssertEqual(SmbCloudEnvironment.production.apiProtocol, "https")
86 XCTAssertEqual(SmbCloudEnvironment.production.apiHost, "api.smbcloud.xyz")
87 XCTAssertEqual(SmbCloudEnvironment.production.baseURL.absoluteString, "https://api.smbcloud.xyz/")
88 }
89
90 // MARK: - ClientCredentials URL building
91
92 func testTenantURLCarriesCredentialsAsQuery() {
93 let creds = ClientCredentials(appId: "app-1", appSecret: "secret-1")
94 let url = creds.tenantURL(environment: .production, path: "v1/client/me")
95 let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
96 XCTAssertEqual(comps?.scheme, "https")
97 XCTAssertEqual(comps?.host, "api.smbcloud.xyz")
98 XCTAssertEqual(comps?.path, "/v1/client/me")
99 XCTAssertEqual(query(url, "client_id"), "app-1")
100 XCTAssertEqual(query(url, "client_secret"), "secret-1")
101 }
102
103 func testTenantURLDevPreservesPort() {
104 let creds = ClientCredentials(appId: "a", appSecret: "s")
105 let url = creds.tenantURL(environment: .dev, path: "v1/client/users/sign_in")
106 let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
107 XCTAssertEqual(comps?.host, "localhost")
108 XCTAssertEqual(comps?.port, 8088)
109 XCTAssertEqual(comps?.scheme, "http")
110 }
111
112 // MARK: - OIDC
113
114 func testOIDCAuthorizationRequestProducesPkceChallenge() throws {
115 let request = try OIDC.buildAuthorizationRequest(
116 environment: .production, oidcClientId: "oidc-1", redirectURI: "myapp://auth/callback")
117 let url = request.authorizeURL
118 XCTAssertEqual(
119 URLComponents(url: url, resolvingAgainstBaseURL: false)?.path, "/v1/client/oauth/authorize")
120 XCTAssertEqual(query(url, "response_type"), "code")
121 XCTAssertEqual(query(url, "code_challenge_method"), "S256")
122 XCTAssertEqual(query(url, "client_id"), "oidc-1")
123 XCTAssertEqual(query(url, "scope"), "openid profile email")
124 XCTAssertEqual(query(url, "state"), request.state)
125 XCTAssertNotNil(query(url, "code_challenge"))
126 XCTAssertEqual(request.codeVerifier.count, 64) // two uuid-simple strings
127 }
128
129 func testOIDCParseCallbackURL() throws {
130 let payload = try OIDC.parseCallbackURL("myapp://cb?code=abc&state=xyz")
131 XCTAssertEqual(payload.code, "abc")
132 XCTAssertEqual(payload.state, "xyz")
133 }
134
135 func testOIDCParseCallbackURLMissingParamsThrows() {
136 XCTAssertThrowsError(try OIDC.parseCallbackURL("myapp://cb?code=abc")) { error in
137 XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
138 }
139 }
140
141 // MARK: - Apple provider
142
143 func testBuildAppleAuthorizationRequest() {
144 let client = AuthCoreClient(
145 environment: .production, credentials: .init(appId: "app-1", appSecret: "secret-1"))
146 let request = client.buildAppleAuthorizationRequest(redirectURI: "myapp://cb", state: "the-state")
147 XCTAssertEqual(request.state, "the-state")
148 XCTAssertEqual(
149 URLComponents(url: request.authorizeURL, resolvingAgainstBaseURL: false)?.path,
150 "/v1/client/oauth/apple/authorize")
151 XCTAssertEqual(query(request.authorizeURL, "client_id"), "app-1")
152 XCTAssertEqual(query(request.authorizeURL, "redirect_uri"), "myapp://cb")
153 XCTAssertEqual(query(request.authorizeURL, "state"), "the-state")
154 }
155
156 func testParseAppleCallbackURL() throws {
157 let client = AuthCoreClient(
158 environment: .production, credentials: .init(appId: "a", appSecret: "s"))
159 let session = try client.parseAppleCallbackURL(
160 "myapp://cb?access_token=AT&provider_account_id=PA&email=e@x.com&state=S",
161 expectedState: "S")
162 XCTAssertEqual(session.accessToken, "AT")
163 XCTAssertEqual(session.providerAccountId, "PA")
164 XCTAssertEqual(session.email, "e@x.com")
165 XCTAssertEqual(session.provider, "apple")
166 XCTAssertEqual(session.state, "S")
167 }
168
169 func testParseAppleCallbackURLStateMismatchThrows() {
170 let client = AuthCoreClient(
171 environment: .production, credentials: .init(appId: "a", appSecret: "s"))
172 XCTAssertThrowsError(
173 try client.parseAppleCallbackURL(
174 "myapp://cb?access_token=AT&provider_account_id=PA&state=S", expectedState: "other")
175 ) { error in
176 XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
177 }
178 }
179
180 func testParseAppleCallbackURLPropagatesError() {
181 let client = AuthCoreClient(
182 environment: .production, credentials: .init(appId: "a", appSecret: "s"))
183 XCTAssertThrowsError(
184 try client.parseAppleCallbackURL("myapp://cb?error=access_denied")
185 ) { error in
186 XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
187 XCTAssertEqual((error as? SmbCloudError)?.message, "access_denied")
188 }
189 }
190
191 // MARK: - Errors
192
193 func testErrorCodeRawValuesMatchContract() {
194 XCTAssertEqual(SmbCloudErrorCode.unauthorized.rawValue, 100)
195 XCTAssertEqual(SmbCloudErrorCode.emailNotVerified.rawValue, 1001)
196 XCTAssertEqual(AccountErrorCode.emailUnverified.rawValue, 1001)
197 XCTAssertEqual(SmbCloudError(code: .unauthorized).message, "Unauthorized access.")
198 }
199
200 // MARK: - Helpers
201
202 private func query(_ url: URL, _ name: String) -> String? {
203 URLComponents(url: url, resolvingAgainstBaseURL: false)?
204 .queryItems?.first { $0.name == name }?.value
205 }
206 }