Phase 2 — internal/storage/: Content-addressed blob storage with atomic writes via rename. BlobWriter stages data in uploads dir with running SHA-256 hash, commits by verifying digest then renaming to layers/sha256/<prefix>/<hex>. Reader provides Open, Stat, Delete, Exists with digest validation. Phase 3 — internal/auth/ + internal/server/: MCIAS client with Login and ValidateToken, 30s SHA-256-keyed cache with lazy eviction and injectable clock for testing. TLS 1.3 minimum with optional custom CA cert. Chi router with RequireAuth middleware (Bearer token extraction, WWW-Authenticate header, OCI error format), token endpoint (Basic auth → bearer exchange via MCIAS), and /v2/ version check handler. 52 tests passing (14 storage + 9 auth + 9 server + 20 existing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
72 lines
1.5 KiB
Go
72 lines
1.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCachePutGet(t *testing.T) {
|
|
t.Helper()
|
|
c := newCache(30 * time.Second)
|
|
|
|
claims := &Claims{Subject: "alice", AccountType: "user", Roles: []string{"reader"}}
|
|
c.put("abc123", claims)
|
|
|
|
got, ok := c.get("abc123")
|
|
if !ok {
|
|
t.Fatal("expected cache hit, got miss")
|
|
}
|
|
if got.Subject != "alice" {
|
|
t.Fatalf("subject: got %q, want %q", got.Subject, "alice")
|
|
}
|
|
}
|
|
|
|
func TestCacheTTLExpiry(t *testing.T) {
|
|
t.Helper()
|
|
now := time.Now()
|
|
c := newCache(30 * time.Second)
|
|
c.now = func() time.Time { return now }
|
|
|
|
claims := &Claims{Subject: "bob"}
|
|
c.put("def456", claims)
|
|
|
|
// Still within TTL.
|
|
got, ok := c.get("def456")
|
|
if !ok {
|
|
t.Fatal("expected cache hit before TTL expiry")
|
|
}
|
|
if got.Subject != "bob" {
|
|
t.Fatalf("subject: got %q, want %q", got.Subject, "bob")
|
|
}
|
|
|
|
// Advance clock past TTL.
|
|
c.now = func() time.Time { return now.Add(31 * time.Second) }
|
|
|
|
_, ok = c.get("def456")
|
|
if ok {
|
|
t.Fatal("expected cache miss after TTL expiry, got hit")
|
|
}
|
|
}
|
|
|
|
func TestCacheConcurrent(t *testing.T) {
|
|
t.Helper()
|
|
c := newCache(30 * time.Second)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := range 100 {
|
|
wg.Add(2)
|
|
key := string(rune('A' + i%26))
|
|
go func() {
|
|
defer wg.Done()
|
|
c.put(key, &Claims{Subject: key})
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
c.get(key) //nolint:gosec // result intentionally ignored in concurrency test
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
// If we get here without a race detector complaint, the test passes.
|
|
}
|