feat(backend): update JWT auth implementation and tests

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-18 16:26:00 +01:00
co-authored by Sisyphus
parent 2d0071c453
commit b5c1eef8c2
2 changed files with 148 additions and 51 deletions
+41 -3
View File
@@ -14,18 +14,45 @@ package auth
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"crussell/db"
"crussell/testutils/testdb"
)
func TestMain(m *testing.M) {
InitJWT("test-secret-key-for-jwt-test")
pool, err := testdb.NewPool("")
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: No test DB available — JTI revocation tests will fail: %v\n", err)
} else {
// Run migration to ensure the revoked_jtis and refresh_tokens tables exist
testdb.Migrate(&testing.T{}, pool)
db.DB = pool
}
code := m.Run()
if pool != nil {
pool.Close()
}
os.Exit(code)
}
// requiresDB skips the test if the database is not available (e.g. running
// tests standalone without test DB setup). DB-backed JTI revocation tests
// need a connection to the revoked_jtis table.
func requiresDB(t *testing.T) {
t.Helper()
if db.DB == nil {
t.Skip("skipping: no database connection")
}
}
// =============================================================================
// generateJTI Tests
// =============================================================================
@@ -158,6 +185,7 @@ func TestVerifyToken_ReturnsJTI(t *testing.T) {
// TestVerifyToken_RevokedJTI creates a token, revokes its JTI, and verifies
// that VerifyToken returns an error containing "token revoked".
func TestVerifyToken_RevokedJTI(t *testing.T) {
requiresDB(t)
token, jti, err := GenerateToken("user-004", "verified_email")
if err != nil {
t.Fatalf("GenerateToken() failed: %v", err)
@@ -203,6 +231,7 @@ func TestVerifyToken_MissingJTI(t *testing.T) {
// TestRevokeJTI_AddsToSet verifies that calling RevokeJTI adds the JTI to the
// revoked set, and IsJTIRevoked returns true for it.
func TestRevokeJTI_AddsToSet(t *testing.T) {
requiresDB(t)
_, jti, err := GenerateToken("user-006", "verified_email")
if err != nil {
t.Fatalf("GenerateToken() failed: %v", err)
@@ -234,16 +263,24 @@ func TestIsJTIRevoked_NonExistent(t *testing.T) {
// TestCleanupRevokedJTIs_RemovesExpired adds a JTI with a past expiry time,
// runs CleanupRevokedJTIs, and verifies the JTI is removed from the set.
func TestCleanupRevokedJTIs_RemovesExpired(t *testing.T) {
requiresDB(t)
_, jti, err := GenerateToken("user-007", "verified_email")
if err != nil {
t.Fatalf("GenerateToken() failed: %v", err)
}
// Add with past expiry (1 hour ago)
RevokeJTI(jti, time.Now().Add(-1*time.Hour))
// Add with future expiry so IsJTIRevoked sees it
RevokeJTI(jti, time.Now().Add(1*time.Hour))
if !IsJTIRevoked(jti) {
t.Fatal("JTI should be in revoked set before cleanup")
t.Fatal("JTI should be in revoked set after RevokeJTI")
}
// Directly update the DB to set expiry in the past
_, err = db.DB.Exec(context.Background(),
"UPDATE revoked_jtis SET expires_at = NOW() - INTERVAL '1 hour' WHERE jti = $1", jti)
if err != nil {
t.Fatalf("failed to expire JTI: %v", err)
}
CleanupRevokedJTIs()
@@ -256,6 +293,7 @@ func TestCleanupRevokedJTIs_RemovesExpired(t *testing.T) {
// TestCleanupRevokedJTIs_KeepsValid adds a JTI with a future expiry time,
// runs CleanupRevokedJTIs, and verifies the JTI is still in the set.
func TestCleanupRevokedJTIs_KeepsValid(t *testing.T) {
requiresDB(t)
_, jti, err := GenerateToken("user-008", "verified_email")
if err != nil {
t.Fatalf("GenerateToken() failed: %v", err)