Files
FPandA-Engine/tests/refrence_test.go
2026-03-21 08:14:36 +01:00

335 lines
11 KiB
Go

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.Code != "Revenue Updated" {
t.Errorf("upsert name: got %q, want %q", got.Code, "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)
}