diff --git a/internal/api/v2/local_fault_test.go b/internal/api/v2/local_fault_test.go new file mode 100644 index 0000000..3159411 --- /dev/null +++ b/internal/api/v2/local_fault_test.go @@ -0,0 +1,88 @@ +package v2 + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + "git.unkin.net/unkin/artifactapi/internal/database" + "git.unkin.net/unkin/artifactapi/internal/storage" + "git.unkin.net/unkin/artifactapi/internal/testsupport" + "git.unkin.net/unkin/artifactapi/pkg/models" +) + +// TestLocalUploadStoreFailure covers the upload handlers' store-error branches +// by killing the object store after a successful upload. +func TestLocalUploadStoreFailure(t *testing.T) { + if testDSN == "" { + t.Skip("Docker unavailable") + } + ctx := context.Background() + db, err := database.New(testDSN) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + conn, termMinio, err := testsupport.StartMinio(ctx) + if err != nil { + t.Skip("minio unavailable") + } + var store *storage.S3 + for i := 0; i < 20; i++ { + if store, err = storage.NewS3(conn.Endpoint, conn.AccessKey, conn.SecretKey, "fault", false, ""); err == nil { + break + } + time.Sleep(500 * time.Millisecond) + } + if err != nil { + termMinio() + t.Fatal(err) + } + + for _, pt := range []models.PackageType{models.PackageGeneric, models.PackagePyPI} { + if err := db.CreateRemote(ctx, &models.Remote{Name: "fault-" + string(pt), PackageType: pt, RepoType: models.RepoTypeLocal}); err != nil { + t.Fatal(err) + } + } + + h := NewLocalHandler(db, store) + router := chi.NewRouter() + router.Route("/remotes/{name}/files", func(r chi.Router) { + r.Put("/*", h.Routes().ServeHTTP) + }) + srv := httptest.NewServer(router) + defer srv.Close() + + put := func(name, path, body string) int { + rq, _ := http.NewRequest("PUT", srv.URL+"/remotes/"+name+"/files/"+path, strings.NewReader(body)) + resp, err := http.DefaultClient.Do(rq) + if err != nil { + t.Fatalf("put: %v", err) + } + resp.Body.Close() + return resp.StatusCode + } + + // Sanity: uploads succeed while the store is up. + if c := put("fault-generic", "ok.bin", "data"); c != 201 { + t.Fatalf("generic upload while up = %d", c) + } + if c := put("fault-pypi", "foo-1.0-py3-none-any.whl", "wheel"); c != 201 { + t.Fatalf("pypi upload while up = %d", c) + } + + // Kill the store; subsequent CAS.Store calls fail -> 500. + termMinio() + if c := put("fault-generic", "after.bin", "data"); c != 500 { + t.Errorf("generic upload after store down = %d, want 500", c) + } + if c := put("fault-pypi", "bar-1.0-py3-none-any.whl", "wheel"); c != 500 { + t.Errorf("pypi upload after store down = %d, want 500", c) + } +}