| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package api | |
| import ( | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "net/http" | |
| "net/http/httptest" | |
| "testing" | |
| "golang.org/x/pkgsite/internal" | |
| "golang.org/x/pkgsite/internal/derrors" | |
| "golang.org/x/pkgsite/internal/osv" | |
| "golang.org/x/pkgsite/internal/testing/fakedatasource" | |
| "golang.org/x/pkgsite/internal/vuln" | |
| ) | |
| // Tests here do not depend on postgres. | |
| // See internal/tests/api for the ones that do. | |
| func TestServeVulnerabilities(t *testing.T) { | |
| // This test doesn't need to run against a Postgres DB, because | |
| // vulnerabilities are not read from the database. | |
| vc, err := vuln.NewInMemoryClient([]*osv.Entry{ | |
| { | |
| ID: "VULN-1", | |
| Summary: "Vulnerability 1", | |
| Affected: []osv.Affected{ | |
| { | |
| Module: osv.Module{Path: "example.com"}, | |
| Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}}, | |
| }, | |
| }, | |
| }, | |
| { | |
| ID: "VULN-2", | |
| Summary: "Vulnerability 2", | |
| Affected: []osv.Affected{ | |
| { | |
| Module: osv.Module{Path: "example.com"}, | |
| Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}}, | |
| EcosystemSpecific: osv.EcosystemSpecific{ | |
| Packages: []osv.Package{ | |
| {Path: "example.com/pkg"}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| ID: "VULN-3", | |
| Summary: "Vulnerability 3", | |
| Affected: []osv.Affected{ | |
| { | |
| Module: osv.Module{Path: "example.com"}, | |
| Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}}, | |
| EcosystemSpecific: osv.EcosystemSpecific{ | |
| Packages: []osv.Package{ | |
| {Path: "example.com/other"}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| ds := fakedatasource.New() | |
| insertModule := func(v string) { | |
| ds.MustInsertModule(t, &internal.Module{ | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: "example.com", | |
| Version: v, | |
| }, | |
| Units: []*internal.Unit{ | |
| { | |
| UnitMeta: internal.UnitMeta{ | |
| Path: "example.com", | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: "example.com", | |
| Version: v, | |
| }, | |
| }, | |
| }, | |
| { | |
| UnitMeta: internal.UnitMeta{ | |
| Path: "example.com/pkg", | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: "example.com", | |
| Version: v, | |
| }, | |
| }, | |
| }, | |
| { | |
| UnitMeta: internal.UnitMeta{ | |
| Path: "example.com/other", | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: "example.com", | |
| Version: v, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }) | |
| } | |
| insertModule("v1.0.0") | |
| insertModule("v1.2.0") | |
| for _, test := range []struct { | |
| name string | |
| url string | |
| wantStatus int | |
| wantCount int | |
| want any | |
| }{ | |
| { | |
| name: "all vulns", | |
| url: "/v1/vulns/example.com?version=v1.0.0", | |
| wantStatus: http.StatusOK, | |
| wantCount: 3, | |
| }, | |
| { | |
| name: "no vulns", | |
| url: "/v1/vulns/example.com?version=v1.2.0", | |
| wantStatus: http.StatusOK, | |
| wantCount: 0, | |
| }, | |
| { | |
| name: "package path in vulns endpoint", | |
| url: "/v1/vulns/example.com/pkg?version=v1.0.0", | |
| wantStatus: http.StatusOK, | |
| wantCount: 2, | |
| }, | |
| { | |
| name: "another package path", | |
| url: "/v1/vulns/example.com/other?version=v1.0.0", | |
| wantStatus: http.StatusOK, | |
| wantCount: 2, | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| r := httptest.NewRequest("GET", test.url, nil) | |
| w := httptest.NewRecorder() | |
| if err := ServeVulnerabilities(vc)(w, r, ds); err != nil { | |
| ServeError(w, r, err) | |
| } | |
| if w.Code != test.wantStatus { | |
| t.Errorf("status = %d, want %d", w.Code, test.wantStatus) | |
| } | |
| if test.wantStatus == http.StatusOK { | |
| var got PaginatedResponse[Vulnerability] | |
| if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { | |
| t.Fatalf("json.Unmarshal: %v", err) | |
| } | |
| if len(got.Items) != test.wantCount { | |
| t.Errorf("count = %d, want %d", len(got.Items), test.wantCount) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| func TestCacheControl(t *testing.T) { | |
| // This test doesn't need to run against a Postgres DB, because | |
| // it's concerned only with headers. | |
| ds := fakedatasource.New() | |
| const modulePath = "example.com" | |
| for _, v := range []string{"v1.0.0", "master"} { | |
| ds.MustInsertModule(t, &internal.Module{ | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: modulePath, | |
| Version: v, | |
| }, | |
| Units: []*internal.Unit{{ | |
| UnitMeta: internal.UnitMeta{ | |
| Path: modulePath, | |
| ModuleInfo: internal.ModuleInfo{ | |
| ModulePath: modulePath, | |
| Version: v, | |
| }, | |
| }, | |
| }}, | |
| }) | |
| } | |
| for _, test := range []struct { | |
| version string | |
| want string | |
| }{ | |
| {"v1.0.0", "public, max-age=10800"}, | |
| {"latest", "public, max-age=3600"}, | |
| {"master", "public, max-age=3600"}, | |
| {"", "public, max-age=3600"}, | |
| } { | |
| t.Run(test.version, func(t *testing.T) { | |
| url := "/v1/module/" + modulePath | |
| if test.version != "" { | |
| url += "?version=" + test.version | |
| } | |
| r := httptest.NewRequest("GET", url, nil) | |
| w := httptest.NewRecorder() | |
| if err := ServeModule(w, r, ds); err != nil { | |
| t.Fatal(err) | |
| } | |
| if w.Code != http.StatusOK { | |
| t.Fatalf("status = %d, want %d", w.Code, http.StatusOK) | |
| } | |
| got := w.Header().Get("Cache-Control") | |
| if got != test.want { | |
| t.Errorf("Cache-Control = %q, want %q", got, test.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestServeErrorCacheControl(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| err error | |
| wantStatus int | |
| wantCC string | |
| }{ | |
| { | |
| name: "404 not found is cached for 10 min", | |
| err: fmt.Errorf("module not found: %w", derrors.NotFound), | |
| wantStatus: http.StatusNotFound, | |
| wantCC: "public, max-age=600", | |
| }, | |
| { | |
| name: "400 bad request is cached for 10 min", | |
| err: BadRequest("invalid query", "details"), | |
| wantStatus: http.StatusBadRequest, | |
| wantCC: "public, max-age=600", | |
| }, | |
| { | |
| name: "500 internal error is not cached", | |
| err: errors.New("db failure"), | |
| wantStatus: http.StatusInternalServerError, | |
| wantCC: "no-store", | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| r := httptest.NewRequest("GET", "/v1/module/nonexistent", nil) | |
| w := httptest.NewRecorder() | |
| if err := ServeError(w, r, test.err); err != nil { | |
| t.Fatal(err) | |
| } | |
| if w.Code != test.wantStatus { | |
| t.Errorf("status = %d, want %d", w.Code, test.wantStatus) | |
| } | |
| if got := w.Header().Get("Cache-Control"); got != test.wantCC { | |
| t.Errorf("Cache-Control = %q, want %q", got, test.wantCC) | |
| } | |
| }) | |
| } | |
| } |