package user import ( "context" "testing" "time" "github.com/stretchr/testify/assert" ) func TestJWTSecretManager(t *testing.T) { // Create a new secret manager with initial secret manager := NewJWTSecretManager("primary-secret") // Test initial state assert.Equal(t, "primary-secret", manager.GetPrimarySecret()) // Test GetAllValidSecrets initially secrets := manager.GetAllValidSecrets() assert.Len(t, secrets, 1) assert.Equal(t, "primary-secret", secrets[0].Secret) assert.True(t, secrets[0].IsPrimary) assert.Nil(t, secrets[0].ExpiresAt) // Add a secondary secret manager.AddSecret("secondary-secret", false, 0) // 0 means no expiration // Test after adding secondary secret assert.Equal(t, "primary-secret", manager.GetPrimarySecret()) // Primary should not change secrets = manager.GetAllValidSecrets() assert.Len(t, secrets, 2) // Find the secondary secret foundSecondary := false for _, secret := range secrets { if secret.Secret == "secondary-secret" { foundSecondary = true assert.False(t, secret.IsPrimary) assert.Nil(t, secret.ExpiresAt) // Should have no expiration break } } assert.True(t, foundSecondary, "Secondary secret should be found in valid secrets") // Test rotation manager.RotateToSecret("new-primary-secret") assert.Equal(t, "new-primary-secret", manager.GetPrimarySecret()) secrets = manager.GetAllValidSecrets() assert.Len(t, secrets, 3) // Should have 3 secrets now // Find the new primary secret foundNewPrimary := false for _, secret := range secrets { if secret.Secret == "new-primary-secret" { foundNewPrimary = true assert.True(t, secret.IsPrimary) assert.Nil(t, secret.ExpiresAt) // Should have no expiration break } } assert.True(t, foundNewPrimary, "New primary secret should be found in valid secrets") } func TestJWTSecretExpiration(t *testing.T) { manager := NewJWTSecretManager("primary-secret") // Add a secret with expiration manager.AddSecret("expiring-secret", false, 1*time.Hour) // Expires in 1 hour // Should have 2 secrets initially secrets := manager.GetAllValidSecrets() assert.Len(t, secrets, 2) // Test expiration logic foundExpiring := false for _, secret := range secrets { if secret.Secret == "expiring-secret" { foundExpiring = true assert.NotNil(t, secret.ExpiresAt) assert.True(t, secret.ExpiresAt.After(time.Now())) break } } assert.True(t, foundExpiring) } // TestRemoveExpiredSecrets_ExpiredNonPrimaryRemoved confirms that // RemoveExpiredSecrets drops a non-primary secret whose ExpiresAt is in the past. func TestRemoveExpiredSecrets_ExpiredNonPrimaryRemoved(t *testing.T) { manager := NewJWTSecretManager("primary") // Add a secret that expired 1 hour ago by setting expiresIn to a small // positive duration then mutating after via AddSecret + manipulation. // Simpler: add with a 1ns lifetime and sleep 2ns equivalent (tiny TTL). manager.AddSecret("about-to-expire", false, 1*time.Nanosecond) time.Sleep(5 * time.Millisecond) removed := manager.RemoveExpiredSecrets() assert.Equal(t, 1, removed, "one expired secret should be removed") secrets := manager.GetAllValidSecrets() assert.Len(t, secrets, 1, "only primary should remain") assert.Equal(t, "primary", secrets[0].Secret) assert.True(t, secrets[0].IsPrimary) } // TestRemoveExpiredSecrets_PrimaryNeverRemoved confirms the primary secret // is preserved even if (somehow) marked expired - ADR-0021 invariant. func TestRemoveExpiredSecrets_PrimaryNeverRemoved(t *testing.T) { manager := NewJWTSecretManager("primary") // Add a non-primary that doesn't expire manager.AddSecret("kept", false, 0) // Simulate an "expired primary" by manipulating internals via Reset then // re-creating - here we rely on the public contract: primary has no // ExpiresAt by default. Confirm cleanup leaves it. removed := manager.RemoveExpiredSecrets() assert.Equal(t, 0, removed) assert.Equal(t, "primary", manager.GetPrimarySecret()) } // TestRemoveExpiredSecrets_NonExpiredKept confirms a future-expiring secret // stays after cleanup. func TestRemoveExpiredSecrets_NonExpiredKept(t *testing.T) { manager := NewJWTSecretManager("primary") manager.AddSecret("future", false, 1*time.Hour) removed := manager.RemoveExpiredSecrets() assert.Equal(t, 0, removed) assert.Len(t, manager.GetAllValidSecrets(), 2) } // TestStartCleanupLoop_FiresAndStops confirms the goroutine actually calls // RemoveExpiredSecrets on each tick and stops cleanly when the context is // cancelled. Uses a short interval to keep the test fast. func TestStartCleanupLoop_FiresAndStops(t *testing.T) { manager := NewJWTSecretManager("primary") manager.AddSecret("dies", false, 5*time.Millisecond) ctx, cancel := context.WithCancel(context.Background()) defer cancel() manager.StartCleanupLoop(ctx, 10*time.Millisecond) // Wait long enough for at least one tick + the secret's TTL time.Sleep(50 * time.Millisecond) cancel() // stop the loop secrets := manager.GetAllValidSecrets() assert.Len(t, secrets, 1, "expired secret should have been removed by the loop") assert.Equal(t, "primary", secrets[0].Secret) }