package httpapi import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "evobgp/internal/runtimelogs" ) func TestRuntimeLogsFSUnavailable503(t *testing.T) { srv, err := New(Options{SeedDemo: true, InsecureDev: true}) if err != nil { t.Fatal(err) } defer srv.Close() handler := srv.Handler() tests := []struct { method string path string }{ {http.MethodGet, "/v1/runtime-logs/files"}, {http.MethodGet, "/v1/runtime-logs/files/evobgp-all.log"}, {http.MethodDelete, "/v1/runtime-logs/files/evobgp-all.log"}, } for _, tc := range tests { t.Run(tc.method+" "+tc.path, func(t *testing.T) { req := httptest.NewRequest(tc.method, tc.path, nil) req.Header.Set("Authorization", "Bearer dev") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "runtime_logs_unavailable") { t.Fatalf("expected runtime_logs_unavailable detail, body=%s", rec.Body.String()) } }) } } func TestRuntimeLogsCleanupAuditWithoutFS(t *testing.T) { srv, err := New(Options{SeedDemo: true, InsecureDev: true}) if err != nil { t.Fatal(err) } defer srv.Close() handler := srv.Handler() req := httptest.NewRequest(http.MethodGet, "/v1/runtime-logs/cleanup-audit", nil) req.Header.Set("Authorization", "Bearer dev") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } } func TestRuntimeLogsHappyPath(t *testing.T) { dir := t.TempDir() logPath := filepath.Join(dir, "evobgp-all.log") if err := os.WriteFile(logPath, []byte("line1\nline2\nline3\n"), 0o644); err != nil { t.Fatal(err) } srv, err := New(Options{SeedDemo: true, InsecureDev: true}) if err != nil { t.Fatal(err) } defer srv.Close() srv.runtimeLogs = runtimelogs.NewService(runtimelogs.Config{ RootDir: dir, ServiceName: runtimelogs.ServiceNameAll, }) handler := srv.Handler() tenant, _, _, _, _ := srv.Store().DemoIDs() mustSetTestAPIKeys(t, srv, "vwkey|"+tenant+"|viewer,opkey|"+tenant+"|operator") t.Run("list", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/v1/runtime-logs/files", nil) req.Header.Set("Authorization", "Bearer vwkey") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "evobgp-all.log") { t.Fatalf("expected file in list, body=%s", rec.Body.String()) } }) t.Run("tail", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/v1/runtime-logs/files/evobgp-all.log?lines=2", nil) req.Header.Set("Authorization", "Bearer vwkey") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "line2") || !strings.Contains(rec.Body.String(), "line3") { t.Fatalf("unexpected tail body=%s", rec.Body.String()) } }) t.Run("viewer cannot cleanup", func(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, "/v1/runtime-logs/files/evobgp-all.log", nil) req.Header.Set("Authorization", "Bearer vwkey") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } }) t.Run("cleanup truncate and audit", func(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, "/v1/runtime-logs/files/evobgp-all.log?mode=truncate", nil) req.Header.Set("Authorization", "Bearer opkey") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"action":"truncate"`) { t.Fatalf("unexpected cleanup body=%s", rec.Body.String()) } st, err := os.Stat(logPath) if err != nil { t.Fatal(err) } if st.Size() != 0 { t.Fatalf("expected truncated file, size=%d", st.Size()) } auditReq := httptest.NewRequest(http.MethodGet, "/v1/runtime-logs/cleanup-audit", nil) auditReq.Header.Set("Authorization", "Bearer vwkey") auditRec := httptest.NewRecorder() handler.ServeHTTP(auditRec, auditReq) if auditRec.Code != http.StatusOK { t.Fatalf("audit status=%d body=%s", auditRec.Code, auditRec.Body.String()) } if !strings.Contains(auditRec.Body.String(), "evobgp-all.log") { t.Fatalf("expected audit entry, body=%s", auditRec.Body.String()) } }) t.Run("auto estimate and run", func(t *testing.T) { bigPath := filepath.Join(dir, "big.log") if err := os.WriteFile(bigPath, make([]byte, 2*1024*1024), 0o644); err != nil { t.Fatal(err) } if err := srv.store.PatchGlobalSettings(tenant, map[string]any{ runtimelogs.KeyMaxFileMB: 1, runtimelogs.KeyAutoMode: "truncate", }); err != nil { t.Fatal(err) } estReq := httptest.NewRequest(http.MethodGet, "/v1/runtime-logs/auto-estimate", nil) estReq.Header.Set("Authorization", "Bearer opkey") estRec := httptest.NewRecorder() handler.ServeHTTP(estRec, estReq) if estRec.Code != http.StatusOK { t.Fatalf("estimate status=%d body=%s", estRec.Code, estRec.Body.String()) } if !strings.Contains(estRec.Body.String(), `"would_count":1`) { t.Fatalf("expected would_count=1, body=%s", estRec.Body.String()) } runReq := httptest.NewRequest(http.MethodPost, "/v1/runtime-logs/auto-run", nil) runReq.Header.Set("Authorization", "Bearer opkey") runRec := httptest.NewRecorder() handler.ServeHTTP(runRec, runReq) if runRec.Code != http.StatusOK { t.Fatalf("auto-run status=%d body=%s", runRec.Code, runRec.Body.String()) } if !strings.Contains(runRec.Body.String(), "auto:scheduler") && !strings.Contains(runRec.Body.String(), `"cleaned_count":1`) { t.Fatalf("unexpected auto-run body=%s", runRec.Body.String()) } st, err := os.Stat(bigPath) if err != nil { t.Fatal(err) } if st.Size() != 0 { t.Fatalf("expected truncated big.log, size=%d", st.Size()) } }) }