Skip to content

Commit

Permalink
Migrate ClaimTests
Browse files Browse the repository at this point in the history
  • Loading branch information
0xTim committed Sep 17, 2024
1 parent 0601ef5 commit 8a01876
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 4 deletions.
6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ let package = Package(
.copy("TestCertificates"),
]
),
.testTarget(
name: "JWTKitNewTests",
dependencies: [
"JWTKit",
]
),
],
swiftLanguageModes: [.v6]
)
16 changes: 12 additions & 4 deletions Sources/JWTKit/JWTError.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Foundation

/// JWT error type.
public struct JWTError: Error, Sendable {
public struct ErrorType: Sendable, Hashable, CustomStringConvertible {
enum Base: String, Sendable {
public struct JWTError: Error, Sendable, Equatable {
public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable {
enum Base: String, Sendable, Equatable {
case claimVerificationFailure
case signingAlgorithmFailure
case malformedToken
Expand Down Expand Up @@ -46,7 +46,7 @@ public struct JWTError: Error, Sendable {
}
}

private struct Backing: Sendable {
private struct Backing: Sendable, Equatable {
fileprivate let errorType: ErrorType
fileprivate let name: String?
fileprivate let reason: String?
Expand Down Expand Up @@ -75,6 +75,10 @@ public struct JWTError: Error, Sendable {
self.failedClaim = failedClaim
self.curve = curve
}

static func == (lhs: JWTError.Backing, rhs: JWTError.Backing) -> Bool {
lhs.errorType == rhs.errorType
}
}

private var backing: Backing
Expand Down Expand Up @@ -143,6 +147,10 @@ public struct JWTError: Error, Sendable {
public static func generic(identifier: String, reason: String) -> Self {
.init(backing: .init(errorType: .generic, reason: reason))
}

public static func == (lhs: JWTError, rhs: JWTError) -> Bool {
lhs.backing == rhs.backing
}
}

extension JWTError: CustomStringConvertible {
Expand Down
106 changes: 106 additions & 0 deletions Tests/JWTKitNewTests/ClaimTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import JWTKit
import Testing
import Foundation

@Suite("Claim Tests")
struct ClaimTests {
@Test("Test Claim with Boolean")
func boolClaim() async throws {
let payload = #"{"trueStr":"true","trueBool":true,"falseStr":"false","falseBool":false}"#
var data = Data(payload.utf8)
let decoded = try! JSONDecoder().decode(BoolPayload.self, from: data)

#expect(decoded.trueStr.value == true)
#expect(decoded.trueBool.value == true)
#expect(decoded.falseBool.value == false)
#expect(decoded.falseStr.value == false)

data = Data(#"{"bad":"Not boolean"}"#.utf8)
#expect(throws: DecodingError.self) {
try JSONDecoder().decode(BoolPayload.self, from: data)
}
}

@Test("Test Claim with Locale")
func localeClaim() async throws {
let ptBR = #"{"locale":"pt-BR"}"#

let plainEnglish = try LocalePayload.from(#"{"locale":"en"}"#)
let brazillianPortugese = try LocalePayload.from(ptBR)
let nadizaDialectSlovenia = try LocalePayload.from(#"{"locale":"sl-nedis"}"#)
let germanSwissPost1996 = try LocalePayload.from(#"{"locale":"de-CH-1996"}"#)
let chineseTraditionalTwoPrivate = try LocalePayload.from(#"{"locale":"zh-Hant-CN-x-private1-private2"}"#)

#expect(plainEnglish.locale.value.identifier == "en")
#expect(brazillianPortugese.locale.value.identifier == "pt-BR")
#expect(nadizaDialectSlovenia.locale.value.identifier == "sl-nedis")
#expect(germanSwissPost1996.locale.value.identifier == "de-CH-1996")
#expect(chineseTraditionalTwoPrivate.locale.value.identifier == "zh-Hant-CN-x-private1-private2")

let encoded = try JSONEncoder().encode(brazillianPortugese)
let string = String(bytes: encoded, encoding: .utf8)!
#expect(string == ptBR)
}

@Test("Test Claim with Sindle Audience")
func singleAudienceClaim() async throws {
let id = UUID()
let str = "{\"audience\":\"\(id.uuidString)\"}"
let data = Data(str.utf8)
let decoded = try! JSONDecoder().decode(AudiencePayload.self, from: data)

#expect(decoded.audience.value == [id.uuidString])
#expect(throws: Never.self) {
try decoded.audience.verifyIntendedAudience(includes: id.uuidString)
}
#expect {
try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)
} throws : { error in
guard let jwtError = error as? JWTError else { return false }
return jwtError.errorType == .claimVerificationFailure
&& jwtError.failedClaim is AudienceClaim
&& (jwtError.failedClaim as? AudienceClaim)?.value == [id.uuidString]
}
}

@Test("Test Claim with Multiple Audiences")
func multipleAudienceClaims() async throws {
let id1 = UUID(), id2 = UUID()
let str = "{\"audience\":[\"\(id1.uuidString)\", \"\(id2.uuidString)\"]}"
let data = Data(str.utf8)
let decoded = try! JSONDecoder().decode(AudiencePayload.self, from: data)

#expect(decoded.audience.value == [id1.uuidString, id2.uuidString])
#expect(throws: Never.self) {
try decoded.audience.verifyIntendedAudience(includes: id1.uuidString)
}
#expect(throws: Never.self) {
try decoded.audience.verifyIntendedAudience(includes: id2.uuidString)
}
#expect {
try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)
} throws : { error in
guard let jwtError = error as? JWTError else { return false }
return jwtError.errorType == .claimVerificationFailure
&& jwtError.failedClaim is AudienceClaim
&& (jwtError.failedClaim as? AudienceClaim)?.value == [id1.uuidString, id2.uuidString]
}
}

@Test("Test Expiration Encoding")
func expirationEncoding() async throws {
let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000))
let parser = DefaultJWTParser()
let keyCollection = await JWTKeyCollection().add(hmac: .init(from: "secret".bytes), digestAlgorithm: .sha256, parser: parser)
let jwt = try await keyCollection.sign(ExpirationPayload(exp: exp))
let parsed = try parser.parse(jwt.bytes, as: ExpirationPayload.self)
let header = parsed.header

let typ = try #require(header.typ)
#expect(typ == "JWT")
let alg = try #require(header.alg)
#expect(alg == "HS256")
#expect(parsed.payload.exp == exp)
_ = try await keyCollection.verify(jwt, as: ExpirationPayload.self)
}
}
5 changes: 5 additions & 0 deletions Tests/JWTKitNewTests/Helpers/String+bytes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extension String {
var bytes: [UInt8] {
.init(utf8)
}
}
5 changes: 5 additions & 0 deletions Tests/JWTKitNewTests/Types/AudiencePayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import JWTKit

struct AudiencePayload: Codable {
var audience: AudienceClaim
}
8 changes: 8 additions & 0 deletions Tests/JWTKitNewTests/Types/BoolPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import JWTKit

struct BoolPayload: Decodable {
var trueStr: BoolClaim
var trueBool: BoolClaim
var falseStr: BoolClaim
var falseBool: BoolClaim
}
9 changes: 9 additions & 0 deletions Tests/JWTKitNewTests/Types/ExpirationPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import JWTKit

struct ExpirationPayload: JWTPayload {
var exp: ExpirationClaim

func verify(using _: some JWTAlgorithm) throws {
try self.exp.verifyNotExpired()
}
}
13 changes: 13 additions & 0 deletions Tests/JWTKitNewTests/Types/LocalePayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import JWTKit
import Foundation

struct LocalePayload: Codable {
var locale: LocaleClaim
}

extension LocalePayload {
static func from(_ string: String) throws -> LocalePayload {
let data = string.data(using: .utf8)!
return try JSONDecoder().decode(LocalePayload.self, from: data)
}
}
12 changes: 12 additions & 0 deletions Tests/JWTKitNewTests/Types/TestPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import JWTKit

struct TestPayload: JWTPayload, Equatable {
var sub: SubjectClaim
var name: String
var admin: Bool
var exp: ExpirationClaim

func verify(using _: some JWTAlgorithm) throws {
try exp.verifyNotExpired()
}
}

0 comments on commit 8a01876

Please sign in to comment.