diff --git a/internal/proxy/engine_test.go b/internal/proxy/engine_test.go index 8d2274b..d4a38d4 100644 --- a/internal/proxy/engine_test.go +++ b/internal/proxy/engine_test.go @@ -99,6 +99,13 @@ func mockUpstream(w http.ResponseWriter, r *http.Request) { return } w.Write([]byte("protected payload")) + case "/protected2.bin": // same challenge as /protected.bin + if r.Header.Get("Authorization") != "Bearer minted-token" { + w.Header().Set("Www-Authenticate", `Bearer realm="`+upstream.URL+`/token",service="reg",scope="repo:pull"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Write([]byte("protected payload 2")) case "/token": w.Write([]byte(`{"token":"minted-token","expires_in":300}`)) default: @@ -285,6 +292,15 @@ func TestBearerTokenFlow(t *testing.T) { t.Error("bearer-protected content mismatch") } + // A second protected path with the same challenge reuses the cached token. + res2, err := testEngine.Fetch(ctx, r, "protected2.bin", p) + if err != nil { + t.Fatalf("second bearer fetch: %v", err) + } + if readAll(t, res2) != "protected payload 2" { + t.Error("second bearer content mismatch") + } + // HEAD path also negotiates a bearer token (uncached). testCache.FlushRemote(ctx, "eng-bearer") testDB.DeleteArtifact(ctx, "eng-bearer", "protected.bin") @@ -359,6 +375,56 @@ func TestUpstreamErrorUnwrap(t *testing.T) { } } +func TestImmutableBlobDedup(t *testing.T) { + requireStack(t) + ctx := context.Background() + p := prov(t, models.PackageGeneric) + // Two remotes serving identical content: the second store hits the + // already-exists branch (blob content is deduplicated). + for _, name := range []string{"eng-dedup-a", "eng-dedup-b"} { + r := seed(t, genericRemote(name)) + res, err := testEngine.Fetch(ctx, r, "blob.bin", p) + if err != nil { + t.Fatalf("%s fetch: %v", name, err) + } + if readAll(t, res) != "immutable blob" { + t.Errorf("%s content mismatch", name) + } + } +} + +func TestCircuitBreakerStates(t *testing.T) { + requireStack(t) + ctx := context.Background() + cb := NewCircuitBreaker(testCache) + const key = "cb-states" + testCache.ResetCircuit(ctx, key) + + if cb.IsOpen(ctx, key) { + t.Error("fresh breaker should be closed") + } + if cb.Health(ctx, key).Status != "healthy" { + t.Error("fresh breaker should be healthy") + } + cb.RecordFailure(ctx, key) + if s := cb.Health(ctx, key).Status; s != "degraded" { + t.Errorf("one failure should be degraded, got %q", s) + } + for i := 0; i < 6; i++ { + cb.RecordFailure(ctx, key) + } + if !cb.IsOpen(ctx, key) { + t.Error("breaker should be open after threshold failures") + } + if s := cb.Health(ctx, key).Status; s != "down" { + t.Errorf("open breaker should be down, got %q", s) + } + cb.RecordSuccess(ctx, key) + if cb.IsOpen(ctx, key) { + t.Error("breaker should close after success") + } +} + func asProxyError(err error, target **ProxyError) bool { pe, ok := err.(*ProxyError) if ok { diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 0bd4b2d..f927826 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -12,7 +12,10 @@ import ( "git.unkin.net/unkin/artifactapi/internal/testsupport" ) -var testS3 *S3 +var ( + testS3 *S3 + testEndpoint string +) func TestMain(m *testing.M) { ctx := context.Background() @@ -32,6 +35,7 @@ func TestMain(m *testing.M) { panic(err) } testS3 = s3 + testEndpoint = conn.Endpoint code := m.Run() terminate() if code != 0 { @@ -95,6 +99,15 @@ func TestS3RoundTrip(t *testing.T) { } } +func TestNewS3ExistingBucket(t *testing.T) { + requireS3(t) + // The bucket already exists from TestMain, so ensureBucket takes the + // "already present" path. + if _, err := NewS3(testEndpoint, "minioadmin", "minioadmin", "test-bucket", false, ""); err != nil { + t.Fatalf("second NewS3: %v", err) + } +} + func TestS3DownloadMissing(t *testing.T) { requireS3(t) if _, _, err := testS3.Download(context.Background(), "does/not/exist"); err == nil {