feat: migrate to dedicated patch test management

This commit is contained in:
2026-05-29 17:19:02 +01:00
parent 9d8015f1b8
commit f36497090a
8 changed files with 450 additions and 124 deletions
+173
View File
@@ -0,0 +1,173 @@
package admin
import (
"crussell/db"
"crussell/internal/validators"
"crussell/mw"
"database/sql"
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
)
// PatchTest represents a patch test definition
type PatchTest struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
NoticeDurationHours int `json:"notice_duration_hours"`
ExpiryMonths int `json:"expiry_months"`
ServiceIDs []string `json:"service_ids"`
}
// CreatePatchTestRequest represents the request payload
type CreatePatchTestRequest struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
NoticeDurationHours int `json:"notice_duration_hours"`
ExpiryMonths int `json:"expiry_months"`
ServiceIDs []string `json:"service_ids"`
}
// UpdatePatchTestRequest represents the request payload
type UpdatePatchTestRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
NoticeDurationHours *int `json:"notice_duration_hours,omitempty"`
ExpiryMonths *int `json:"expiry_months,omitempty"`
ServiceIDs []string `json:"service_ids,omitempty"`
}
// GetPatchTests handles GET /api/admin/patch-tests
func GetPatchTests(w http.ResponseWriter, r *http.Request) {
query := `
SELECT id, name, description, notice_duration_hours, expiry_months, service_ids
FROM patch_tests
ORDER BY name
`
rows, err := db.DB.Query(r.Context(), query)
if err != nil {
http.Error(w, "Failed to fetch patch tests: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var patchTests []PatchTest
for rows.Next() {
var pt PatchTest
var desc sql.NullString
err := rows.Scan(&pt.ID, &pt.Name, &desc, &pt.NoticeDurationHours, &pt.ExpiryMonths, &pt.ServiceIDs)
if err != nil {
http.Error(w, "Failed to read patch test data: "+err.Error(), http.StatusInternalServerError)
return
}
if desc.Valid {
pt.Description = &desc.String
}
patchTests = append(patchTests, pt)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(patchTests)
}
// CreatePatchTest handles POST /api/admin/patch-tests
func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
var req CreatePatchTestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
query := `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
var id string
err := db.DB.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id)
if err != nil {
http.Error(w, "Failed to create patch test: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"id": id})
}
// UpdatePatchTest handles PUT /api/admin/patch-tests/{id}
func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" || !validators.IsValidID(id) {
http.Error(w, "Patch test not found", http.StatusNotFound)
return
}
var req UpdatePatchTestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
query := "UPDATE patch_tests SET "
args := []interface{}{}
i := 1
if req.Name != nil {
query += "name = $" + string(rune('0'+i)) + ", "
args = append(args, *req.Name)
i++
}
if req.Description != nil {
query += "description = $" + string(rune('0'+i)) + ", "
args = append(args, *req.Description)
i++
}
if req.NoticeDurationHours != nil {
query += "notice_duration_hours = $" + string(rune('0'+i)) + ", "
args = append(args, *req.NoticeDurationHours)
i++
}
if req.ExpiryMonths != nil {
query += "expiry_months = $" + string(rune('0'+i)) + ", "
args = append(args, *req.ExpiryMonths)
i++
}
if req.ServiceIDs != nil {
query += "service_ids = $" + string(rune('0'+i)) + ", "
args = append(args, req.ServiceIDs)
i++
}
query = query[:len(query)-2]
query += " WHERE id = $" + string(rune('0'+i))
args = append(args, id)
_, err := db.DB.Exec(r.Context(), query, args...)
if err != nil {
http.Error(w, "Failed to update patch test: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// DeletePatchTest handles DELETE /api/admin/patch-tests/{id}
func DeletePatchTest(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" || !validators.IsValidID(id) {
http.Error(w, "Patch test not found", http.StatusNotFound)
return
}
_, err := db.DB.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id)
if err != nil {
http.Error(w, "Failed to delete patch test: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
@@ -0,0 +1,71 @@
//go:build test
// +build test
package admin
import (
"bytes"
"context"
"encoding/json"
"net/http"
"testing"
"crussell/db"
"crussell/testutils/fixtures"
)
func TestPatchTests_CRUD(t *testing.T) {
resetTestData(t)
// Create a service to link
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
req := CreatePatchTestRequest{
Name: "Test Patch Test",
Description: strPtr("Description"),
NoticeDurationHours: 24,
ExpiryMonths: 6,
ServiceIDs: []string{serviceID},
}
body, _ := json.Marshal(req)
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", bytes.NewReader(body))
if w.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", w.Code)
}
var created struct {
ID string `json:"id"`
}
parseResponseBody(w, &created)
w = makeAdminRequest(http.HandlerFunc(GetPatchTests), "GET", "/api/admin/patch-tests", nil)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var list []PatchTest
parseResponseBody(w, &list)
if len(list) != 1 {
t.Errorf("expected 1 patch test, got %d", len(list))
}
newName := "Updated Name"
updateReq := UpdatePatchTestRequest{Name: &newName}
updateBody, _ := json.Marshal(updateReq)
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, bytes.NewReader(updateBody))
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", w.Code)
}
w = makeAdminRequest(http.HandlerFunc(DeletePatchTest), "DELETE", "/api/admin/patch-tests/"+created.ID, nil)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", w.Code)
}
}
func strPtr(s string) *string {
return &s
}