315 lines
8.5 KiB
Go
315 lines
8.5 KiB
Go
package test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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, int, int) {
|
|
t.Helper()
|
|
db := testutil.NewTestDB(t)
|
|
deptID, glID := testutil.SeedFixtures(t, db)
|
|
|
|
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, deptID, glID
|
|
}
|
|
|
|
func newBudgetHandler(t *testing.T) (*handler.BudgetHandler, int, int) {
|
|
t.Helper()
|
|
db := testutil.NewTestDB(t)
|
|
deptID, glID := testutil.SeedFixtures(t, db)
|
|
return handler.NewBudgetHandler(service.NewBudgetService(database.NewBudgetRepo(db))), deptID, glID
|
|
}
|
|
|
|
func validBudget(deptID, glID int) map[string]any {
|
|
return map[string]any{
|
|
"fiscal_year": 2024,
|
|
"fiscal_period": 1,
|
|
"version": "original",
|
|
"department_id": deptID,
|
|
"gl_account_id": glID,
|
|
"amount": 5000.00,
|
|
"currency": "DKK",
|
|
"notes": "",
|
|
"created_by": "test",
|
|
}
|
|
}
|
|
|
|
// ── Create ────────────────────────────────────────────────────────────────────
|
|
|
|
func TestCreateBudget_OK(t *testing.T) {
|
|
h, deptID, glID := newBudgetHandler(t)
|
|
w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", validBudget(deptID, glID))
|
|
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, deptID, glID := newBudgetHandler(t)
|
|
body := validBudget(deptID, glID)
|
|
delete(body, "fiscal_period")
|
|
w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body)
|
|
testutil.AssertStatus(t, w, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestCreateBudget_NegativeAmount(t *testing.T) {
|
|
h, deptID, glID := newBudgetHandler(t)
|
|
body := validBudget(deptID, glID)
|
|
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, deptID, glID := newBudgetServer(t)
|
|
client := srv.Client()
|
|
|
|
resp, err := client.Post(
|
|
srv.URL+"/api/v1/budget/create",
|
|
"application/json",
|
|
mustJSON(t, validBudget(deptID, glID)),
|
|
)
|
|
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_BadRequest(t *testing.T) {
|
|
srv, _, _ := newBudgetServer(t)
|
|
|
|
// bad request...
|
|
wantNotes := "updated in test"
|
|
updateReq := model.UpdateBudgetRequest{
|
|
Notes: &wantNotes,
|
|
ChangedBy: "idk",
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
http.MethodPut,
|
|
srv.URL+"/api/v1/budget/update",
|
|
mustJSON(t, updateReq),
|
|
)
|
|
resp, err := srv.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusNoContent {
|
|
t.Errorf("update non-existent: got %d, want 400 or 204", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// ── Delete ────────────────────────────────────────────────────────────────────
|
|
|
|
func TestDeleteBudget_OK(t *testing.T) {
|
|
srv, deptID, glID := newBudgetServer(t)
|
|
client := srv.Client()
|
|
|
|
resp, err := client.Post(
|
|
srv.URL+"/api/v1/budget/create",
|
|
"application/json",
|
|
mustJSON(t, validBudget(deptID, glID)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created model.Budget
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
if created.ID == 0 {
|
|
t.Fatal("expected non-zero ID from create")
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
http.MethodDelete,
|
|
srv.URL+"/api/v1/budget/delete",
|
|
mustJSON(t, model.DeleteBudgetRequest{ID: created.ID}),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
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/budget/delete",
|
|
mustJSON(t, model.DeleteBudgetRequest{ID: 9999}),
|
|
)
|
|
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("delete non-existent: got %d, want 404 or 204", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestDeleteBudget_DoubleDelete(t *testing.T) {
|
|
srv, deptID, glID := newBudgetServer(t)
|
|
client := srv.Client()
|
|
|
|
resp, err := client.Post(
|
|
srv.URL+"/api/v1/budget/create",
|
|
"application/json",
|
|
mustJSON(t, validBudget(deptID, glID)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created model.Budget
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
if created.ID == 0 {
|
|
t.Fatal("expected non-zero ID from create")
|
|
}
|
|
|
|
deleteBody := model.DeleteBudgetRequest{ID: created.ID}
|
|
|
|
req1, _ := http.NewRequest(
|
|
http.MethodDelete,
|
|
srv.URL+"/api/v1/budget/delete",
|
|
mustJSON(t, deleteBody),
|
|
)
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
resp1, _ := client.Do(req1)
|
|
resp1.Body.Close()
|
|
|
|
// Second delete — should not panic, should return 404 or 204
|
|
req2, err := http.NewRequest(
|
|
http.MethodDelete,
|
|
srv.URL+"/api/v1/budget/delete",
|
|
mustJSON(t, deleteBody),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
|
|
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)
|
|
}
|
|
}
|