296 lines
8.5 KiB
Go
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/budgets/update", h.Update)
|
|
mux.HandleFunc("DELETE /api/v1/budgets/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)
|
|
}
|
|
}
|