package handlers import ( "encoding/json" "net" "net/http" "os" "path/filepath" "strings" "testing" "time" "teraclone/internal/service" ) // Opt-in visual fixture, compiled only into tests. No connection to the device, // no persistent credentials, and no writes through the HTTP surface. func TestLogExplorerPreview(t *testing.T) { if os.Getenv("TERA_LOG_PREVIEW") != "1" { t.Skip("set TERA_LOG_PREVIEW=1 for a five-minute loopback-only preview") } app, err := service.NewAppService(filepath.Join(t.TempDir(), "preview.db")) if err != nil { t.Fatal(err) } defer app.Close() handler, err := NewDeviceHandler(app, "http://127.0.0.1:1") if err != nil { t.Fatal(err) } handler.sessions["preview-only"] = browserSession{Username: "UI 미리보기 · 테스트 데이터", BackendToken: "not-a-device-token", Role: "administrator", WorkspaceID: "log-preview"} mux := http.NewServeMux() handler.RegisterRoutes(mux) listener, err := net.Listen("tcp", "127.0.0.1:19083") if err != nil { t.Fatal(err) } server := &http.Server{ReadHeaderTimeout: 3 * time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Preview is read-only; no logs were deleted", http.StatusMethodNotAllowed) return } if strings.HasPrefix(r.URL.Path, "/api/v1/") { w.Header().Set("Content-Type", "application/json") var payload any = []any{} switch r.URL.Path { case "/api/v1/system/status": payload = map[string]any{"system_time": time.Now().UTC(), "cpu_usage_percent": 15, "memory_usage_percent": 28, "memory_total_bytes": 2000000000, "memory_used_bytes": 560000000, "memory_available_bytes": 1440000000, "storage": map[string]any{"path": "/userdata", "total_bytes": 8000000000, "used_bytes": 1600000000, "available_bytes": 6000000000, "usage_percent": 20}} case "/api/v1/network": payload = map[string]any{"host_name": "미리보기", "operating_mode": "Single", "interfaces": []any{}} case "/api/v1/logs/system": payload = []map[string]any{{"timestamp": time.Now().UTC(), "category": "system", "severity": "info", "source": "preview", "message": "테스트: 일반 시스템 이벤트"}, {"timestamp": time.Now().UTC(), "category": "system", "severity": "warning", "source": "authentication", "message": "테스트: 보존 감사 기록", "audit_protected": true, "audit_id": "preview-audit"}} case "/api/v1/logs/links": payload = []map[string]any{{"timestamp": time.Now().UTC(), "event_type": "snapshot", "resource": "CH18", "state": "up", "message": "must not display"}, {"timestamp": time.Now().UTC(), "event_type": "transition", "resource": "CH18", "previous_state": "down", "state": "up", "signal": "RX", "message": "테스트: 상태 변경"}} case "/api/v1/time/settings": payload = map[string]any{"timezone": "Asia/Seoul"} case "/api/v1/logs/port": channel := r.URL.Query().Get("channel") if channel == "" || channel == "CH18" { payload = []map[string]any{{"timestamp": time.Now().UTC(), "channel": "CH18", "event_type": "traffic", "direction": "RX", "actor": "preview", "message": "테스트 데이터: 장비 응답을 수신했습니다."}} } } _ = json.NewEncoder(w).Encode(payload) return } if !strings.HasPrefix(r.URL.Path, "/static/") && r.URL.Path != "/" && r.URL.Path != "/logs/ports" && r.URL.Path != "/logs/system" && r.URL.Path != "/logs/links" { http.NotFound(w, r) return } r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "preview-only"}) mux.ServeHTTP(w, r) })} defer server.Close() go server.Serve(listener) t.Log("READ_ONLY_PREVIEW http://127.0.0.1:19083/logs/ports") <-time.After(5 * time.Minute) }