package test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" "Engine/internal/database" "Engine/internal/handler" "Engine/internal/model" "Engine/internal/service" "Engine/tests/internal/testutil" ) // ── wire helpers ────────────────────────────────────────────────────────────── func newBudgetServer(t *testing.T) *httptest.Server { t.Helper() db := testutil.NewTestDB(t) repo := database.NewBudgetRepo(db) h := handler.NewBudgetHandler(service.NewBudgetService(repo)) mux := http.NewServeMux() mux.HandleFunc("POST /api/v1/budget/create", h.Create) mux.HandleFunc("PUT /api/v1/budget/update", h.Update) mux.HandleFunc("DELETE /api/v1/budget/delete", h.Delete) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } func newBudgetHandler(t *testing.T) *handler.BudgetHandler { t.Helper() return handler.NewBudgetHandler(service.NewBudgetService(database.NewBudgetRepo(testutil.NewTestDB(t)))) } func validBudget() map[string]any { return map[string]any{ "fiscal_year": 2024, "fiscal_period": 1, "version": "original", // adjust to match your BudgetVersion values "department_id": 1, "gl_account_id": 1, "amount": 5000.00, "currency": "USD", "notes": "", "created_by": "test", } } // ── Create ──────────────────────────────────────────────────────────────────── func TestCreateBudget_OK(t *testing.T) { h := newBudgetHandler(t) w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", validBudget()) testutil.AssertStatus(t, w, http.StatusCreated) var got model.Budget testutil.DecodeJSON(t, w, &got) if got.ID == 0 { t.Error("expected non-zero ID") } if got.Amount != 5000.00 { t.Errorf("amount: got %v, want 5000.00", got.Amount) } } func TestCreateBudget_InvalidJSON(t *testing.T) { h := newBudgetHandler(t) w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", nil) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateBudget_MissingPeriod(t *testing.T) { h := newBudgetHandler(t) body := validBudget() delete(body, "fiscal_period") w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateBudget_ZeroAmount(t *testing.T) { h := newBudgetHandler(t) body := validBudget() body["amount"] = 0 w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) // Whether 0 is rejected or accepted depends on your business rule — adjust to match t.Logf("zero amount response: %d — verify against your handler", w.Code) } func TestCreateBudget_NegativeAmount(t *testing.T) { h := newBudgetHandler(t) body := validBudget() body["amount"] = -100 w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body) testutil.AssertStatus(t, w, http.StatusBadRequest) } // ── Update ──────────────────────────────────────────────────────────────────── func TestUpdateBudget_OK(t *testing.T) { srv := newBudgetServer(t) client := srv.Client() // Create a budget to update resp, err := client.Post( srv.URL+"/api/v1/budget/create", "application/json", mustJSON(t, validBudget()), ) if err != nil { t.Fatal(err) } defer resp.Body.Close() var created model.Budget if err := json.NewDecoder(resp.Body).Decode(&created); err != nil { t.Fatalf("decode created budget: %v", err) } if created.ID == 0 { t.Fatal("expected a non-zero ID from create") } // Update only the fields the endpoint is meant to change wantAmount := 9999.99 wantNotes := "updated in test" updateReq := model.UpdateBudgetRequest{ ID: created.ID, Amount: &wantAmount, Notes: &wantNotes, ChangedBy: created.CreatedBy, } req, err := http.NewRequest( http.MethodPut, srv.URL+"/api/v1/budget/update", mustJSON(t, updateReq), ) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") resp2, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusOK { t.Fatalf("update: got %d, want 200", resp2.StatusCode) } var got model.Budget if err := json.NewDecoder(resp2.Body).Decode(&got); err != nil { t.Fatalf("decode updated budget: %v", err) } if got.Amount != wantAmount { t.Errorf("amount: got %v, want %v", got.Amount, wantAmount) } if got.Notes != wantNotes { t.Errorf("notes: got %q, want %q", got.Notes, wantNotes) } if got.ID != created.ID { t.Errorf("ID changed after update: got %v, want %v", got.ID, created.ID) } // Fields not included in the update should be unchanged if got.FiscalYear != created.FiscalYear { t.Errorf("fiscal_year changed unexpectedly: got %v, want %v", got.FiscalYear, created.FiscalYear) } if got.DepartmentID != created.DepartmentID { t.Errorf("department_id changed unexpectedly: got %v, want %v", got.DepartmentID, created.DepartmentID) } } func TestUpdateBudget_NotFound(t *testing.T) { srv := newBudgetServer(t) req, _ := http.NewRequest(http.MethodPut, srv.URL+"/api/v1/budgets/9999", mustJSON(t, validBudget())) req.Header.Set("Content-Type", "application/json") resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { t.Errorf("update non-existent: got %d, want 404 or 204", resp.StatusCode) } } func TestUpdateBudget_InvalidJSON(t *testing.T) { srv := newBudgetServer(t) // Create one first so the ID exists resp, _ := srv.Client().Post(srv.URL+"/api/v1/budgets", "application/json", mustJSON(t, validBudget())) var created model.Budget json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/budgets/%d", srv.URL, created.ID), bytes.NewBufferString("not-json")) req.Header.Set("Content-Type", "application/json") resp2, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } resp2.Body.Close() if resp2.StatusCode != http.StatusBadRequest { t.Errorf("invalid JSON update: got %d, want 400", resp2.StatusCode) } } // ── Delete ──────────────────────────────────────────────────────────────────── func TestDeleteBudget_OK(t *testing.T) { srv := newBudgetServer(t) client := srv.Client() resp, err := client.Post(srv.URL+"/api/v1/budgets", "application/json", mustJSON(t, validBudget())) if err != nil { t.Fatal(err) } var created model.Budget json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/budgets/"+strconv.Itoa(created.ID), nil) resp2, err := client.Do(req) if err != nil { t.Fatal(err) } resp2.Body.Close() if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK { t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode) } } func TestDeleteBudget_NotFound(t *testing.T) { srv := newBudgetServer(t) req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/budgets/9999", nil) resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) } } func TestDeleteBudget_DoubleDelete(t *testing.T) { srv := newBudgetServer(t) client := srv.Client() resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", mustJSON(t, validBudget())) var created model.Budget json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() url := srv.URL + "/api/v1/budgets/" + strconv.Itoa(created.ID) req1, _ := http.NewRequest(http.MethodDelete, url, nil) resp1, _ := client.Do(req1) resp1.Body.Close() // Second delete — should not panic, should return 404 or 204 req2, _ := http.NewRequest(http.MethodDelete, url, nil) resp2, err := client.Do(req2) if err != nil { t.Fatal(err) } resp2.Body.Close() if resp2.StatusCode != http.StatusNotFound && resp2.StatusCode != http.StatusNoContent { t.Errorf("double delete: got %d, want 404 or 204", resp2.StatusCode) } }