package controller import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" "hy2xs-admin/model/constant" ) // Форма ответа страницы системных логов. // // Панель читает `data.records` и передаёт результат прямо в `:data` таблицы. // Поэтому ответ обязан нести records/total на КАЖДОЙ ветке — иначе таблица // получает undefined и страница ломается целиком. type logPageEnvelope struct { Code int `json:"code"` Type string `json:"type"` Message string `json:"message"` Data *struct { Records []map[string]any `json:"records"` Total int64 `json:"total"` } `json:"data"` } // requestSystemLog выполняет запрос к странице системных логов, подставив // вместо журнала переданное содержимое. Пустой content означает «файла нет». func requestSystemLog(t *testing.T, content *string) (logPageEnvelope, string) { t.Helper() gin.SetMode(gin.TestMode) path := filepath.Join(t.TempDir(), "hy2xs-admin.log") if content != nil { if err := os.WriteFile(path, []byte(*content), 0o600); err != nil { t.Fatalf("не удалось записать тестовый журнал: %v", err) } } previous := constant.SystemLogPath constant.SystemLogPath = path t.Cleanup(func() { constant.SystemLogPath = previous }) engine := gin.New() engine.GET("/log/logSystem", LogSystem) recorder := httptest.NewRecorder() engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/log/logSystem?numLine=100", nil)) raw := recorder.Body.String() var envelope logPageEnvelope if err := json.Unmarshal([]byte(raw), &envelope); err != nil { t.Fatalf("ответ не разбирается как один JSON-документ: %v\nтело: %s", err, raw) } return envelope, raw } // Регрессия LOG-04. Ветка «файла ещё нет» отвечала ГОЛЫМ МАССИВОМ: // // vo.Success(logSystemVos, c) // // Панель получала `data.records === undefined` и передавала undefined в // таблицу. То есть на свежепоставленном хосте — до первой записи в журнал — // страница системных логов не работала вовсе, а это ровно тот сценарий, // который проверяется на приёмке каждой чистой установки. func TestLogSystemKeepsPageShapeWhenFileIsMissing(t *testing.T) { envelope, raw := requestSystemLog(t, nil) if envelope.Type != "ok" { t.Fatalf("отсутствие журнала признано отказом: %s", raw) } if envelope.Data == nil { t.Fatalf("ответ не несёт объекта страницы: %s", raw) } if envelope.Data.Records == nil { t.Fatalf("ответ не несёт поля records: %s", raw) } if len(envelope.Data.Records) != 0 || envelope.Data.Total != 0 { t.Fatalf("ожидалась пустая страница, получено %+v", *envelope.Data) } } // Регрессия LOG-05. При неразбираемой строке вызывался vo.Fail(...) и следом // `continue`: ответ уже записан в поток, цикл идёт дальше, в конце безусловно // выполняется vo.Success. Клиент получал два JSON-документа подряд — то есть // невалидный ответ, — и одной битой строки хватало, чтобы страница перестала // открываться целиком. // // Строка бьётся штатно: lumberjack ротирует файл, и обрыв последней записи на // границе ротации — обычное событие. func TestLogSystemSkipsBrokenLineWithoutBreakingResponse(t *testing.T) { content := `{"level":"info","msg":"GET /api/peers → 200 (7 ms)","time":"2026-09-01 10:00:00"} {"level":"info","msg":"обрыв записи на границе ротации {"level":"warning","msg":"PATCH /api/peers/12 → 400 (4 ms)","time":"2026-09-01 10:00:01"} ` envelope, raw := requestSystemLog(t, &content) if envelope.Type != "ok" { t.Fatalf("битая строка отказала всей выдаче: %s", raw) } if envelope.Data == nil { t.Fatalf("ответ не несёт объекта страницы: %s", raw) } if len(envelope.Data.Records) != 2 { t.Fatalf("ожидались две читаемые записи, получено %d: %+v", len(envelope.Data.Records), envelope.Data.Records) } for _, record := range envelope.Data.Records { if message, _ := record["msg"].(string); message == "" { t.Errorf("запись пришла с пустым msg: %+v", record) } } } // Читаемый журнал доезжает до панели в той же форме. func TestLogSystemReturnsRecordsForReadableFile(t *testing.T) { content := `{"level":"info","msg":"GET /api/peers → 200 (7 ms)","reqMethod":"GET","reqPath":"/api/peers","statusCode":200,"latencyTime":7,"time":"2026-09-01 10:00:00"} ` envelope, raw := requestSystemLog(t, &content) if envelope.Data == nil || len(envelope.Data.Records) != 1 { t.Fatalf("читаемая запись не доехала до панели: %s", raw) } record := envelope.Data.Records[0] if message, _ := record["msg"].(string); message == "" { t.Fatalf("msg потерян при отдаче: %+v", record) } if path, _ := record["reqPath"].(string); path != "/api/peers" { t.Fatalf("структурные поля не доехали: %+v", record) } }