test: circuit breaker states, bearer cache-hit, blob dedup, terraform/storage branches
This commit is contained in:
@@ -99,6 +99,13 @@ func mockUpstream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte("protected payload"))
|
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":
|
case "/token":
|
||||||
w.Write([]byte(`{"token":"minted-token","expires_in":300}`))
|
w.Write([]byte(`{"token":"minted-token","expires_in":300}`))
|
||||||
default:
|
default:
|
||||||
@@ -285,6 +292,15 @@ func TestBearerTokenFlow(t *testing.T) {
|
|||||||
t.Error("bearer-protected content mismatch")
|
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).
|
// HEAD path also negotiates a bearer token (uncached).
|
||||||
testCache.FlushRemote(ctx, "eng-bearer")
|
testCache.FlushRemote(ctx, "eng-bearer")
|
||||||
testDB.DeleteArtifact(ctx, "eng-bearer", "protected.bin")
|
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 {
|
func asProxyError(err error, target **ProxyError) bool {
|
||||||
pe, ok := err.(*ProxyError)
|
pe, ok := err.(*ProxyError)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import (
|
|||||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testS3 *S3
|
var (
|
||||||
|
testS3 *S3
|
||||||
|
testEndpoint string
|
||||||
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -32,6 +35,7 @@ func TestMain(m *testing.M) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
testS3 = s3
|
testS3 = s3
|
||||||
|
testEndpoint = conn.Endpoint
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
terminate()
|
terminate()
|
||||||
if code != 0 {
|
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) {
|
func TestS3DownloadMissing(t *testing.T) {
|
||||||
requireS3(t)
|
requireS3(t)
|
||||||
if _, _, err := testS3.Download(context.Background(), "does/not/exist"); err == nil {
|
if _, _, err := testS3.Download(context.Background(), "does/not/exist"); err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user