ficha/revocation_test.go

145 lines
3.5 KiB
Go

package ficha
import (
"context"
"sync"
"testing"
"time"
)
func TestMemoryRevocationStoreEmpty(t *testing.T) {
s := NewMemoryRevocationStore()
revoked, err := s.IsRevoked(context.Background(), "anything")
if err != nil {
t.Fatalf("IsRevoked: %v", err)
}
if revoked {
t.Error("empty store should not report any token as revoked")
}
if s.Len() != 0 {
t.Errorf("Len: got %d, want 0", s.Len())
}
}
func TestMemoryRevocationStoreRevokeAndCheck(t *testing.T) {
s := NewMemoryRevocationStore()
ctx := context.Background()
until := time.Now().Add(1 * time.Hour)
if err := s.Revoke(ctx, "tok_abc", until); err != nil {
t.Fatalf("Revoke: %v", err)
}
revoked, err := s.IsRevoked(ctx, "tok_abc")
if err != nil {
t.Fatalf("IsRevoked: %v", err)
}
if !revoked {
t.Error("token should be revoked")
}
// Different ID should not be flagged.
revoked, _ = s.IsRevoked(ctx, "tok_other")
if revoked {
t.Error("unrelated token should not be revoked")
}
}
func TestMemoryRevocationStoreExpiredEntry(t *testing.T) {
s := NewMemoryRevocationStore()
// Inject a controllable clock.
clock := time.Unix(1_700_000_000, 0)
s.now = func() time.Time { return clock }
ctx := context.Background()
until := time.Unix(1_700_000_500, 0)
if err := s.Revoke(ctx, "tok_abc", until); err != nil {
t.Fatalf("Revoke: %v", err)
}
// Before expiry: revoked.
revoked, _ := s.IsRevoked(ctx, "tok_abc")
if !revoked {
t.Error("should be revoked before expiry")
}
// Advance clock past expiry.
clock = time.Unix(1_700_000_600, 0)
revoked, _ = s.IsRevoked(ctx, "tok_abc")
if revoked {
t.Error("expired entry should not be reported as revoked")
}
}
func TestMemoryRevocationStoreCleanup(t *testing.T) {
s := NewMemoryRevocationStore()
clock := time.Unix(1_700_000_000, 0)
s.now = func() time.Time { return clock }
ctx := context.Background()
_ = s.Revoke(ctx, "expired1", time.Unix(1_700_000_100, 0))
_ = s.Revoke(ctx, "expired2", time.Unix(1_700_000_200, 0))
_ = s.Revoke(ctx, "stillvalid", time.Unix(1_700_000_999, 0))
if s.Len() != 3 {
t.Errorf("Len before cleanup: got %d, want 3", s.Len())
}
// Move clock past the first two expiries but not the third.
clock = time.Unix(1_700_000_500, 0)
removed := s.Cleanup()
if removed != 2 {
t.Errorf("Cleanup removed: got %d, want 2", removed)
}
if s.Len() != 1 {
t.Errorf("Len after cleanup: got %d, want 1", s.Len())
}
// The remaining one is still flagged.
revoked, _ := s.IsRevoked(ctx, "stillvalid")
if !revoked {
t.Error("non-expired entry should survive cleanup")
}
}
func TestMemoryRevocationStoreReRevoke(t *testing.T) {
// Revoking the same ID twice should be idempotent.
s := NewMemoryRevocationStore()
ctx := context.Background()
until := time.Now().Add(1 * time.Hour)
if err := s.Revoke(ctx, "tok", until); err != nil {
t.Fatalf("Revoke 1: %v", err)
}
if err := s.Revoke(ctx, "tok", until.Add(1*time.Hour)); err != nil {
t.Fatalf("Revoke 2: %v", err)
}
if s.Len() != 1 {
t.Errorf("Len: got %d, want 1", s.Len())
}
}
func TestMemoryRevocationStoreConcurrent(t *testing.T) {
s := NewMemoryRevocationStore()
ctx := context.Background()
until := time.Now().Add(1 * time.Hour)
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
id := "tok_" + string(rune('a'+i%26))
for j := 0; j < 100; j++ {
_ = s.Revoke(ctx, id, until)
_, _ = s.IsRevoked(ctx, id)
}
}(i)
}
wg.Wait()
}
// Compile-time check: MemoryRevocationStore satisfies RevocationStore.
var _ RevocationStore = (*MemoryRevocationStore)(nil)