diff --git a/internal/api/v1/scheme_test.go b/internal/api/v1/scheme_test.go new file mode 100644 index 0000000..e74ce67 --- /dev/null +++ b/internal/api/v1/scheme_test.go @@ -0,0 +1,20 @@ +package v1 + +import ( + "crypto/tls" + "net/http" + "testing" +) + +func TestScheme(t *testing.T) { + if got := scheme(&http.Request{TLS: &tls.ConnectionState{}}); got != "https" { + t.Errorf("TLS request scheme = %q, want https", got) + } + r := &http.Request{Header: http.Header{"X-Forwarded-Proto": {"https"}}} + if got := scheme(r); got != "https" { + t.Errorf("X-Forwarded-Proto scheme = %q, want https", got) + } + if got := scheme(&http.Request{Header: http.Header{}}); got != "http" { + t.Errorf("default scheme = %q, want http", got) + } +} diff --git a/internal/api/v2/errorpaths_test.go b/internal/api/v2/errorpaths_test.go new file mode 100644 index 0000000..56bd343 --- /dev/null +++ b/internal/api/v2/errorpaths_test.go @@ -0,0 +1,123 @@ +package v2 + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "git.unkin.net/unkin/artifactapi/internal/database" + "git.unkin.net/unkin/artifactapi/internal/testsupport" +) + +var testDSN string + +func TestMain(m *testing.M) { + ctx := context.Background() + dsn, terminate, err := testsupport.StartPostgres(ctx) + if err != nil { + os.Exit(m.Run()) + } + testDSN = dsn + code := m.Run() + terminate() + if code != 0 { + os.Exit(code) + } +} + +// closedDB returns a DB whose pool has been closed, so every query fails — +// used to drive the handlers' error branches. +func closedDB(t *testing.T) *database.DB { + t.Helper() + if testDSN == "" { + t.Skip("Docker unavailable") + } + db, err := database.New(testDSN) + if err != nil { + t.Fatalf("new db: %v", err) + } + db.Close() + return db +} + +func do(t *testing.T, h http.Handler, method, path, body string) int { + t.Helper() + var r io.Reader + if body != "" { + r = strings.NewReader(body) + } + req := httptest.NewRequest(method, path, r) + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + return w.Code +} + +func TestRemotesErrorPaths(t *testing.T) { + h := NewRemotesHandler(closedDB(t)).Routes() + if c := do(t, h, "GET", "/", ""); c != 500 { + t.Errorf("list with dead db = %d, want 500", c) + } + if c := do(t, h, "POST", "/", `{"name":"x","package_type":"generic","repo_type":"remote","base_url":"https://x"}`); c != 500 { + t.Errorf("create with dead db = %d, want 500", c) + } + if c := do(t, h, "PUT", "/x", `{"package_type":"generic","base_url":"https://x"}`); c != 500 { + t.Errorf("update with dead db = %d, want 500", c) + } + if c := do(t, h, "GET", "/x", ""); c != 404 { + t.Errorf("get missing = %d, want 404", c) + } + if c := do(t, h, "DELETE", "/x", ""); c != 500 { + t.Errorf("delete with dead db = %d, want 500", c) + } + // Bad request bodies never reach the db. + if c := do(t, h, "POST", "/", `not json`); c != 400 { + t.Errorf("invalid json = %d, want 400", c) + } +} + +func TestVirtualsErrorPaths(t *testing.T) { + h := NewVirtualsHandler(closedDB(t)).Routes() + if c := do(t, h, "GET", "/", ""); c != 500 { + t.Errorf("list = %d, want 500", c) + } + if c := do(t, h, "GET", "/x", ""); c != 404 { + t.Errorf("get missing = %d, want 404", c) + } + if c := do(t, h, "POST", "/", `{"name":"v","package_type":"helm","members":["a"]}`); c != 500 { + t.Errorf("create = %d, want 500", c) + } + if c := do(t, h, "PUT", "/v", `{"package_type":"helm","members":["a"]}`); c != 500 { + t.Errorf("update = %d, want 500", c) + } + if c := do(t, h, "DELETE", "/v", ""); c != 500 { + t.Errorf("delete = %d, want 500", c) + } +} + +func TestStatsErrorPaths(t *testing.T) { + h := NewStatsHandler(closedDB(t)).Routes() + for _, p := range []string{"/", "/top-remotes", "/top-files-by-hits", "/top-files-by-bandwidth"} { + if c := do(t, h, "GET", p, ""); c != 500 { + t.Errorf("stats %s = %d, want 500", p, c) + } + } +} + +func TestLocalErrorPaths(t *testing.T) { + h := NewLocalHandler(closedDB(t), nil).Routes() + // GetRemote fails on the closed db -> not found. + if c := do(t, h, "PUT", "/x/files/a.bin", "data"); c != 404 { + t.Errorf("upload unknown repo = %d, want 404", c) + } + // download / remove hit the db and 500. + if c := do(t, h, "GET", "/x/files/a.bin", ""); c != 500 { + t.Errorf("download = %d, want 500", c) + } + if c := do(t, h, "DELETE", "/x/files/a.bin", ""); c != 500 { + t.Errorf("remove = %d, want 500", c) + } +} diff --git a/internal/provider/rpm/rpm_meta_test.go b/internal/provider/rpm/rpm_meta_test.go index 64ed91e..9a0c525 100644 --- a/internal/provider/rpm/rpm_meta_test.go +++ b/internal/provider/rpm/rpm_meta_test.go @@ -192,6 +192,41 @@ func TestRPMPrimaryXMLContents(t *testing.T) { } } +func TestRPMContentTypeAndHelpers(t *testing.T) { + p := &Provider{} + for path, want := range map[string]string{ + "x.rpm": "application/x-rpm", + "repodata/repomd.xml": "application/xml", + "repodata/h-primary.xml.gz": "application/xml", + "repodata/h-primary.xml.xz": "application/xml", + "Packages/other": "application/octet-stream", + } { + if got := p.ContentType(path); got != want { + t.Errorf("ContentType(%q)=%q want %q", path, got, want) + } + } + + for flag, want := range map[int]string{ + 0x08 | 0x04: "GE", + 0x02 | 0x04: "LE", + 0x08: "GT", + 0x02: "LT", + 0x04: "EQ", + 0x00: "", + } { + if got := rpmFlagString(flag); got != want { + t.Errorf("rpmFlagString(%d)=%q want %q", flag, got, want) + } + } + + if firstGroup(nil) != "Unspecified" { + t.Error("empty groups should be Unspecified") + } + if firstGroup([]string{"System", "Base"}) != "System" { + t.Error("firstGroup should return the first") + } +} + func TestGenerateLocalIndexUnsupported(t *testing.T) { if _, err := (&Provider{}).GenerateLocalIndex(context.Background(), fakeRPMReader{}, "r", "simple/"); err == nil { t.Error("expected unsupported error")