package utils import ( "encoding/base64" "encoding/json" "strings" "testing" "time" ) func withSecret(t *testing.T, secret string) { t.Helper() t.Setenv("POS_TOKEN_SECRET", secret) } const testSecret = "a-test-signing-key-long-enough" func TestASessionSurvivesTheRoundTrip(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC) token, expires, err := MintPosToken(PosClaims{ Userid: 42, Tenantid: 1087, Locationid: 1135, Roleid: 3, Terminalid: "T5EDD", }, now) if err != nil { t.Fatalf("minting: %v", err) } claims, err := ParsePosToken(token, now.Add(time.Hour)) if err != nil { t.Fatalf("parsing a token we just issued: %v", err) } if claims.Tenantid != 1087 || claims.Locationid != 1135 { t.Fatalf("the outlet did not survive: tenant %d location %d", claims.Tenantid, claims.Locationid) } if claims.Terminalid != "T5EDD" { t.Fatalf("terminal id lost: %q", claims.Terminalid) } if !expires.After(now) { t.Fatalf("expiry %v is not after issue %v", expires, now) } } // The whole point of signing. Before this existed a till named its own outlet // on the wire and was believed, so this is the test that says it no longer can. func TestARewrittenOutletIsRefused(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC) token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now) if err != nil { t.Fatalf("minting: %v", err) } // Tamper: decode the payload, move it to another tenant's outlet, re-encode // and keep the original signature — exactly what an attacker holding a real // token would try. encoded, signature, _ := strings.Cut(token, ".") payload, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { t.Fatalf("decoding our own payload: %v", err) } var claims PosClaims if err := json.Unmarshal(payload, &claims); err != nil { t.Fatalf("unmarshalling our own payload: %v", err) } claims.Tenantid = 916 claims.Locationid = 1185 forged, _ := json.Marshal(claims) tampered := base64.RawURLEncoding.EncodeToString(forged) + "." + signature if _, err := ParsePosToken(tampered, now); err == nil { t.Fatal("a token whose outlet was rewritten was accepted") } } func TestAnExpiredSessionIsRefused(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC) token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParsePosToken(token, now.Add(PosTokenTTL+time.Minute)); err == nil { t.Fatal("an expired session was accepted") } } // A token signed by somebody else must not verify here, or the signature is // decoration. func TestATokenFromAnotherKeyIsRefused(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC) token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now) if err != nil { t.Fatalf("minting: %v", err) } withSecret(t, "a-completely-different-key-here") if _, err := ParsePosToken(token, now); err == nil { t.Fatal("a token signed with another key verified") } } func TestAMalformedTokenIsRefused(t *testing.T) { withSecret(t, testSecret) now := time.Now() for _, token := range []string{ "", "nodot", ".", "only.", ".onlysignature", "not-base64!.also-not-base64!", } { if _, err := ParsePosToken(token, now); err == nil { t.Fatalf("malformed token %q was accepted", token) } } } // A deployment with no signing key must fail loudly rather than fall back to a // key anyone reading the source could compute. func TestNoSecretMeansNoSessions(t *testing.T) { t.Setenv("POS_TOKEN_SECRET", "") t.Setenv("JWT_SECRET_KEY", "") if PosTokenConfigured() { t.Fatal("reported configured with no secret set") } if _, _, err := MintPosToken(PosClaims{Tenantid: 1, Locationid: 1}, time.Now()); err == nil { t.Fatal("minted a session with no signing key") } } func TestAShortSecretIsRefused(t *testing.T) { t.Setenv("POS_TOKEN_SECRET", "short") t.Setenv("JWT_SECRET_KEY", "") if _, _, err := MintPosToken(PosClaims{Tenantid: 1, Locationid: 1}, time.Now()); err == nil { t.Fatal("signed with a secret too short to be worth signing with") } } // A token that verifies but names no outlet authorises nothing, and must not be // mistaken for one that authorises everything. func TestASessionNamingNoOutletIsRefused(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC) token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 0, Locationid: 0}, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParsePosToken(token, now); err == nil { t.Fatal("a session naming no outlet was accepted") } }