Skip to content

Commit ac1b267

Browse files
authored
Mike/session tests (#200)
* disable concurrent testing in ci * session tests
1 parent 56f9776 commit ac1b267

6 files changed

Lines changed: 196 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ jobs:
6767
set -o pipefail
6868
xcodebuild test \
6969
-scheme Clerk \
70-
-destination "platform=iOS Simulator,name=iPhone 16 Pro" | xcpretty
70+
-destination "platform=iOS Simulator,name=iPhone 16 Pro" \
71+
-disable-concurrent-testing | xcpretty

Sources/Clerk/Models/Mocks/MockModels.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extension Client {
1111

1212
static var mock: Client {
1313
return Client(
14-
id: "1",
14+
id: "sess_1",
1515
signIn: .mock,
1616
signUp: .mock,
1717
sessions: [.mock],
@@ -58,6 +58,14 @@ extension Session {
5858

5959
}
6060

61+
extension TokenResource {
62+
63+
static var mock: TokenResource {
64+
.init(jwt: "jwt")
65+
}
66+
67+
}
68+
6169
extension SignIn {
6270

6371
static var mock: SignIn {

Sources/Clerk/Models/Session.swift renamed to Sources/Clerk/Models/Session/Session.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,7 @@ extension Session {
162162
/// Marks this session as revoked. If this is the active session, the attempt to revoke it will fail. Users can revoke only their own sessions.
163163
@discardableResult @MainActor
164164
public func revoke() async throws -> Session {
165-
let request = ClerkFAPI.v1.me.sessions.withId(id: id).revoke.post(
166-
queryItems: [.init(name: "_clerk_session_id", value: Clerk.shared.session?.id)]
167-
)
168-
169-
let response = try await Container.shared.apiClient().send(request)
170-
return response.value.response
165+
try await Container.shared.sessionService().revoke(self)
171166
}
172167

173168
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// SessionService.swift
3+
// Clerk
4+
//
5+
// Created by Mike Pitre on 3/11/25.
6+
//
7+
8+
import Factory
9+
import Foundation
10+
11+
struct SessionService {
12+
var revoke: (_ session: Session) async throws -> Session
13+
}
14+
15+
extension SessionService {
16+
17+
static var liveValue: Self {
18+
.init(
19+
revoke: { session in
20+
let request = ClerkFAPI.v1.me.sessions.withId(id: session.id).revoke.post(
21+
queryItems: [.init(name: "_clerk_session_id", value: await Clerk.shared.session?.id)]
22+
)
23+
return try await Container.shared.apiClient().send(request).value.response
24+
}
25+
)
26+
}
27+
28+
}
29+
30+
extension Container {
31+
32+
var sessionService: Factory<SessionService> {
33+
self { .liveValue }
34+
}
35+
36+
}

Sources/Clerk/APIClient/SessionTokenFetcher.swift renamed to Sources/Clerk/Models/Session/SessionTokenFetcher.swift

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import Foundation
1111
// The purpose of this actor is to NOT trigger refreshes of tokens if a refresh is already in progress.
1212
// This is not a token cache. It is only responsible to returning in progress tasks to refresh a token.
1313
actor SessionTokenFetcher {
14-
static let shared = SessionTokenFetcher()
14+
static var shared = SessionTokenFetcher()
1515

1616
// Key is `tokenCacheKey` property of a `session`
17-
private var tokenTasks: [String: Task<TokenResource?, Error>] = [:]
17+
var tokenTasks: [String: Task<TokenResource?, Error>] = [:]
1818

1919
func getToken(_ session: Session, options: Session.GetTokenOptions = .init()) async throws -> TokenResource? {
2020

@@ -43,44 +43,39 @@ actor SessionTokenFetcher {
4343
*/
4444
@discardableResult @MainActor
4545
func fetchToken(_ session: Session, options: Session.GetTokenOptions = .init()) async throws -> TokenResource? {
46-
47-
let cacheKey = session.tokenCacheKey(template: options.template)
48-
49-
if options.skipCache == false,
50-
let token = await SessionTokensCache.shared.getToken(cacheKey: cacheKey),
51-
let expiresAt = token.decodedJWT?.expiresAt,
52-
Date.now.distance(to: expiresAt) > options.expirationBuffer
53-
{
54-
return token
55-
}
56-
57-
var token: TokenResource?
58-
59-
let tokensRequest = ClerkFAPI.v1.client.sessions.id(session.id).tokens
60-
61-
if let template = options.template {
62-
let templateTokenRequest = tokensRequest
63-
.template(template)
64-
.post()
65-
66-
token = try await Container.shared.apiClient().send(templateTokenRequest).value
67-
} else {
68-
let defaultTokenRequest = tokensRequest.post()
69-
70-
token = try await Container.shared.apiClient().send(defaultTokenRequest).value
71-
}
72-
73-
if let token {
74-
await SessionTokensCache.shared.insertToken(token, cacheKey: cacheKey)
75-
}
76-
77-
return token
46+
let cacheKey = session.tokenCacheKey(template: options.template)
47+
48+
if options.skipCache == false,
49+
let token = await SessionTokensCache.shared.getToken(cacheKey: cacheKey),
50+
let expiresAt = token.decodedJWT?.expiresAt,
51+
Date.now.distance(to: expiresAt) > options.expirationBuffer
52+
{
53+
return token
54+
}
55+
56+
var token: TokenResource?
57+
58+
let tokensRequest = ClerkFAPI.v1.client.sessions.id(session.id).tokens
59+
60+
if let template = options.template {
61+
let templateTokenRequest = tokensRequest.template(template).post()
62+
token = try await Container.shared.apiClient().send(templateTokenRequest).value
63+
} else {
64+
let defaultTokenRequest = tokensRequest.post()
65+
token = try await Container.shared.apiClient().send(defaultTokenRequest).value
66+
}
67+
68+
if let token {
69+
await SessionTokensCache.shared.insertToken(token, cacheKey: cacheKey)
70+
}
71+
72+
return token
7873
}
7974

8075
}
8176

8277
actor SessionTokensCache {
83-
static var shared = SessionTokensCache()
78+
static let shared = SessionTokensCache()
8479

8580
private var cache: [String: TokenResource] = [:]
8681

@@ -89,7 +84,7 @@ actor SessionTokensCache {
8984
/// For example, `sess_abc12345` or `sess_abc12345-supabase`.
9085
/// - Returns: ``TokenResource``
9186
func getToken(cacheKey: String) -> TokenResource? {
92-
return cache[cacheKey]
87+
cache[cacheKey]
9388
}
9489

9590
/// Inserts a session token into the cache.

Tests/SessionTests.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import ConcurrencyExtras
2+
import Factory
3+
import Foundation
4+
import Mocker
5+
import Testing
6+
7+
@testable import Clerk
8+
9+
// Any test that accesses Container.shared or performs networking
10+
// should be placed in the serialized tests below
11+
12+
@Suite struct SessionTests {
13+
14+
@Test func testTokenCacheKeyWithoutTemplate() {
15+
let session = Session.mock
16+
#expect(session.tokenCacheKey(template: nil) == session.id)
17+
}
18+
19+
@Test func testTokenCacheKeyWithTemplate() {
20+
let session = Session.mock
21+
#expect(session.tokenCacheKey(template: "supabase") == "\(session.id)-supabase")
22+
}
23+
24+
@Test func testGetTokenOptionsExpirationBuffer() {
25+
let options = Session.GetTokenOptions(expirationBuffer: 120)
26+
#expect(options.expirationBuffer == 60)
27+
}
28+
29+
@Test func testGetTokenOptionsDefaults() {
30+
let options = Session.GetTokenOptions()
31+
#expect(options.template == nil)
32+
#expect(options.expirationBuffer == 10)
33+
#expect(options.skipCache == false)
34+
}
35+
36+
@Test func testGetTokenOptionsCustomValues() {
37+
let options = Session.GetTokenOptions(
38+
template: "firebase",
39+
expirationBuffer: 30,
40+
skipCache: true
41+
)
42+
#expect(options.template == "firebase")
43+
#expect(options.expirationBuffer == 30)
44+
#expect(options.skipCache == true)
45+
}
46+
}
47+
48+
@Suite(.serialized) struct SessionSerializedTests {
49+
50+
init() {
51+
Container.shared.reset()
52+
}
53+
54+
@MainActor
55+
@Test func testRevokeRequest() async throws {
56+
let requestHandled = LockIsolated(false)
57+
let session = Session.mock
58+
let originalUrl = mockBaseUrl.appending(path: "/v1/me/sessions/\(session.id)/revoke")
59+
var mock = Mock(url: originalUrl, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
60+
.post: try! JSONEncoder.clerkEncoder.encode(ClientResponse<Session>.init(response: .mock, client: .mock))
61+
])
62+
mock.onRequestHandler = OnRequestHandler { request in
63+
#expect(request.httpMethod == "POST")
64+
#expect(request.url!.query()!.contains("_clerk_session_id"))
65+
requestHandled.setValue(true)
66+
}
67+
mock.register()
68+
try await session.revoke()
69+
#expect(requestHandled.value)
70+
}
71+
72+
@MainActor
73+
@Test func testFetchTokenRequestWithoutTemplate() async throws {
74+
let requestHandled = LockIsolated(false)
75+
let session = Session.mock
76+
let originalUrl = mockBaseUrl.appending(path: "v1/client/sessions/\(session.id)/tokens")
77+
var mock = Mock(url: originalUrl, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
78+
.post: try! JSONEncoder.clerkEncoder.encode(TokenResource.mock)
79+
])
80+
mock.onRequestHandler = OnRequestHandler { request in
81+
#expect(request.httpMethod == "POST")
82+
requestHandled.setValue(true)
83+
}
84+
mock.register()
85+
try await session.getToken()
86+
#expect(requestHandled.value)
87+
}
88+
89+
@MainActor
90+
@Test func testFetchTokenRequestWithTemplate() async throws {
91+
let requestHandled = LockIsolated(false)
92+
let session = Session.mock
93+
let originalUrl = mockBaseUrl.appending(path: "v1/client/sessions/\(session.id)/tokens/supabase")
94+
var mock = Mock(url: originalUrl, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
95+
.post: try! JSONEncoder.clerkEncoder.encode(TokenResource.mock)
96+
])
97+
mock.onRequestHandler = OnRequestHandler { request in
98+
#expect(request.httpMethod == "POST")
99+
requestHandled.setValue(true)
100+
}
101+
mock.register()
102+
try await session.getToken(.init(template: "supabase"))
103+
#expect(requestHandled.value)
104+
}
105+
106+
@MainActor
107+
@Test func testFetchTokenStoresTokenInCache() async throws {
108+
let token = try await Session.mock.getToken()
109+
#expect(token == .mock)
110+
111+
let cacheKey = Session.mock.tokenCacheKey(template: nil)
112+
let tokenFromCache = await SessionTokensCache.shared.getToken(cacheKey: cacheKey)
113+
#expect(tokenFromCache == .mock)
114+
}
115+
}
116+
117+

0 commit comments

Comments
 (0)