package utils import ( "strings" "testing" "time" ) func TestAConsoleSessionSurvivesTheRoundTrip(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) token, expires, err := MintWebToken(WebClaims{ Userid: 904, Tenantid: 1147, Locationid: 1172, Roleid: 3, Configid: 2, }, now) if err != nil { t.Fatalf("minting: %v", err) } claims, err := ParseWebToken(token, now.Add(time.Hour)) if err != nil { t.Fatalf("parsing a token we just issued: %v", err) } if claims.Tenantid != 1147 || claims.Userid != 904 { t.Fatalf("the identity did not survive: user %d tenant %d", claims.Userid, claims.Tenantid) } if claims.Locationid != 1172 || claims.Roleid != 3 { t.Fatalf("branch or role lost: location %d role %d", claims.Locationid, claims.Roleid) } if !expires.After(now) { t.Fatalf("expiry is not in the future: %v", expires) } } /* ── The two token kinds must not be interchangeable ────────────────────── */ func TestATillsTokenIsNotAConsoleSession(t *testing.T) { // The whole reason `webTokenPrefix` exists. Both tokens are the same shape // signed with the same key, so without the prefix a POS token verifies as a // web one — and its `Locationid` would land where `Tenantid` is read, which // is the field every permission decision is made on. withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) pos, _, err := MintPosToken(PosClaims{Userid: 7, Tenantid: 916, Locationid: 1185, Roleid: 8}, now) if err != nil { t.Fatalf("minting a POS token: %v", err) } if _, err := ParseWebToken(pos, now); err == nil { t.Fatal("a cashier's token was accepted as a console session") } } func TestAConsoleSessionIsNotATillsToken(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) web, _, err := MintWebToken(WebClaims{Userid: 904, Tenantid: 1147}, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParsePosToken(web, now); err == nil { t.Fatal("a console session was accepted at the till") } } /* ── Forgery and tampering ─────────────────────────────────────────────── */ func TestATamperedTenantDoesNotVerify(t *testing.T) { // The attack this is all for: take a valid session, change the tenant, read // somebody else's shop. withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) token, _, err := MintWebToken(WebClaims{Userid: 904, Tenantid: 1147}, now) if err != nil { t.Fatalf("minting: %v", err) } body, signature, _ := strings.Cut(strings.TrimPrefix(token, webTokenPrefix), ".") forged := webTokenPrefix + body[:len(body)-1] + "X" + "." + signature if _, err := ParseWebToken(forged, now); err == nil { t.Fatal("an edited payload verified") } } func TestATokenSignedWithAnotherKeyIsRefused(t *testing.T) { now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) withSecret(t, "a-completely-different-signing-key") token, _, err := MintWebToken(WebClaims{Userid: 904, Tenantid: 1147}, now) if err != nil { t.Fatalf("minting: %v", err) } withSecret(t, testSecret) if _, err := ParseWebToken(token, now); err == nil { t.Fatal("a token signed with someone else's key verified") } } func TestNoSigningKeyMeansNoSessions(t *testing.T) { t.Setenv("POS_TOKEN_SECRET", "") t.Setenv("JWT_SECRET_KEY", "") if _, _, err := MintWebToken(WebClaims{Userid: 1, Tenantid: 1}, time.Now()); err == nil { t.Fatal("a session was issued with no signing key") } if WebTokenConfigured() { t.Fatal("reported configured with no signing key") } } /* ── Expiry ────────────────────────────────────────────────────────────── */ func TestASessionExpires(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) token, _, err := MintWebToken(WebClaims{Userid: 904, Tenantid: 1147}, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParseWebToken(token, now.Add(WebTokenTTL-time.Minute)); err != nil { t.Fatalf("refused inside its life: %v", err) } if _, err := ParseWebToken(token, now.Add(WebTokenTTL+time.Minute)); err == nil { t.Fatal("a tab left open overnight still authorised") } } /* ── The platform account ──────────────────────────────────────────────── */ func TestPlatformAccessComesFromSuperadminAndNothingElse(t *testing.T) { withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) token, _, err := MintWebToken(WebClaims{Userid: 12, Superadmin: true, Roleid: 1}, now) if err != nil { t.Fatalf("minting: %v", err) } claims, err := ParseWebToken(token, now) if err != nil { t.Fatalf("a staff session was refused: %v", err) } if !claims.IsPlatformAccount() { t.Fatal("issuperadmin did not grant platform access") } } func TestRoleid1IsAMerchantNotAPlatformOperator(t *testing.T) { // `app_roles` calls roleid 1 "Super admin", and tenant onboarding wrote 1 // for every shop owner. A role test here would hand cross-tenant access to // every merchant on the system — the console already had to fix this once. claims := WebClaims{Userid: 904, Tenantid: 1147, Roleid: 1} if claims.IsPlatformAccount() { t.Fatal("roleid 1 claimed platform access") } } func TestAMissingTenantIsNotAPlatformAccount(t *testing.T) { // The other near-miss: a user row whose tenant was never filled in must not // become the one session that reads everything. Go's zero value is 0, so // this is exactly what a forgotten field looks like. withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) claims := WebClaims{Userid: 904, Tenantid: 0} if claims.IsPlatformAccount() { t.Fatal("a missing tenant claimed platform access") } token, _, err := MintWebToken(claims, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParseWebToken(token, now); err == nil { t.Fatal("a session naming no tenant and claiming no staff status verified") } } func TestATokenNamingNobodyIsRefused(t *testing.T) { // A token that authorises nothing must not be mistaken for one that // authorises everything. withSecret(t, testSecret) now := time.Date(2026, 9, 23, 9, 0, 0, 0, time.UTC) token, _, err := MintWebToken(WebClaims{Userid: 0, Tenantid: 1147}, now) if err != nil { t.Fatalf("minting: %v", err) } if _, err := ParseWebToken(token, now); err == nil { t.Fatal("a token naming no user verified") } } /* ── Shape ─────────────────────────────────────────────────────────────── */ func TestMalformedTokensAreRefusedWithoutPanicking(t *testing.T) { withSecret(t, testSecret) now := time.Now() for _, token := range []string{ "", " ", "w1.", "w1..", "w1.onlyonepart", "w1.!!!not-base64!!!.sig", "no-prefix.payload.sig", } { if _, err := ParseWebToken(token, now); err == nil { t.Fatalf("accepted a malformed token: %q", token) } } }