import "package:flutter_test/flutter_test.dart"; import "package:garage_entitlements/src/jwks_verify.dart"; import "package:garage_entitlements/src/models.dart"; import "package:pointycastle/export.dart"; import "keys.dart"; void main() { late RSAPublicKey pub; late RSAPrivateKey priv; late Map jwks; setUpAll(() { final pair = genKey(1); pub = pair.publicKey as RSAPublicKey; priv = pair.privateKey as RSAPrivateKey; jwks = jwksOf(pub); }); GarageKey verify(String token, { Map? doc, String iss = "https://pay.imbenji.net", String project = "field-notes", String sku = "pro", String sub = "user-123", String mode = "live", }) => verifyKey( token, doc ?? jwks, expectedIssuer: iss, expectedProject: project, expectedSku: sku, expectedSub: sub, expectedMode: mode, ); String? codeOf(Object? e) => e is EntitlementsError ? e.code : null; test("verifies a signed key and reads every claim", () { final ends = "2027-03-01T00:00:00.000Z"; final key = verify( keyToken( priv, kind: "subscription", entitlementExpiresAt: ends, ), ); expect(key.subject, "user-123"); expect(key.issuer, "https://pay.imbenji.net"); expect(key.project, "field-notes"); expect(key.sku, "pro"); expect(key.kind, "subscription"); expect(key.mode, "live"); expect(key.isSubscription, isTrue); expect(key.isExpired, isFalse); // the two clocks are separate things and both survive the round trip expect(key.entitlementExpiresAt, DateTime.parse(ends).toUtc()); expect(key.expiresAt.isBefore(DateTime.parse(ends)), isTrue); }); test("a one-off owned outright carries no entitlement expiry", () { final key = verify(keyToken(priv)); expect(key.kind, "one_off"); expect(key.entitlementExpiresAt, isNull); }); test("a tampered signature is refused", () { final good = keyToken(priv); final tampered = "${good.substring(0, good.length - 4)}AAAA"; expect( () => verify(tampered), throwsA(predicate((e) => codeOf(e) == "bad_signature")), ); }); test("a key signed by somebody elses key is refused", () { final other = genKey(9); final token = keyToken(other.privateKey as RSAPrivateKey); expect( () => verify(token), throwsA(predicate((e) => codeOf(e) == "bad_signature")), ); }); test("iss mismatch", () { final token = keyToken(priv, iss: "https://not-us.example"); expect( () => verify(token), throwsA(predicate((e) => codeOf(e) == "iss_mismatch")), ); }); test("aud mismatch — right project, wrong sku", () { // the exact thing aud exists to stop: a key for the cheap tier being // handed to the lock on the expensive one. final token = keyToken(priv, sku: "basic"); expect( () => verify(token, sku: "pro"), throwsA(predicate((e) => codeOf(e) == "aud_mismatch")), ); }); test("aud mismatch — right sku, wrong project", () { final token = keyToken(priv, project: "someone-else", sku: "pro"); expect( () => verify(token, project: "field-notes"), throwsA(predicate((e) => codeOf(e) == "aud_mismatch")), ); }); test("aud that isnt project/sku at all", () { final token = keyToken(priv, audOverride: "field-notes"); expect( () => verify(token), throwsA(predicate((e) => codeOf(e) == "aud_mismatch")), ); }); test("sub mismatch — somebody elses key on this device", () { final token = keyToken(priv, sub: "user-999"); expect( () => verify(token, sub: "user-123"), throwsA(predicate((e) => codeOf(e) == "sub_mismatch")), ); }); test("mode mismatch — a sandbox key never satisfies a live check", () { final token = keyToken(priv, mode: "sandbox"); expect( () => verify(token, mode: "live"), throwsA(predicate((e) => codeOf(e) == "mode_mismatch")), ); // and the other way, so nobody can force live data into a sandbox build final live = keyToken(priv, mode: "live"); expect( () => verify(live, mode: "sandbox"), throwsA(predicate((e) => codeOf(e) == "mode_mismatch")), ); }); test("an expired key is refused", () { final token = keyToken(priv, life: const Duration(hours: -1)); expect( () => verify(token), throwsA(predicate((e) => codeOf(e) == "expired")), ); }); test("an unknown kid is refused rather than guessed at", () { final token = keyToken(priv, kid: "rotated-away"); expect( () => verify(token), throwsA(predicate((e) => codeOf(e) == "kid_not_found")), ); }); test("rotation: the jwks carrying both keys still verifies the old one", () { final next = genKey(4); final rotated = { "keys": [ ...(jwksOf(next.publicKey as RSAPublicKey, kid: "k2")["keys"] as List), ...(jwks["keys"] as List), ], }; final old = keyToken(priv, kid: "k1"); expect(verify(old, doc: rotated).sku, "pro"); }); test("a malformed token is refused, not thrown past", () { expect( () => verify("not.a.jwt.at.all"), throwsA(isA()), ); expect(() => verify("rubbish"), throwsA(isA())); }); test("peekAudience splits project and sku without verifying", () { final aud = peekAudience(keyToken(priv, project: "p", sku: "s")); expect(aud?.project, "p"); expect(aud?.sku, "s"); expect(peekAudience("rubbish"), isNull); }); }