package database import ( "bytes" "context" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/kandev/kandev/internal/system/jobs" ) // httpGet builds a GET request and is shared by stats/maintenance/reset tests. func httpGet(t *testing.T, path string) *http.Request { t.Helper() req, err := http.NewRequest(http.MethodGet, path, nil) if err != nil { t.Fatalf("http.NewRequest: %v", err) } return req } // httpPost builds a POST request with an optional JSON body. func httpPost(t *testing.T, path string, body string) *http.Request { t.Helper() var reader *bytes.Reader if body == "" { reader = bytes.NewReader(nil) } else { reader = bytes.NewReader([]byte(body)) } req, err := http.NewRequest(http.MethodPost, path, reader) if err != nil { t.Fatalf("http.NewRequest: %v", err) } if body != "" { req.Header.Set("Content-Type", "application/json") } return req } func serveHTTP(r *gin.Engine, req *http.Request) *httptest.ResponseRecorder { w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func contains(haystack, needle string) bool { return strings.Contains(haystack, needle) } func TestVacuum_StartsJobAndShrinksDB(t *testing.T) { svc, tracker, _, _ := newTestService(t) before, err := svc.Stats() if err != nil { t.Fatalf("stats before: %v", err) } id := svc.Vacuum(context.Background()) if id == "" { t.Fatal("Vacuum returned empty job id") } job := waitForState(t, tracker, id, jobs.StateSucceeded) if job.Result["reclaimed_bytes"] == nil { t.Errorf("expected reclaimed_bytes in result, got %+v", job.Result) } after, err := svc.Stats() if err != nil { t.Fatalf("stats after: %v", err) } // DB file must still exist (Stats reads from it). if after.Path != before.Path { t.Errorf("path changed: %s -> %s", before.Path, after.Path) } if after.SizeBytes <= 0 { t.Errorf("size after vacuum = %d, want > 0", after.SizeBytes) } // schema_version preserved across vacuum. if after.SchemaVersion != "v0.99.0" { t.Errorf("schema_version lost after vacuum: %q", after.SchemaVersion) } } func TestOptimize_StartsJobAndSucceeds(t *testing.T) { svc, tracker, _, _ := newTestService(t) id := svc.Optimize(context.Background()) if id == "" { t.Fatal("Optimize returned empty job id") } job := waitForState(t, tracker, id, jobs.StateSucceeded) if job.State != jobs.StateSucceeded { t.Errorf("state = %s, want succeeded", job.State) } // Verify a follow-up query against the DB still works (sanity). stats, err := svc.Stats() if err != nil { t.Fatalf("post-optimize stats: %v", err) } if stats.SchemaVersion != "v0.99.0" { t.Errorf("schema_version lost after optimize: %q", stats.SchemaVersion) } } func TestHandleVacuum_Returns202WithJobID(t *testing.T) { svc, tracker, _, _ := newTestService(t) gin.SetMode(gin.TestMode) r := gin.New() r.POST("/vacuum", HandleVacuum(svc)) w := serveHTTP(r, httpPost(t, "/vacuum", "")) if w.Code != http.StatusAccepted { t.Fatalf("status = %d body=%s", w.Code, w.Body.String()) } if !contains(w.Body.String(), `"job_id"`) { t.Errorf("body missing job_id: %s", w.Body.String()) } // Make sure the spawned job finishes before the test exits so the temp dir // cleanup does not race with VACUUM still writing the SQLite journal. for _, j := range tracker.List() { waitForState(t, tracker, j.ID, jobs.StateSucceeded) } } func TestHandleOptimize_Returns202WithJobID(t *testing.T) { svc, tracker, _, _ := newTestService(t) gin.SetMode(gin.TestMode) r := gin.New() r.POST("/optimize", HandleOptimize(svc)) w := serveHTTP(r, httpPost(t, "/optimize", "")) if w.Code != http.StatusAccepted { t.Fatalf("status = %d body=%s", w.Code, w.Body.String()) } if !contains(w.Body.String(), `"job_id"`) { t.Errorf("body missing job_id: %s", w.Body.String()) } // Same cleanup-race guard as TestHandleVacuum_Returns202WithJobID. for _, j := range tracker.List() { waitForState(t, tracker, j.ID, jobs.StateSucceeded) } }