256 lines
8.8 KiB
Go
256 lines
8.8 KiB
Go
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()
|
||
}
|