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/budgets", h.Create) mux.HandleFunc("PUT /api/v1/budgets/{id}", h.Update) mux.HandleFunc("DELETE /api/v1/budgets/{id}", 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 first 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() // Update amount updated := validBudget() updated["amount"] = 9999.99 req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/budgets/%d", srv.URL, created.ID), mustJSON(t, updated)) 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 && resp2.StatusCode != http.StatusNoContent { t.Errorf("update: got %d, want 200 or 204", resp2.StatusCode) } if resp2.StatusCode == http.StatusOK { var got model.Budget json.NewDecoder(resp2.Body).Decode(&got) if got.Amount != 9999.99 { t.Errorf("updated amount: got %v, want 9999.99", got.Amount) } } } 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) } }