wip6
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user