package web import ( "encoding/json" "net/http" "strings" "testing" ) // TestOpenAPISpec проверяет, что /api/openapi.json отдаёт валидную спецификацию func TestOpenAPISpec(t *testing.T) { ts, _ := makeTestServer(t) resp, err := http.Get(ts.URL + "/api/openapi.json") if err != nil { t.Fatalf("GET /api/openapi.json: %s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") { t.Errorf("expected application/json content-type, got %s", ct) } var spec map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&spec); err != nil { t.Fatalf("cannot decode spec: %s", err) } // базовые поля OpenAPI 3.0 if v, ok := spec["openapi"].(string); !ok || !strings.HasPrefix(v, "3.0") { t.Errorf("expected openapi 3.0.x, got %v", spec["openapi"]) } info, ok := spec["info"].(map[string]interface{}) if !ok { t.Fatal("missing 'info' section") } if _, ok := info["title"].(string); !ok { t.Error("missing info.title") } if _, ok := info["version"].(string); !ok { t.Error("missing info.version") } // сервер должен быть указан servers, ok := spec["servers"].([]interface{}) if !ok || len(servers) == 0 { t.Error("expected at least one server") } // пути paths, ok := spec["paths"].(map[string]interface{}) if !ok { t.Fatal("missing 'paths' section") } expectedPaths := []string{ "/api/playlists", "/api/playlists/{code}", "/api/playlists/{code}/channels", "/api/version", "/api/health", "/api/stats", } for _, p := range expectedPaths { if _, ok := paths[p]; !ok { t.Errorf("missing path %s in spec", p) } } // компоненты/схемы components, ok := spec["components"].(map[string]interface{}) if !ok { t.Fatal("missing 'components' section") } schemas, ok := components["schemas"].(map[string]interface{}) if !ok { t.Fatal("missing 'components.schemas' section") } expectedSchemas := []string{ "PlaylistListResponse", "PlaylistResponse", "Playlist", "ChannelListResponse", "Channel", "Group", "Attribute", "VersionResponse", "HealthResponse", "StatsResponse", "ErrorResponse", } for _, s := range expectedSchemas { if _, ok := schemas[s]; !ok { t.Errorf("missing schema %s in components.schemas", s) } } } // TestSwaggerUI проверяет, что /api/ отдаёт HTML-страницу Swagger UI // и что CSS/JS-ассеты доступны без внешних зависимостей func TestSwaggerUI(t *testing.T) { ts, _ := makeTestServer(t) // /api → редирект на /api/ noRedirect := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} resp, err := noRedirect.Get(ts.URL + "/api") if err != nil { t.Fatalf("GET /api: %s", err) } resp.Body.Close() if resp.StatusCode != http.StatusMovedPermanently { t.Fatalf("expected 301 redirect, got %d", resp.StatusCode) } // /api/ → HTML-страница resp, err = http.Get(ts.URL + "/api/") if err != nil { t.Fatalf("GET /api/: %s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Errorf("expected text/html content-type, got %s", ct) } body := fetchBody(t, resp) if !strings.Contains(body, "swagger-ui") { t.Error("Swagger UI page should contain 'swagger-ui' reference") } if !strings.Contains(body, "/api/openapi.json") { t.Error("Swagger UI page should reference /api/openapi.json") } // проверяем, что нет ссылок на CDN if strings.Contains(body, "cdn.jsdelivr.net") || strings.Contains(body, "unpkg.com") { t.Error("Swagger UI page should not reference any CDN") } // CSS-ассет respCSS, err := http.Get(ts.URL + "/api/swagger-ui.css") if err != nil { t.Fatalf("GET /api/swagger-ui.css: %s", err) } defer respCSS.Body.Close() if respCSS.StatusCode != http.StatusOK { t.Fatalf("expected 200 for CSS, got %d", respCSS.StatusCode) } if ct := respCSS.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/css") { t.Errorf("expected text/css content-type, got %s", ct) } // JS-ассет respJS, err := http.Get(ts.URL + "/api/swagger-ui-bundle.js") if err != nil { t.Fatalf("GET /api/swagger-ui-bundle.js: %s", err) } defer respJS.Body.Close() if respJS.StatusCode != http.StatusOK { t.Fatalf("expected 200 for JS, got %d", respJS.StatusCode) } if ct := respJS.Header.Get("Content-Type"); !strings.Contains(ct, "javascript") { t.Errorf("expected javascript content-type, got %s", ct) } }