package api import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "git.avsdev.uk/Go/job-search/internal/store" ) // The description is served as JSON, and the docs page points at it. func TestDocsRoutes(t *testing.T) { router := chi.NewRouter() New().Routes(router) server := httptest.NewServer(router) t.Cleanup(server.Close) res, err := http.Get(server.URL + "/openapi.json") if err != nil { t.Fatal(err) } defer res.Body.Close() var spec struct { OpenAPI string `json:"openapi"` } if err := json.NewDecoder(res.Body).Decode(&spec); err != nil { t.Fatal(err) } if res.Header.Get("Content-Type") != "application/json" || spec.OpenAPI != "3.1.0" { t.Errorf( "openapi.json: %s, openapi %q; want JSON, 3.1.0", res.Header.Get("Content-Type"), spec.OpenAPI, ) } res, err = http.Get(server.URL + "/docs") if err != nil { t.Fatal(err) } defer res.Body.Close() page, _ := io.ReadAll(res.Body) if !strings.Contains(string(page), `url: "openapi.json"`) { t.Errorf("docs page doesn't load openapi.json:\n%s", page) } } // Every route the API serves, apart from its description's own, is in the // description, and the description has no route that isn't served, so the // annotations can't fall behind as resources are added. func TestOpenAPICoversRoutes(t *testing.T) { var spec struct { Paths map[string]map[string]json.RawMessage `json:"paths"` } if err := json.Unmarshal(openAPISpec, &spec); err != nil { t.Fatal(err) } documented := map[string]bool{} for path, operations := range spec.Paths { for method := range operations { documented[strings.ToUpper(method)+" "+path] = true } } // A store that is never opened: registering routes doesn't use it. router := chi.NewRouter() New(WithStore(&store.Store{})).Routes(router) description := map[string]bool{ "GET /openapi.json": true, "GET /docs": true, } served := map[string]bool{} err := chi.Walk(router, func( method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler, ) error { if !description[method+" "+route] { served[method+" "+route] = true } return nil }) if err != nil { t.Fatal(err) } for route := range served { if !documented[route] { t.Errorf("%s is served but not in the OpenAPI description", route) } } for route := range documented { if !served[route] { t.Errorf("%s is in the OpenAPI description but not served", route) } } } // The description's list of marks matches OpportunityMarks, so its Enums // annotation can't fall behind the struct. func TestOpenAPIMarkEnum(t *testing.T) { var spec struct { Paths map[string]map[string]struct { Parameters []struct { Name string `json:"name"` Schema struct { Enum []string `json:"enum"` } `json:"schema"` } `json:"parameters"` } `json:"paths"` } if err := json.Unmarshal(openAPISpec, &spec); err != nil { t.Fatal(err) } want := strings.Join(markNames, ",") checked := 0 operations := spec.Paths["/opportunities/{uuid}/marks/{mark}"] for method, operation := range operations { for _, parameter := range operation.Parameters { if parameter.Name != "mark" { continue } checked++ if got := strings.Join(parameter.Schema.Enum, ","); got != want { t.Errorf("%s mark enum = %s; want %s", method, got, want) } } } if checked != 2 { t.Errorf("checked the mark of %d routes; want 2 (PUT, DELETE)", checked) } }