package service import ( "net/http" "testing" "hy2xs-admin/model/vo" ) // Дашборд не имеет права утверждать физически несовместимые вещи. // // Что было: // // Hysteria stopped // Traffic Stats API reachable // 0 online users // // Все три строки выводились из ОДНОГО ответа systemctl: общий Hysteria2Online // при неактивной службе отдавал пустую карту без ошибки, и сборщик метрик // выставлял `apiReachable = true`, ни разу не сходив в API. // // Матрица ниже проверяет все четыре клетки: два источника, два исхода каждый. func TestParseSystemdActiveState(t *testing.T) { cases := []struct { name string output string want HysteriaServiceState }{ {"active", "active\n", HysteriaServiceActive}, {"inactive", "inactive\n", HysteriaServiceInactive}, {"failed", "failed\n", HysteriaServiceInactive}, {"activating", "activating\n", HysteriaServiceInactive}, {"deactivating", "deactivating\n", HysteriaServiceInactive}, {"пробелы по краям", " active ", HysteriaServiceActive}, {"несколько строк", "active\nactive\n", HysteriaServiceActive}, {"пустой ответ", "", HysteriaServiceUnknown}, // Список состояний systemd пополнялся и раньше. Молчаливое приведение // незнакомого слова к «остановлена» вернуло бы ровно ту ложь, ради // которой заведено третье значение. {"неизвестное состояние", "quantum\n", HysteriaServiceUnknown}, {"текст ошибки вместо состояния", "Failed to connect to bus\n", HysteriaServiceUnknown}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := parseSystemdActiveState(tc.output); got != tc.want { t.Fatalf("parseSystemdActiveState(%q) = %q, ожидалось %q", tc.output, got, tc.want) } }) } } // Клетка 1: служба активна, API отвечает. func TestDashboardStateActiveServiceReachableApi(t *testing.T) { newTestDB(t) startTrafficStats(t, &trafficStatsStub{online: map[string]int64{"alpha-auth-id": 2}}) withHysteriaServiceState(t, HysteriaServiceActive) CollectMetricsSnapshot() snapshot := DashboardSnapshot() if snapshot.Hysteria.ServiceState != string(HysteriaServiceActive) || !snapshot.Hysteria.Running { t.Fatalf("состояние службы: %+v", snapshot.Hysteria) } if !snapshot.Hysteria.ApiReachable { t.Fatal("API отвечает, но объявлен недоступным") } if snapshot.OnlineDevices != 2 || snapshot.OnlinePeers != 1 { t.Fatalf("картина подключений: пиров %d, устройств %d", snapshot.OnlinePeers, snapshot.OnlineDevices) } } // Клетка 2: служба неактивна, API молчит. Оба факта согласованы, и оба верны. func TestDashboardStateInactiveServiceUnreachableApi(t *testing.T) { newTestDB(t) pointHysteriaConfigAtDeadPort(t) withHysteriaServiceState(t, HysteriaServiceInactive) CollectMetricsSnapshot() snapshot := DashboardSnapshot() if snapshot.Hysteria.ServiceState != string(HysteriaServiceInactive) { t.Fatalf("состояние службы объявлено как %q", snapshot.Hysteria.ServiceState) } if snapshot.Hysteria.ApiReachable { t.Fatal("недоступный Traffic Stats API объявлен доступным") } } // Клетка 3 — главная регрессия: спросить systemctl не удалось, но Hysteria // жива и API отвечает. // // Прежний путь показывал «служба остановлена» и «API доступен» одновременно, // причём второе — не сходив в API. Теперь неизвестность названа неизвестностью, // а доступность API получена фактическим обращением. func TestDashboardStateUnknownServiceReachableApi(t *testing.T) { newTestDB(t) startTrafficStats(t, &trafficStatsStub{online: map[string]int64{"alpha-auth-id": 1}}) withHysteriaServiceState(t, HysteriaServiceUnknown) CollectMetricsSnapshot() snapshot := DashboardSnapshot() if snapshot.Hysteria.ServiceState != string(HysteriaServiceUnknown) { t.Fatalf("неизвестное состояние выдано за %q", snapshot.Hysteria.ServiceState) } if snapshot.Hysteria.Running { t.Fatal("неизвестное состояние выдано за работающую службу") } if !snapshot.Hysteria.ApiReachable { t.Fatal("мнение systemd подменило фактический ответ Traffic Stats API") } if snapshot.OnlineDevices != 1 { t.Fatalf("картина подключений потеряна: устройств %d", snapshot.OnlineDevices) } } // Клетка 4: служба активна, но API отказывает — например, Hysteria ещё // поднимается или порт занят чужим процессом. func TestDashboardStateActiveServiceFailingApi(t *testing.T) { newTestDB(t) startTrafficStats(t, &trafficStatsStub{onlineStatus: http.StatusInternalServerError}) withHysteriaServiceState(t, HysteriaServiceActive) CollectMetricsSnapshot() snapshot := DashboardSnapshot() if !snapshot.Hysteria.Running { t.Fatal("активная служба объявлена остановленной") } if snapshot.Hysteria.ApiReachable { t.Fatal("отказ API объявлен доступностью") } if snapshot.HysteriaState.Status != "error" { t.Fatalf("отказ API не попал в состояние данных: %+v", snapshot.HysteriaState) } } // «Состояние неизвестно» и «служба остановлена» — разные сообщения оператору с // разной срочностью и разными действиями. func TestDashboardSecurityRisksDistinguishUnknownFromStopped(t *testing.T) { riskKeys := func(state HysteriaServiceState, apiReachable bool) []string { summary := vo.DashboardSummaryVo{} summary.Hysteria.ServiceState = string(state) summary.Hysteria.Running = state == HysteriaServiceActive summary.Hysteria.ApiReachable = apiReachable keys := make([]string, 0) for _, risk := range DashboardSecurityRisks(summary) { keys = append(keys, risk.Key) } return keys } if got := riskKeys(HysteriaServiceActive, true); len(got) != 0 { t.Fatalf("исправная система получила предупреждения: %v", got) } stopped := riskKeys(HysteriaServiceInactive, true) if len(stopped) != 1 || stopped[0] != "dashboard.security.hysteriaStopped" { t.Fatalf("остановленная служба: %v", stopped) } unknown := riskKeys(HysteriaServiceUnknown, true) if len(unknown) != 1 || unknown[0] != "dashboard.security.hysteriaStateUnknown" { t.Fatalf("неизвестное состояние выдано за остановленную службу: %v", unknown) } both := riskKeys(HysteriaServiceInactive, false) if len(both) != 2 { t.Fatalf("два независимых отказа схлопнулись в один: %v", both) } }