package test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "Engine/internal/database" "Engine/internal/handler" "Engine/internal/model" "Engine/tests/internal/testutil" ) // ── wire helpers ────────────────────────────────────────────────────────────── func newReferenceHandler(t *testing.T) *handler.ReferenceHandler { t.Helper() return handler.NewReferenceHandler(database.NewReferenceRepo(testutil.NewTestDB(t))) } // newReferenceServer spins up a real httptest.Server with the production // mux routes. Use this for delete/path-param tests that need {id} routing. func newReferenceServer(t *testing.T) *httptest.Server { t.Helper() h := newReferenceHandler(t) mux := http.NewServeMux() mux.HandleFunc("POST /api/v1/departments", h.CreateDepartment) mux.HandleFunc("GET /api/v1/departments", h.ListDepartments) mux.HandleFunc("DELETE /api/v1/departments/{id}", h.DeleteDepartment) mux.HandleFunc("POST /api/v1/gl-accounts", h.CreateGLAccount) mux.HandleFunc("GET /api/v1/gl-accounts", h.ListGLAccounts) mux.HandleFunc("DELETE /api/v1/gl-accounts/{id}", h.DeleteGLAccount) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } // ── Department: Create ──────────────────────────────────────────────────────── func TestCreateDepartment_OK(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", map[string]any{"code": "ENG", "name": "Engineering", "active": true}) testutil.AssertStatus(t, w, http.StatusCreated) var got database.Department testutil.DecodeJSON(t, w, &got) if got.Code != "ENG" { t.Errorf("code: got %q, want %q", got.Code, "ENG") } if got.ID == 0 { t.Error("expected non-zero ID in response") } if !got.Active { t.Error("expected active=true") } } func TestCreateDepartment_DefaultsActiveTrue(t *testing.T) { h := newReferenceHandler(t) // active field omitted — handler must default it to true w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", map[string]any{"code": "MKT", "name": "Marketing"}) testutil.AssertStatus(t, w, http.StatusCreated) var got database.Department testutil.DecodeJSON(t, w, &got) if !got.Active { t.Error("expected active to default to true when omitted") } } func TestCreateDepartment_MissingCode(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", map[string]any{"name": "Engineering"}) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateDepartment_MissingName(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", map[string]any{"code": "ENG"}) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateDepartment_WhitespaceOnly(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", map[string]any{"code": " ", "name": " "}) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateDepartment_InvalidJSON(t *testing.T) { h := newReferenceHandler(t) // nil body → empty body → JSON decode fails → 400 w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", nil) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateDepartment_Upsert(t *testing.T) { h := newReferenceHandler(t) fn := http.HandlerFunc(h.CreateDepartment) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance"}) w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance Updated"}) testutil.AssertStatus(t, w, http.StatusCreated) var got database.Department testutil.DecodeJSON(t, w, &got) if got.Name != "Finance Updated" { t.Errorf("upsert name: got %q, want %q", got.Name, "Finance Updated") } } // ── Department: List ────────────────────────────────────────────────────────── func TestListDepartments_Empty(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) var got []database.Department testutil.DecodeJSON(t, w, &got) if len(got) != 0 { t.Errorf("expected empty list, got %d", len(got)) } } func TestListDepartments_ReturnAll(t *testing.T) { h := newReferenceHandler(t) fn := http.HandlerFunc(h.CreateDepartment) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "HR", "name": "Human Resources"}) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "IT", "name": "Information Technology"}) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "OPS", "name": "Operations"}) w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) var got []database.Department testutil.DecodeJSON(t, w, &got) if len(got) != 3 { t.Errorf("expected 3 departments, got %d", len(got)) } } // ── Department: Delete ──────────────────────────────────────────────────────── func TestDeleteDepartment_OK(t *testing.T) { srv := newReferenceServer(t) client := srv.Client() resp, err := client.Post(srv.URL+"/api/v1/departments", "application/json", mustJSON(t, map[string]any{"code": "OPS", "name": "Operations"})) if err != nil { t.Fatal(err) } var created database.Department json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/"+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 TestDeleteDepartment_NotFound(t *testing.T) { srv := newReferenceServer(t) req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/9999", nil) resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() // Accept 404 or 204 — adjust to match your handler's behaviour if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent { t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode) } } // ── GL Account: Create ──────────────────────────────────────────────────────── func TestCreateGLAccount_OK(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", map[string]any{"code": "5001", "name": "Travel Expenses", "type": "expense"}) testutil.AssertStatus(t, w, http.StatusCreated) var got database.GLAccount testutil.DecodeJSON(t, w, &got) if got.Code != "5001" { t.Errorf("code: got %q, want %q", got.Code, "5001") } if got.ID == 0 { t.Error("expected non-zero ID") } } func TestCreateGLAccount_MissingCode(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", map[string]any{"name": "Travel Expenses"}) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateGLAccount_MissingName(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/", map[string]any{"code": "5001"}) testutil.AssertStatus(t, w, http.StatusBadRequest) } func TestCreateGLAccount_Upsert(t *testing.T) { h := newReferenceHandler(t) fn := http.HandlerFunc(h.CreateGLAccount) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue Updated"}) testutil.AssertStatus(t, w, http.StatusCreated) var got model.GLAccount testutil.DecodeJSON(t, w, &got) if got.Name != "Revenue Updated" { t.Errorf("upsert name: got %q, want %q", got.Name, "Revenue Updated") } } // ── GL Account: List ────────────────────────────────────────────────────────── func TestListGLAccounts_Empty(t *testing.T) { h := newReferenceHandler(t) w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) var got []database.GLAccount testutil.DecodeJSON(t, w, &got) if len(got) != 0 { t.Errorf("expected empty list, got %d", len(got)) } } func TestListGLAccounts_ReturnAll(t *testing.T) { h := newReferenceHandler(t) fn := http.HandlerFunc(h.CreateGLAccount) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"}) testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "5001", "name": "COGS"}) w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil) testutil.AssertStatus(t, w, http.StatusOK) var got []database.GLAccount testutil.DecodeJSON(t, w, &got) if len(got) != 2 { t.Errorf("expected 2 GL accounts, got %d", len(got)) } } // ── GL Account: Delete ──────────────────────────────────────────────────────── func TestDeleteGLAccount_OK(t *testing.T) { srv := newReferenceServer(t) client := srv.Client() resp, err := client.Post(srv.URL+"/api/v1/gl-accounts", "application/json", mustJSON(t, map[string]any{"code": "6001", "name": "Rent"})) if err != nil { t.Fatal(err) } var created database.GLAccount json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/"+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 TestDeleteGLAccount_NotFound(t *testing.T) { srv := newReferenceServer(t) req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/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) } } // ── local helpers ───────────────────────────────────────────────────────────── func mustJSON(t *testing.T, v any) *bytes.Reader { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return bytes.NewReader(b) }