This commit is contained in:
2026-07-13 12:28:59 +08:00
parent 6c3de4b2ef
commit 1fca796f2c
56 changed files with 9709 additions and 406 deletions
+255
View File
@@ -0,0 +1,255 @@
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, `<span id="chcount">3</span>`)},
// Данные каналов в 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 возвращает <tr>", strings.Contains(body, "chrow")},
// Счётчики
{"OnlineCount отображается (~66%)", strings.Contains(body, "2 (66%)") || strings.Contains(body, "2 (67%)")},
{"OfflineCount отображается (~33%)", strings.Contains(body, "1 (33%)") || strings.Contains(body, "1 (34%)")},
}
for _, c := range checks {
if !c.want {
t.Errorf("page missing: %s", c.name)
}
}
}
// TestDetailsPageOfflinePlaylist проверяет, что для оффлайн-плейлиста
// (IsKnown=true, IsOnline=false) вместо таблицы каналов показывается
// сообщение об ошибке
func TestDetailsPageOfflinePlaylist(t *testing.T) {
ts, srv := makeTestServer(t)
srv.putPlaylist(playlist.Playlist{
Code: "test1",
Name: "Test Playlist 1",
Url: "http://example.com/test1.m3u8",
IsOnline: false,
Content: "HTTP status 404",
CheckedAt: 1700000000,
})
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("expected 200, got %d", resp.StatusCode)
}
body := fetchBody(t, resp)
if !strings.Contains(body, "Ошибка плейлиста") {
t.Error("offline playlist should show error message")
}
if !strings.Contains(body, "HTTP status 404") {
t.Error("offline playlist should show error text from Content")
}
if strings.Contains(body, `id="chlist"`) {
t.Error("offline playlist should NOT render channels table")
}
}
// TestDetailsPageUnknownPlaylist проверяет, что для непроверенного плейлиста
// (IsKnown=false) вместо таблицы каналов показывается предупреждение
func TestDetailsPageUnknownPlaylist(t *testing.T) {
ts, _ := makeTestServer(t)
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("expected 200, got %d", resp.StatusCode)
}
body := fetchBody(t, resp)
if !strings.Contains(body, "Список каналов сейчас неизвестен") {
t.Error("unknown playlist should show 'list unknown' warning")
}
if strings.Contains(body, `id="chlist"`) {
t.Error("unknown playlist should NOT render channels table")
}
}
// TestDetailsPageNonexistentPlaylist проверяет, что несуществующий код
// возвращает 404
func TestDetailsPageNonexistentPlaylist(t *testing.T) {
ts, _ := makeTestServer(t)
resp, err := http.Get(ts.URL + "/nonexistent/details")
if err != nil {
t.Fatalf("GET /nonexistent/details: %s", err)
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("expected 404, got %d", resp.StatusCode)
}
resp.Body.Close()
}