package web import ( "axenov/iptv-checker/app/config" "axenov/iptv-checker/app/playlist" "io" "net/http" "net/http/httptest" "strings" "testing" ) // makeTestServer создаёт HTTP-сервер с тестовыми плейлистами в memCache. // Возвращает сервер и обёртку для добавления плейлистов в memCache. func makeTestServer(t *testing.T) (*httptest.Server, *Server) { t.Helper() cfg := config.Init("") cfg.App.Playlists = "testdata/playlists.ini" srv := NewServer(cfg, nil) mux := http.NewServeMux() // API routes mux.HandleFunc("GET /api/playlists", srv.handleAPIGetAll) mux.HandleFunc("GET /api/playlists/{code}", srv.handleAPIGetOne) mux.HandleFunc("GET /api/playlists/{code}/channels", srv.handleAPIGetChannels) mux.HandleFunc("GET /api/version", srv.handleAPIVersion) mux.HandleFunc("GET /api/health", srv.handleAPIHealth) mux.HandleFunc("GET /api/stats", srv.handleAPIStats) mux.HandleFunc("GET /api/openapi.json", srv.handleOpenAPISpec) mux.HandleFunc("GET /api", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/api/", http.StatusMovedPermanently) }) mux.Handle("GET /api/", http.StripPrefix("/api/", swaggerFileServer)) // page routes mux.HandleFunc("GET /{$}", srv.handleHome) mux.HandleFunc("GET /{path...}", srv.handleRoute) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) return ts, srv } // putPlaylist кладёт плейлист в in-memory кеш сервера func (s *Server) putPlaylist(pls playlist.Playlist) { s.memMu.Lock() defer s.memMu.Unlock() if s.memCache == nil { s.memCache = make(map[string]playlist.Playlist) } s.memCache[pls.Code] = pls } // makeOnlinePlaylist создаёт тестовый плейлист с онлайн/оффлайн каналами func makeOnlinePlaylist() playlist.Playlist { return playlist.Playlist{ Code: "test1", Name: "Test Playlist 1", Description: "A test playlist", Url: "http://example.com/test1.m3u8", Source: "test source", IsOnline: true, CheckedAt: 1700000000, Attributes: map[string]string{"url-tvg": "http://example.com/epg.xml"}, Groups: map[string]playlist.Group{ "group1": {Id: "group1", Name: "Group One"}, "group2": {Id: "group2", Name: "Group Two"}, }, Channels: map[string]playlist.Channel{ "id1": { Id: "id1", Title: "Channel One", URL: "http://example.com/stream1.m3u8", GroupId: "group1", Attributes: map[string]string{"tvg-id": "ch1", "tvg-logo": "http://example.com/logo1.png"}, Status: 200, IsOnline: true, Tags: []string{"hd"}, }, "id2": { Id: "id2", Title: "Channel Two", URL: "http://example.com/stream2.m3u8", GroupId: "group2", Attributes: map[string]string{"tvg-id": "ch2", "tvg-logo": "http://example.com/logo2.png"}, Status: 404, IsOnline: false, Tags: []string{"untagged"}, }, "id3": { Id: "id3", Title: "Channel Three", URL: "http://example.com/stream3.m3u8", GroupId: "group1", Attributes: map[string]string{"tvg-id": "ch3", "tvg-logo": "http://example.com/logo3.png"}, Status: 200, IsOnline: true, Tags: []string{"hd", "news"}, }, }, OnlineCount: 2, OfflineCount: 1, } } // fetchBody читает тело HTTP-ответа целиком и возвращает его как строку func fetchBody(t *testing.T, resp *http.Response) string { t.Helper() defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read body: %s", err) } return string(b) } // TestDetailsPageRendersChannelsTable проверяет, что на странице деталей // онлайн-плейлиста присутствует таблица с каналами и данными для list.js func TestDetailsPageRendersChannelsTable(t *testing.T) { ts, srv := makeTestServer(t) srv.putPlaylist(makeOnlinePlaylist()) // Главная страница должна быть доступна resp, err := http.Get(ts.URL + "/") if err != nil { t.Fatalf("GET /: %s", err) } if resp.StatusCode != http.StatusOK { t.Errorf("GET /: expected 200, got %d", resp.StatusCode) } resp.Body.Close() // Страница деталей — основной сценарий resp, err = http.Get(ts.URL + "/test1/details") if err != nil { t.Fatalf("GET /test1/details: %s", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("GET /test1/details: expected 200, got %d", resp.StatusCode) } body := fetchBody(t, resp) checks := []struct { name string want bool }{ // Заголовок и базовая структура {"заголовок 'Список каналов'", strings.Contains(body, "Список каналов")}, {"id таблицы 'chlist'", strings.Contains(body, `id="chlist"`)}, {"класс 'list' у tbody", strings.Contains(body, `class="list"`)}, {"chcount = 3", strings.Contains(body, `3`)}, // Данные каналов в JSON {"values JSON", strings.Contains(body, "const values =")}, {"Channel One в JSON", strings.Contains(body, "Channel One")}, {"Channel Two в JSON", strings.Contains(body, "Channel Two")}, {"Channel Three в JSON", strings.Contains(body, "Channel Three")}, // Группы (отображаются когда > 1) {"id группы group1", strings.Contains(body, `value="group1"`)}, {"id группы group2", strings.Contains(body, `value="group2"`)}, // Теги {"тег #hd", strings.Contains(body, "btn-tag-hd")}, {"тег #news", strings.Contains(body, "btn-tag-news")}, {"тег #untagged", strings.Contains(body, "btn-tag-untagged")}, // list.js {"list.js подключён", strings.Contains(body, "list.js@2.3.1")}, {"getChannelTemplate функция", strings.Contains(body, "function getChannelTemplate")}, {"опции list.js", strings.Contains(body, "const options =")}, {"new List('chlist')", strings.Contains(body, "new List('chlist'")}, {"getChannelTemplate возвращает