package handlers import ( "bytes" "database/sql" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "pdf-form-api/db" "pdf-form-api/models" ) const samplePDF = "../samples/OoPdfFormExample.pdf" func setupTestServer(t *testing.T) (*Server, *sql.DB, func()) { t.Helper() dir := t.TempDir() dbPath := filepath.Join(dir, "test.db") database, err := db.InitDB(dbPath, dir) if err != nil { t.Fatalf("InitDB: %v", err) } srv := NewServer(database, dir) cleanup := func() { database.Close() } return srv, database, cleanup } func TestUploadPDFAcceptsValidPDF(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) resp := w.Result() if resp.StatusCode != http.StatusCreated { t.Fatalf("expected status 201, got %d: %s", resp.StatusCode, w.Body.String()) } var result models.UploadResponse if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON response: %v: %s", err, w.Body.String()) } if result.PDF.ID == 0 { t.Error("expected non-zero PDF ID") } if len(result.Fields) == 0 { t.Error("expected form fields in response") } } func TestUploadPDFReturnsFieldsWithCorrectTypes(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var result models.UploadResponse if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON: %v", err) } typeMap := make(map[string]string) for _, f := range result.Fields { typeMap[f.Name] = string(f.Type) } if typeMap["Given Name Text Box"] != "text" { t.Errorf("Given Name Text Box should be text, got %s", typeMap["Given Name Text Box"]) } if typeMap["Driving License Check Box"] != "checkbox" { t.Errorf("Driving License Check Box should be checkbox, got %s", typeMap["Driving License Check Box"]) } } func TestUploadPDFReturnsChoicesForComboFields(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var result models.UploadResponse if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON: %v", err) } for _, f := range result.Fields { if f.Name == "Country Combo Box" { if len(f.Choices) == 0 { t.Error("Country Combo Box should have choices") } } if f.Name == "Gender List Box" { if len(f.Choices) == 0 { t.Error("Gender List Box should have choices") } } } } func TestUploadPDFReturnsCorrectFieldCount(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var result models.UploadResponse if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(result.Fields) != 17 { t.Errorf("expected 17 fields, got %d", len(result.Fields)) } } func TestUploadPDFRejectsNoFile(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodPost, "/pdf/upload", nil) w := httptest.NewRecorder() srv.UploadPDF(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestUploadPDFRejectsNonPDF(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) part, err := writer.CreateFormFile("file", "document.txt") if err != nil { t.Fatalf("CreateFormFile: %v", err) } part.Write([]byte("not a pdf")) writer.Close() req := httptest.NewRequest(http.MethodPost, "/pdf/upload", buf) req.Header.Set("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() srv.UploadPDF(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestUploadPDFRejectsWrongMethod(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/upload", nil) w := httptest.NewRecorder() srv.UploadPDF(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", w.Code) } } func TestUploadPDFStoresFile(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) entries, err := os.ReadDir(srv.storeDir) if err != nil { t.Fatalf("ReadDir: %v", err) } if len(entries) == 0 { t.Error("expected file to be stored in storeDir") } } func TestUploadPDFAcceptsMultipleUploads(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() for i := 0; i < 3; i++ { reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) if w.Code != http.StatusCreated { t.Errorf("upload %d: expected 201, got %d", i+1, w.Code) } } entries, _ := os.ReadDir(srv.storeDir) // Count only PDF files (not the .db file) pdfCount := 0 for _, e := range entries { if filepath.Ext(e.Name()) == ".pdf" { pdfCount++ } } if pdfCount != 3 { t.Errorf("expected 3 PDF files in storeDir, got %d", pdfCount) } } func TestUploadPDFReturnsCreatedAt(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var result models.UploadResponse json.Unmarshal(w.Body.Bytes(), &result) if result.PDF.CreatedAt.IsZero() { t.Error("expected CreatedAt to be set") } } // --- GET /pdf/{id} --- func TestGetPDFReturnsFields(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() // Upload first reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) id := upload.PDF.ID // Get req = httptest.NewRequest(http.MethodGet, "/pdf/1", nil) w = httptest.NewRecorder() srv.GetPDF(w, req, id) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp GetPDFResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(resp.Fields) != 17 { t.Errorf("expected 17 fields, got %d", len(resp.Fields)) } } func TestGetPDFReturnsFieldDetails(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) req = httptest.NewRequest(http.MethodGet, "/pdf/1", nil) w = httptest.NewRecorder() srv.GetPDF(w, req, upload.PDF.ID) var resp GetPDFResponse json.Unmarshal(w.Body.Bytes(), &resp) for _, f := range resp.Fields { if f.Name == "Height Formatted Field" { if f.Value != "150" { t.Errorf("Height should have value '150', got %q", f.Value) } if f.DefaultVal != "150" { t.Errorf("Height should have default '150', got %q", f.DefaultVal) } } if f.Name == "Country Combo Box" { if len(f.Choices) == 0 { t.Error("Country should have choices") } } if f.Name == "Given Name Text Box" { if f.Title == "" { // Title may or may not be present depending on PDF } } // New fields should be present (empty initially before AI processing) if f.ValueGroup != "" { t.Logf("Field %s has value_group: %s", f.Name, f.ValueGroup) } if f.WizardPage != nil { t.Logf("Field %s has wizard_page: %d", f.Name, *f.WizardPage) } } } func TestGetPDFReturnsNewGroupingFields(t *testing.T) { srv, dbConn, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) // Manually set grouping data on first two fields if len(upload.Fields) >= 2 { db.UpdateFormFieldValueGroup(dbConn, upload.Fields[0].ID, "Name Group") db.UpdateFormFieldValueGroup(dbConn, upload.Fields[1].ID, "Name Group") db.UpdateFormFieldWizardPage(dbConn, upload.Fields[0].ID, 1) db.UpdateFormFieldWizardPage(dbConn, upload.Fields[1].ID, 1) db.UpdateFormFieldWizardPage(dbConn, upload.Fields[2].ID, 2) } req = httptest.NewRequest(http.MethodGet, "/pdf/1", nil) w = httptest.NewRecorder() srv.GetPDF(w, req, upload.PDF.ID) var resp GetPDFResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(resp.Fields) < 3 { t.Fatal("not enough fields to test grouping") } // Verify value_group if resp.Fields[0].ValueGroup != "Name Group" { t.Errorf("expected value_group 'Name Group', got %q", resp.Fields[0].ValueGroup) } if resp.Fields[1].ValueGroup != "Name Group" { t.Errorf("expected value_group 'Name Group' for field 2, got %q", resp.Fields[1].ValueGroup) } if resp.Fields[2].ValueGroup != "" { t.Errorf("expected empty value_group for field 3, got %q", resp.Fields[2].ValueGroup) } // Verify wizard_page if resp.Fields[0].WizardPage == nil || *resp.Fields[0].WizardPage != 1 { t.Errorf("expected wizard_page 1, got %v", resp.Fields[0].WizardPage) } if resp.Fields[1].WizardPage == nil || *resp.Fields[1].WizardPage != 1 { t.Errorf("expected wizard_page 1 for field 2, got %v", resp.Fields[1].WizardPage) } if resp.Fields[2].WizardPage == nil || *resp.Fields[2].WizardPage != 2 { t.Errorf("expected wizard_page 2 for field 3, got %v", resp.Fields[2].WizardPage) } } func TestGetPDFNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/999", nil) w := httptest.NewRecorder() srv.GetPDF(w, req, 999) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } var errResp map[string]string json.Unmarshal(w.Body.Bytes(), &errResp) if errResp["error"] != "pdf not found" { t.Errorf("expected 'pdf not found' error, got %v", errResp) } } func TestGetPDFRejectsWrongMethod(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodPost, "/pdf/1", nil) w := httptest.NewRecorder() srv.GetPDF(w, req, 1) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", w.Code) } } // --- POST /pdf/{id}/fill --- func TestFillPDFReturnsFilledDocument(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) fillReq := models.FillRequest{ Fields: map[string]string{ "Given Name Text Box": "John", "Family Name Text Box": "Doe", }, } body, _ := json.Marshal(fillReq) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.FillPDF(w, req, upload.PDF.ID) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } contentType := w.Header().Get("Content-Type") if contentType != "application/pdf" { t.Errorf("expected Content-Type application/pdf, got %s", contentType) } data := w.Body.Bytes() if len(data) == 0 { t.Error("expected non-empty PDF response") } if string(data[:5]) != "%PDF-" { t.Error("response should be a valid PDF") } } func TestFillPDFAllFieldTypes(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) fillReq := models.FillRequest{ Fields: map[string]string{ "Given Name Text Box": "John", "Family Name Text Box": "Doe", "House nr Text Box": "42", "Address 1 Text Box": "Main Street", "City Text Box": "Berlin", "Postcode Text Box": "10115", "Country Combo Box": "Germany", "Height Formatted Field": "180", "Driving License Check Box": "true", "Gender List Box": "Woman", }, } body, _ := json.Marshal(fillReq) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.FillPDF(w, req, upload.PDF.ID) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } data := w.Body.Bytes() if string(data[:5]) != "%PDF-" { t.Error("response should be a valid PDF") } } func TestFillPDFNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() fillReq := models.FillRequest{ Fields: map[string]string{"test": "value"}, } body, _ := json.Marshal(fillReq) req := httptest.NewRequest(http.MethodPost, "/pdf/999/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.FillPDF(w, req, 999) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestFillPDFInvalidJSON(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", strings.NewReader("invalid json")) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.FillPDF(w, req, upload.PDF.ID) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestFillPDFRejectsWrongMethod(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/1/fill", nil) w := httptest.NewRecorder() srv.FillPDF(w, req, 1) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", w.Code) } } func TestFillPDFSetsContentDisposition(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) fillReq := models.FillRequest{ Fields: map[string]string{"Given Name Text Box": "John"}, } body, _ := json.Marshal(fillReq) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.FillPDF(w, req, upload.PDF.ID) disposition := w.Header().Get("Content-Disposition") if !strings.Contains(disposition, "filled-") { t.Errorf("expected Content-Disposition with filled-, got %s", disposition) } } func TestFillPDFSetsContentLength(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) fillReq := models.FillRequest{ Fields: map[string]string{"Given Name Text Box": "John"}, } body, _ := json.Marshal(fillReq) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.FillPDF(w, req, upload.PDF.ID) contentLength := w.Header().Get("Content-Length") if contentLength == "" { t.Error("expected Content-Length header") } } // --- Routes --- func TestRoutesUploadEndpoint(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("expected 201, got %d", w.Code) } } func TestRoutesGetEndpoint(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() // Upload reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) // Get req = httptest.NewRequest(http.MethodGet, "/pdf/1", nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } } func TestRoutesFillEndpoint(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() // Upload reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) // Fill fillReq := models.FillRequest{ Fields: map[string]string{"Given Name Text Box": "John"}, } body, _ := json.Marshal(fillReq) req = httptest.NewRequest(http.MethodPost, "/pdf/1/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } } func TestRoutesNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestRoutesGetNonExistent(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/999", nil) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestRoutesFillNonExistent(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() fillReq := models.FillRequest{ Fields: map[string]string{"test": "value"}, } body, _ := json.Marshal(fillReq) req := httptest.NewRequest(http.MethodPost, "/pdf/999/fill", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } // --- GET /pdf/{id}/page/{pageNum} --- func TestGetPageImageReturnsImage(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) imgDir := filepath.Join(srv.storeDir, fmt.Sprintf("%d", upload.PDF.ID), "pages", "1") if err := os.MkdirAll(imgDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } pngData := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01") if err := os.WriteFile(filepath.Join(imgDir, "page_1.png"), pngData, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pdf/%d/page/1", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } ctype := w.Header().Get("Content-Type") if ctype != "image/png" { t.Errorf("expected image/png, got %s", ctype) } } func TestGetPageImageAnnotated(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) imgDir := filepath.Join(srv.storeDir, fmt.Sprintf("%d", upload.PDF.ID), "pages", "1") if err := os.MkdirAll(imgDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } pngData := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01") if err := os.WriteFile(filepath.Join(imgDir, "page_1_annotated.png"), pngData, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pdf/%d/page/1/annotated", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } ctype := w.Header().Get("Content-Type") if ctype != "image/png" { t.Errorf("expected image/png, got %s", ctype) } } func TestGetPageImageNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pdf/%d/page/1", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestGetPageImagePdfNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/999/page/1", nil) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestGetPageImageRejectsWrongMethod(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) imgDir := filepath.Join(srv.storeDir, fmt.Sprintf("%d", upload.PDF.ID), "pages", "1") if err := os.MkdirAll(imgDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } pngData := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01") if err := os.WriteFile(filepath.Join(imgDir, "page_1.png"), pngData, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/pdf/%d/page/1", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.GetPageImage(w, req, upload.PDF.ID, 1, false) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", w.Code) } } // --- GET /pdf/{id}/wizard --- func TestWizardRouteExists(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pdf/%d/wizard", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } body := w.Body.String() if !strings.Contains(body, "") { t.Error("expected HTML response") } if !strings.Contains(body, fmt.Sprintf("%d", upload.PDF.ID)) { t.Errorf("expected PDF ID %d in page, got snippet: %s", upload.PDF.ID, body[1500:1700]) } } func TestWizardRouteNotFound(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/pdf/999/wizard", nil) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestWizardRouteRejectsWrongMethod(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() reader := multipartReader(t, samplePDF) req := httptest.NewRequest(http.MethodPost, "/pdf/upload", reader.buf) req.Header.Set("Content-Type", reader.content) w := httptest.NewRecorder() srv.UploadPDF(w, req) var upload models.UploadResponse json.Unmarshal(w.Body.Bytes(), &upload) req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/pdf/%d/wizard", upload.PDF.ID), nil) w = httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", w.Code) } } // --- sanitizeFilename --- func TestSanitizeFilename(t *testing.T) { tests := []struct { input string expected string }{ {"valid-file.pdf", "valid-file.pdf"}, {"file with spaces.pdf", "file_with_spaces.pdf"}, {"file/with/slashes.pdf", "file_with_slashes.pdf"}, {"file\\with\\backslashes.pdf", "file_with_backslashes.pdf"}, {"file