172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|