Files
iptvc/app/web/details_test.go
T
2026-07-19 19:14:55 +08:00

256 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}