Files
main/tools/test_player_api.sh
Divarion-D aefd1bcbb9 Phase 11: Unify API layer — route all API endpoints through Front Controller
Steps 11.1–11.6: migrated all standalone API entry points to controller classes
dispatched via the Front Controller, then deleted the legacy PHP files.

Controllers created:
- PlayerApiController (player_api.php, 12 actions + numeric aliases)
- Enigma2ApiController (enigma2.php + xplugin.php)
- PlaylistApiController (playlist.php)
- EpgApiController (epg.php)
- InternalApiController (api.php, ~40 server-to-server actions)

Nginx changes (MAIN + LB configs):
- Add location block for streaming API endpoints → FC with XC_SCOPE=api
- Add location block for internal API (/api.php) → FC with XC_API=internal
- Change 6 rewrite rules from break→last for deleted file URLs
- Remove allow/deny from /api.php location — auth handled by PHP
  (password + IP whitelist + brute-force guard)

Front Controller (public/index.php):
- Add section 3a: REST API dispatch (XC_SCOPE=includes/api/*)
- Add section 3b: Streaming API dispatch (XC_SCOPE=api, XC_API=*)
- Move autoloader require to top (section 1b) for all code paths
- Controller map: endpoint name → controller class → init → dispatch

Bootstrap changes:
- www/init.php, www/stream/init.php: replace FC_API_NAME constant with
  direct $rFilename variable (set by FC before require, checked via isset)

Bug fixes:
- PlayerApiController: fix 3 PHP 8 warnings ($rBouquets undefined,
  missing 'rating'/'subtitle' array keys in get_vod_info)
- Fix server-to-server API calls blocked by nginx deny all (api_url_ip
  uses external server IP, not 127.0.0.1)

Deleted files (8):
- www/player_api.php, www/enigma2.php, www/xplugin.php
- www/epg.php, www/playlist.php, www/api.php
- includes/api/admin/index.php, includes/api/reseller/index.php

New files:
- tools/test_player_api.sh — comprehensive Player API test suite (13 sections)
2026-03-12 21:26:20 +03:00

746 lines
28 KiB
Bash
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.
#!/bin/bash
set -euo pipefail
# ═══════════════════════════════════════════════════════════════════
# Player API Test Suite
# ═══════════════════════════════════════════════════════════════════
#
# Тестирует все endpoints PlayerApiController через HTTP.
# Каждый тест проверяет HTTP-статус, Content-Type и структуру JSON.
#
# Использование:
# ./tools/test_player_api.sh <base_url> <username> <password>
# ./tools/test_player_api.sh https://example.com testuser testpass
#
# Опционально (env variables):
# STREAM_ID=123 — ID live-стрима для EPG-тестов
# VOD_ID=456 — ID VOD для get_vod_info
# SERIES_ID=789 — ID сериала для get_series_info
# CATEGORY_ID=10 — ID категории для фильтрации
# VERBOSE=1 — показывать тела ответов
# TIMEOUT=10 — таймаут curl в секундах
#
# ═══════════════════════════════════════════════════════════════════
#
# API Documentation — Player API (player_api.php)
#
# ═══════════════════════════════════════════════════════════════════
#
# Аутентификация:
# GET /player_api.php?username=XXX&password=YYY
# GET /player_api.php?token=ZZZ
#
# Все параметры передаются через query string (GET).
#
# ───────────────────────────────────────────────────────────────────
# Endpoints (action=...)
# ───────────────────────────────────────────────────────────────────
#
# 1. (без action) — Информация о пользователе и сервере
# Params: —
# Response: {
# "user_info": {
# "username", "password", "message", "auth", "status",
# "exp_date", "is_trial", "active_cons", "created_at",
# "max_connections", "allowed_output_formats"
# },
# "server_info": {
# "version", "url", "port", "https_port", "server_protocol",
# "rtmp_port", "timestamp_now", "time_now", "timezone", "process"
# }
# }
#
# 2. get_live_categories — Категории Live/Radio потоков
# Params: —
# Response: [{ "category_id", "category_name", "parent_id" }]
#
# 3. get_vod_categories — Категории VOD (фильмов)
# Params: —
# Response: [{ "category_id", "category_name", "parent_id" }]
#
# 4. get_series_categories — Категории сериалов
# Params: —
# Response: [{ "category_id", "category_name", "parent_id" }]
#
# 5. get_live_streams — Список live-потоков
# Params: category_id (опц.), params[offset], params[items_per_page]
# Response: [{
# "num", "name", "stream_type", "stream_id", "stream_icon",
# "epg_channel_id", "added", "custom_sid", "tv_archive",
# "direct_source", "tv_archive_duration", "category_id",
# "category_ids", "thumbnail"
# }]
#
# 6. get_vod_streams — Список VOD (фильмов)
# Params: category_id (опц.), params[offset], params[items_per_page]
# Response: [{
# "num", "name", "title", "year", "stream_type", "stream_id",
# "stream_icon", "rating", "rating_5based", "added", "plot",
# "cast", "director", "genre", "release_date", "youtube_trailer",
# "episode_run_time", "category_id", "category_ids",
# "container_extension", "custom_sid", "direct_source"
# }]
#
# 7. get_series — Список сериалов
# Params: category_id (опц.)
# Response: [{
# "num", "name", "title", "year", "stream_type", "series_id",
# "cover", "plot", "cast", "director", "genre", "release_date",
# "releaseDate", "last_modified", "rating", "rating_5based",
# "backdrop_path", "youtube_trailer", "episode_run_time",
# "category_id", "category_ids"
# }]
#
# 8. get_series_info — Детальная информация о сериале
# Params: series_id (обяз.)
# Response: {
# "seasons": [{ "cover", "cover_big", ... }],
# "info": { "name", "title", "year", "cover", "plot", ... },
# "episodes": { "1": [{ "id", "episode_num", "title", ... }] }
# }
#
# 9. get_vod_info — Детальная информация о VOD
# Params: vod_id (обяз.)
# Response: {
# "info": { "tmdb_id", "episode_run_time", "releasedate",
# "cover_big", "movie_image", "rating", "backdrop_path",
# "subtitles": [{ "index", "language", "title" }]
# },
# "movie_data": { "stream_id", "name", "title", "year",
# "added", "category_id", "category_ids",
# "container_extension", "custom_sid", "direct_source"
# }
# }
#
# 10. get_epg — Полная EPG-программа для потока(ов)
# Params: stream_id (обяз., одиночный или через запятую)
# from_now (опц., фильтр только будущие)
# Response (single): [{ "title", "description", "start", "end" }]
# Response (multi): { "123": [...], "456": [...] }
#
# 11. get_short_epg — Текущая и ближайшие программы
# Params: stream_id (обяз.), limit (опц., default=4)
# Response: { "epg_listings": [{ "title", "description",
# "start", "stop", "end", "start_timestamp", "stop_timestamp",
# "end_timestamp" }] }
#
# 12. get_simple_data_table — EPG с архивной информацией
# Params: stream_id (обяз.)
# Response: { "epg_listings": [{
# "title", "description", "start", "end",
# "start_timestamp", "stop_timestamp",
# "now_playing", "has_archive"
# }] }
#
# ───────────────────────────────────────────────────────────────────
# Числовые алиасы action
# ───────────────────────────────────────────────────────────────────
#
# 200 → get_vod_categories 205 → get_short_epg
# 201 → get_live_categories 206 → get_series_categories
# 202 → get_live_streams 207 → get_simple_data_table
# 203 → get_vod_streams 208 → get_series
# 204 → get_series_info 209 → get_vod_info
#
# ───────────────────────────────────────────────────────────────────
# Panel API (legacy)
# ───────────────────────────────────────────────────────────────────
#
# URL: /panel_api.php (вместо /player_api.php)
# Требует: настройка legacy_panel_api=1
# Без action: возвращает default info + автозагрузку каналов
# С action: аналогично player_api
#
# ═══════════════════════════════════════════════════════════════════
# ─── Цвета ────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# ─── Аргументы ────────────────────────────────────────────────────
if [[ $# -lt 3 ]]; then
echo -e "${BOLD}Usage:${NC} $0 <base_url> <username> <password>"
echo ""
echo " base_url — URL сервера (https://example.com)"
echo " username — логин пользователя"
echo " password — пароль пользователя"
echo ""
echo " Env vars: STREAM_ID, VOD_ID, SERIES_ID, CATEGORY_ID, VERBOSE, TIMEOUT"
exit 1
fi
BASE_URL="${1%/}"
USERNAME="$2"
PASSWORD="$3"
STREAM_ID="${STREAM_ID:-}"
VOD_ID="${VOD_ID:-}"
SERIES_ID="${SERIES_ID:-}"
CATEGORY_ID="${CATEGORY_ID:-}"
VERBOSE="${VERBOSE:-0}"
TIMEOUT="${TIMEOUT:-10}"
API_URL="${BASE_URL}/player_api.php"
AUTH="username=${USERNAME}&password=${PASSWORD}"
# ─── Счётчики ─────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
WARN=0
# ─── Утилиты ──────────────────────────────────────────────────────
log_pass() { echo -e " ${GREEN}✓ PASS${NC} $1"; ((PASS++)) || true; }
log_fail() { echo -e " ${RED}✗ FAIL${NC} $1"; ((FAIL++)) || true; }
log_skip() { echo -e " ${YELLOW}○ SKIP${NC} $1"; ((SKIP++)) || true; }
log_warn() { echo -e " ${YELLOW}⚠ WARN${NC} $1"; ((WARN++)) || true; }
log_head() { echo -e "\n${CYAN}${BOLD}═══ $1 ═══${NC}"; }
# ─── HTTP-запрос с проверкой ──────────────────────────────────────
#
# api_test <test_name> <url_params> <required_keys...>
#
# Выполняет GET-запрос, проверяет:
# 1. HTTP 200
# 2. Content-Type: application/json
# 3. Ответ — валидный JSON
# 4. Наличие обязательных ключей (если указаны)
#
# Возвращает тело ответа через глобальный RESPONSE.
RESPONSE=""
HTTP_CODE=""
api_get() {
local url="${API_URL}?${AUTH}&${1}"
local tmp_headers
tmp_headers=$(mktemp)
trap "rm -f '$tmp_headers'" RETURN
RESPONSE=$(curl -s -w '\n%{http_code}' \
--max-time "$TIMEOUT" \
-D "$tmp_headers" \
"$url" 2>/dev/null) || {
RESPONSE=""
HTTP_CODE="000"
return 1
}
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
RESPONSE=$(echo "$RESPONSE" | sed '$d')
return 0
}
api_test() {
local test_name="$1"
local params="$2"
shift 2
local required_keys=("$@")
api_get "$params" || true
# HTTP status
if [[ "$HTTP_CODE" != "200" ]]; then
log_fail "${test_name} — HTTP ${HTTP_CODE}"
if [[ "$VERBOSE" == "1" ]] && [[ -n "$RESPONSE" ]]; then
echo " Response: ${RESPONSE:0:200}"
fi
return 1
fi
# Valid JSON
if ! echo "$RESPONSE" | python3 -m json.tool > /dev/null 2>&1; then
log_fail "${test_name} — invalid JSON"
if [[ "$VERBOSE" == "1" ]]; then
echo " Response: ${RESPONSE:0:300}"
fi
return 1
fi
# Required keys
local missing=()
for key in "${required_keys[@]}"; do
if [[ -z "$key" ]]; then
continue
fi
if ! echo "$RESPONSE" | python3 -c "
import json, sys
data = json.load(sys.stdin)
keys = '${key}'.split('.')
obj = data
for k in keys:
if isinstance(obj, dict) and k in obj:
obj = obj[k]
elif isinstance(obj, list) and len(obj) > 0 and k in obj[0]:
obj = obj[0][k]
else:
sys.exit(1)
" 2>/dev/null; then
missing+=("$key")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_warn "${test_name} — missing keys: ${missing[*]}"
if [[ "$VERBOSE" == "1" ]]; then
echo " Response: ${RESPONSE:0:500}"
fi
return 0
fi
log_pass "$test_name"
if [[ "$VERBOSE" == "1" ]]; then
local len=${#RESPONSE}
echo " (${len} bytes)"
fi
return 0
}
# Проверить что ответ — JSON-массив
assert_array() {
local test_name="$1"
if echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if isinstance(d,list) else 1)" 2>/dev/null; then
log_pass "${test_name} — is array"
else
log_fail "${test_name} — expected array"
fi
}
# Проверить что ответ — JSON-объект
assert_object() {
local test_name="$1"
if echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if isinstance(d,dict) else 1)" 2>/dev/null; then
log_pass "${test_name} — is object"
else
log_fail "${test_name} — expected object"
fi
}
# Получить количество элементов в ответе
response_count() {
echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else len(d.keys()))" 2>/dev/null || echo "?"
}
# ═══════════════════════════════════════════════════════════════════
# ТЕСТЫ
# ═══════════════════════════════════════════════════════════════════
echo -e "${BOLD}Player API Test Suite${NC}"
echo -e "URL: ${API_URL}"
echo -e "User: ${USERNAME}"
echo -e "Date: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# ─── 0. Connectivity ─────────────────────────────────────────────
log_head "0. Connectivity"
api_get "" || true
if [[ "$HTTP_CODE" == "000" ]]; then
echo -e "${RED}Cannot connect to ${API_URL}${NC}"
echo "Check URL and network connectivity."
exit 1
fi
log_pass "Server reachable (HTTP ${HTTP_CODE})"
# ─── 1. Auth — Default Info ──────────────────────────────────────
log_head "1. Authentication + Default Info (no action)"
api_test "default info" "" \
"user_info.username" \
"user_info.status" \
"user_info.exp_date" \
"user_info.max_connections" \
"user_info.allowed_output_formats" \
"server_info.version" \
"server_info.url" \
"server_info.port" \
"server_info.timestamp_now"
# Проверяем username в ответе
if echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['user_info']['username'] == '${USERNAME}'
" 2>/dev/null; then
log_pass "username matches"
else
log_fail "username mismatch in response"
fi
# ─── 2. Auth errors ──────────────────────────────────────────────
log_head "2. Auth Error Handling"
# Неверные креды
local_url="${API_URL}?username=__invalid__&password=__invalid__"
local_resp=$(curl -s -w '\n%{http_code}' --max-time "$TIMEOUT" "$local_url" 2>/dev/null) || true
local_code=$(echo "$local_resp" | tail -1)
if [[ "$local_code" != "200" ]] || echo "$local_resp" | head -1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
sys.exit(0 if 'error' in str(d).lower() or d.get('user_info',{}).get('auth') != 1 else 1)
" 2>/dev/null; then
log_pass "invalid credentials rejected"
else
log_warn "invalid credentials may not be properly rejected"
fi
# Пустые креды
local_url="${API_URL}?username=&password="
local_resp=$(curl -s --max-time "$TIMEOUT" "$local_url" 2>/dev/null) || true
log_pass "empty credentials handled (no crash)"
# ─── 3. Categories ───────────────────────────────────────────────
log_head "3. Categories"
api_test "get_live_categories" "action=get_live_categories" \
"category_id" "category_name"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_live_categories"
echo -e " count: $(response_count)"
fi
api_test "get_vod_categories" "action=get_vod_categories" \
"category_id" "category_name"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_vod_categories"
echo -e " count: $(response_count)"
fi
api_test "get_series_categories" "action=get_series_categories" \
"category_id" "category_name"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_series_categories"
echo -e " count: $(response_count)"
fi
# Числовые алиасы
api_test "numeric alias 201=get_live_categories" "action=201"
api_test "numeric alias 200=get_vod_categories" "action=200"
api_test "numeric alias 206=get_series_categories" "action=206"
# ─── 4. Live Streams ─────────────────────────────────────────────
log_head "4. Live Streams"
api_test "get_live_streams" "action=get_live_streams" \
"stream_id" "name" "stream_type" "category_id"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_live_streams"
count=$(response_count)
echo -e " count: ${count}"
# Сохраняем первый stream_id для EPG тестов
if [[ -z "$STREAM_ID" ]]; then
STREAM_ID=$(echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
if d: print(d[0]['stream_id'])
else: print('')
" 2>/dev/null) || true
if [[ -n "$STREAM_ID" ]]; then
echo -e " auto-detected STREAM_ID=${STREAM_ID}"
fi
fi
fi
# С фильтром по категории
if [[ -n "$CATEGORY_ID" ]]; then
api_test "get_live_streams (category_id=${CATEGORY_ID})" \
"action=get_live_streams&category_id=${CATEGORY_ID}" \
"stream_id"
else
log_skip "get_live_streams + category_id (set CATEGORY_ID env)"
fi
# Числовой алиас
api_test "numeric alias 202=get_live_streams" "action=202"
# ─── 5. VOD Streams ──────────────────────────────────────────────
log_head "5. VOD Streams"
api_test "get_vod_streams" "action=get_vod_streams" \
"stream_id" "name" "container_extension"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_vod_streams"
count=$(response_count)
echo -e " count: ${count}"
# Сохраняем первый vod_id
if [[ -z "$VOD_ID" ]]; then
VOD_ID=$(echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
if d: print(d[0]['stream_id'])
else: print('')
" 2>/dev/null) || true
if [[ -n "$VOD_ID" ]]; then
echo -e " auto-detected VOD_ID=${VOD_ID}"
fi
fi
fi
api_test "numeric alias 203=get_vod_streams" "action=203"
# ─── 6. Series ────────────────────────────────────────────────────
log_head "6. Series"
api_test "get_series" "action=get_series" \
"series_id" "name" "title"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_series"
count=$(response_count)
echo -e " count: ${count}"
# Сохраняем первый series_id
if [[ -z "$SERIES_ID" ]]; then
SERIES_ID=$(echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
if d: print(d[0]['series_id'])
else: print('')
" 2>/dev/null) || true
if [[ -n "$SERIES_ID" ]]; then
echo -e " auto-detected SERIES_ID=${SERIES_ID}"
fi
fi
fi
api_test "numeric alias 208=get_series" "action=208"
# ─── 7. Series Info ───────────────────────────────────────────────
log_head "7. Series Info"
if [[ -n "$SERIES_ID" ]]; then
api_test "get_series_info (id=${SERIES_ID})" \
"action=get_series_info&series_id=${SERIES_ID}" \
"info.name" "seasons"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_object "get_series_info"
# Проверяем episodes
has_episodes=$(echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('yes' if 'episodes' in d and d['episodes'] else 'no')
" 2>/dev/null) || true
if [[ "$has_episodes" == "yes" ]]; then
log_pass "get_series_info has episodes"
else
log_warn "get_series_info — no episodes found"
fi
fi
api_test "numeric alias 204=get_series_info" \
"action=204&series_id=${SERIES_ID}"
else
log_skip "get_series_info (no SERIES_ID available)"
log_skip "numeric alias 204 (no SERIES_ID)"
fi
# ─── 8. VOD Info ──────────────────────────────────────────────────
log_head "8. VOD Info"
if [[ -n "$VOD_ID" ]]; then
api_test "get_vod_info (id=${VOD_ID})" \
"action=get_vod_info&vod_id=${VOD_ID}" \
"info" "movie_data.stream_id"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_object "get_vod_info"
# Проверяем subtitles
has_subtitles=$(echo "$RESPONSE" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('yes' if 'subtitles' in d.get('info', {}) else 'no')
" 2>/dev/null) || true
if [[ "$has_subtitles" == "yes" ]]; then
log_pass "get_vod_info has subtitles field"
else
log_warn "get_vod_info — no subtitles field"
fi
fi
api_test "numeric alias 209=get_vod_info" \
"action=209&vod_id=${VOD_ID}"
else
log_skip "get_vod_info (no VOD_ID available)"
log_skip "numeric alias 209 (no VOD_ID)"
fi
# ─── 9. EPG ───────────────────────────────────────────────────────
log_head "9. EPG"
if [[ -n "$STREAM_ID" ]]; then
api_test "get_epg (stream_id=${STREAM_ID})" \
"action=get_epg&stream_id=${STREAM_ID}"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_array "get_epg (single)"
echo -e " entries: $(response_count)"
fi
# EPG from_now
api_test "get_epg + from_now" \
"action=get_epg&stream_id=${STREAM_ID}&from_now=1"
# Short EPG
api_test "get_short_epg (stream_id=${STREAM_ID})" \
"action=get_short_epg&stream_id=${STREAM_ID}" \
"epg_listings"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_object "get_short_epg"
fi
# Short EPG with limit
api_test "get_short_epg + limit=2" \
"action=get_short_epg&stream_id=${STREAM_ID}&limit=2" \
"epg_listings"
# Simple data table
api_test "get_simple_data_table (stream_id=${STREAM_ID})" \
"action=get_simple_data_table&stream_id=${STREAM_ID}" \
"epg_listings"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_object "get_simple_data_table"
fi
# Числовой алиас
api_test "numeric alias 205=get_short_epg" \
"action=205&stream_id=${STREAM_ID}"
api_test "numeric alias 207=get_simple_data_table" \
"action=207&stream_id=${STREAM_ID}"
else
log_skip "get_epg (no STREAM_ID available — set STREAM_ID env)"
log_skip "get_short_epg (no STREAM_ID)"
log_skip "get_simple_data_table (no STREAM_ID)"
log_skip "numeric aliases 205, 207 (no STREAM_ID)"
fi
# ─── 10. Multi-stream EPG ────────────────────────────────────────
log_head "10. Multi-stream EPG"
if [[ -n "$STREAM_ID" ]]; then
# Multi через запятую
api_test "get_epg multi (stream_id=${STREAM_ID},${STREAM_ID})" \
"action=get_epg&stream_id=${STREAM_ID},${STREAM_ID}"
if [[ "$HTTP_CODE" == "200" ]]; then
assert_object "get_epg multi"
fi
api_test "get_short_epg multi" \
"action=get_short_epg&stream_id=${STREAM_ID},${STREAM_ID}"
else
log_skip "multi-stream EPG (no STREAM_ID)"
fi
# ─── 11. Pagination ──────────────────────────────────────────────
log_head "11. Pagination"
api_test "get_live_streams offset=0, limit=3" \
"action=get_live_streams&params[offset]=0&params[items_per_page]=3"
if [[ "$HTTP_CODE" == "200" ]]; then
count=$(response_count)
if [[ "$count" -le 3 ]] 2>/dev/null; then
log_pass "pagination limit respected (got ${count})"
else
log_warn "pagination: expected ≤3, got ${count}"
fi
fi
api_test "get_vod_streams offset=0, limit=2" \
"action=get_vod_streams&params[offset]=0&params[items_per_page]=2"
# ─── 12. Edge Cases ──────────────────────────────────────────────
log_head "12. Edge Cases"
# Неизвестный action → default info
api_test "unknown action → default info" \
"action=nonexistent_action" \
"user_info" "server_info"
# get_vod_info без vod_id → пустой info
api_test "get_vod_info without vod_id" \
"action=get_vod_info" \
"info"
# get_series_info без series_id
api_test "get_series_info without series_id" \
"action=get_series_info"
# get_epg без stream_id → пустой массив
api_test "get_epg without stream_id" \
"action=get_epg"
# Несуществующий stream_id
api_test "get_epg invalid stream_id" \
"action=get_epg&stream_id=999999999"
# Несуществующий vod_id
api_test "get_vod_info invalid vod_id" \
"action=get_vod_info&vod_id=999999999" \
"info"
# ─── 13. Panel API (legacy) ──────────────────────────────────────
log_head "13. Panel API (legacy)"
PANEL_URL="${BASE_URL}/panel_api.php?${AUTH}"
panel_resp=$(curl -s -w '\n%{http_code}' --max-time "$TIMEOUT" "$PANEL_URL" 2>/dev/null) || true
panel_code=$(echo "$panel_resp" | tail -1)
if [[ "$panel_code" == "200" ]]; then
panel_body=$(echo "$panel_resp" | sed '$d')
if echo "$panel_body" | python3 -m json.tool > /dev/null 2>&1; then
log_pass "panel_api.php returns valid JSON (HTTP 200)"
else
log_warn "panel_api.php returns HTTP 200 but invalid JSON"
fi
else
log_warn "panel_api.php returned HTTP ${panel_code} (may be disabled)"
fi
# ═══════════════════════════════════════════════════════════════════
# РЕЗУЛЬТАТЫ
# ═══════════════════════════════════════════════════════════════════
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════${NC}"
echo -e "${BOLD} Results${NC}"
echo -e "${BOLD}═══════════════════════════════════════════════${NC}"
echo -e " ${GREEN}PASS:${NC} ${PASS}"
echo -e " ${RED}FAIL:${NC} ${FAIL}"
echo -e " ${YELLOW}WARN:${NC} ${WARN}"
echo -e " ${YELLOW}SKIP:${NC} ${SKIP}"
TOTAL=$((PASS + FAIL + WARN + SKIP))
echo -e " ${BOLD}TOTAL: ${TOTAL}${NC}"
echo ""
if [[ "$FAIL" -gt 0 ]]; then
echo -e "${RED}${BOLD}Some tests FAILED!${NC}"
exit 1
else
echo -e "${GREEN}${BOLD}All tests passed.${NC}"
exit 0
fi