Batch A: blob storage layer, MCIAS auth, OCI token endpoint
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>
This commit is contained in:
71
internal/auth/cache_test.go
Normal file
71
internal/auth/cache_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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.
|
||||
}
|
||||
Reference in New Issue
Block a user