this repo has no description
1import CryptoKit
2import Foundation
3import Testing
4@testable import CoreATProtocol
5
6private struct DeterministicRandomGenerator: RandomDataGenerating {
7 func data(count: Int) throws -> Data {
8 Data(repeating: 0x42, count: count)
9 }
10}
11
12@Test("Base64URL encodes without padding and decodes back")
13func base64URLRoundTrip() throws {
14 let data = Data([0xde, 0xad, 0xbe, 0xef])
15 let encoded = Base64URL.encode(data)
16 #expect(encoded.contains("=") == false)
17 let decoded = try Base64URL.decode(encoded)
18 #expect(decoded == data)
19}
20
21@Test("PKCE generator creates verifier within bounds and matching challenge")
22func pkceGeneratorProducesExpectedValues() throws {
23 let generator = PKCEGenerator(randomGenerator: DeterministicRandomGenerator())
24 let values = try generator.makeValues()
25 #expect(values.verifier.count >= 43)
26 #expect(values.verifier.count <= 128)
27
28 let expectedDigest = SHA256.hash(data: Data(values.verifier.utf8))
29 let expectedChallenge = Base64URL.encode(Data(expectedDigest))
30 #expect(values.challenge == expectedChallenge)
31}
32
33@Test("DPoP generator signs payload with expected claims")
34func dpopGeneratorProducesValidProof() async throws {
35 let keyPair = DPoPKeyPair()
36 let generator = await DPoPGenerator(clock: { Date(timeIntervalSince1970: 1_700_000_000) })
37 try await generator.updateKey(using: keyPair.export())
38 let url = URL(string: "https://example.com/resource")!
39 let proof = try await generator.generateProof(
40 method: "GET",
41 url: url,
42 nonce: "nonce-value",
43 accessToken: "access-token"
44 )
45
46 let components = proof.split(separator: ".")
47 #expect(components.count == 3)
48
49 let headerData = try Base64URL.decode(String(components[0]))
50 let payloadData = try Base64URL.decode(String(components[1]))
51 let signatureData = try Base64URL.decode(String(components[2]))
52
53 let header = try JSONSerialization.jsonObject(with: headerData) as? [String: Any]
54 let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
55
56 #expect(header?["typ"] as? String == "dpop+jwt")
57 #expect(header?["alg"] as? String == "ES256")
58 let jwk = header?["jwk"] as? [String: String]
59 #expect(jwk?["kty"] == "EC")
60 #expect(jwk?["crv"] == "P-256")
61
62 #expect(payload?["htm"] as? String == "GET")
63 #expect(payload?["htu"] as? String == "https://example.com/resource")
64 #expect(payload?["nonce"] as? String == "nonce-value")
65 #expect(payload?["ath"] as? String == Base64URL.encode(Data(SHA256.hash(data: Data("access-token".utf8)))))
66
67 if let iat = payload?["iat"] as? Int {
68 #expect(iat == 1_700_000_000)
69 } else {
70 Issue.record("DPoP payload missing iat")
71 }
72
73 let signingInput = Data((components[0] + "." + components[1]).utf8)
74 let signature = try P256.Signing.ECDSASignature(derRepresentation: signatureData)
75 #expect(keyPair.privateKey.publicKey.isValidSignature(signature, for: signingInput))
76}
77
78@Test("OAuth session refresh heuristics")
79func oauthSessionRefreshLogic() {
80 let issuedAt = Date()
81 let session = OAuthSession(
82 did: "did:plc:example",
83 pdsURL: URL(string: "https://pds.example.com")!,
84 authorizationServer: URL(string: "https://auth.example.com")!,
85 tokenEndpoint: URL(string: "https://auth.example.com/token")!,
86 accessToken: "token",
87 refreshToken: "refresh",
88 tokenType: "DPoP",
89 scope: "atproto",
90 expiresIn: 3600,
91 issuedAt: issuedAt
92 )
93
94 #expect(session.isExpired(relativeTo: issuedAt.addingTimeInterval(3500)) == false)
95 #expect(session.needsRefresh(relativeTo: issuedAt.addingTimeInterval(3300), threshold: 400))
96 #expect(session.isExpired(relativeTo: issuedAt.addingTimeInterval(3600)))
97}