Files
FPandA-Engine/tests/budget_test.go
2026-03-21 15:47:40 +01:00

296 lines
8.5 KiB
Go

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)
}
}