363 lines
12 KiB
Go
363 lines
12 KiB
Go
package test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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()
|
|
db := testutil.NewTestDB(t)
|
|
h := handler.NewReferenceHandler(database.NewReferenceRepo(db))
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /api/v1/department/create", h.CreateDepartment)
|
|
mux.HandleFunc("GET /api/v1/department/list", h.ListDepartments)
|
|
mux.HandleFunc("DELETE /api/v1/department/delete", h.DeleteDepartment)
|
|
mux.HandleFunc("POST /api/v1/gl-account/create", h.CreateGLAccount)
|
|
mux.HandleFunc("GET /api/v1/gl-account/list", h.ListGLAccounts)
|
|
mux.HandleFunc("DELETE /api/v1/gl-account/delete", 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 model.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 model.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 model.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 []model.Department
|
|
testutil.DecodeJSON(t, w, &got)
|
|
if len(got) != 5 {
|
|
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 []model.Department
|
|
testutil.DecodeJSON(t, w, &got)
|
|
if len(got) != 8 {
|
|
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/department/create",
|
|
"application/json",
|
|
testutil.MustJSON(t, map[string]any{"code": "ENG", "name": "Engineering", "active": true}),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created model.Department
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
|
|
if created.ID == 0 {
|
|
t.Fatal("expected non-zero ID from create")
|
|
}
|
|
|
|
reqdelete := model.DeleteDepartmentRequest{
|
|
ID: created.ID,
|
|
}
|
|
req, _ := http.NewRequest(
|
|
http.MethodDelete,
|
|
fmt.Sprintf("%s/api/v1/department/delete", srv.URL),
|
|
mustJSON(t, reqdelete),
|
|
)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// ── 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", "description": "Travel Expenses", "type": "opex"})
|
|
|
|
testutil.AssertStatus(t, w, http.StatusCreated)
|
|
|
|
var got model.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{"description": "Travel Expenses", "type": "opex"})
|
|
testutil.AssertStatus(t, w, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestCreateGLAccount_MissingDescription(t *testing.T) {
|
|
h := newReferenceHandler(t)
|
|
w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/",
|
|
map[string]any{"code": "5001", "type": "opex"})
|
|
testutil.AssertStatus(t, w, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestCreateGLAccount_InvalidType(t *testing.T) {
|
|
h := newReferenceHandler(t)
|
|
w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/",
|
|
map[string]any{"code": "5001", "description": "Travel Expenses", "type": "expense"})
|
|
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", "description": "Revenue", "type": "revenue"})
|
|
|
|
w := testutil.Do(t, fn, http.MethodPost, "/",
|
|
map[string]any{"code": "4001", "description": "Revenue Updated", "type": "revenue"})
|
|
testutil.AssertStatus(t, w, http.StatusCreated)
|
|
|
|
var got model.GLAccount
|
|
testutil.DecodeJSON(t, w, &got)
|
|
if got.Description != "Revenue Updated" {
|
|
t.Errorf("upsert description: got %q, want %q", got.Description, "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 []model.GLAccount
|
|
testutil.DecodeJSON(t, w, &got)
|
|
// Migrate seeds 12 GL accounts — "empty" means no user-added accounts
|
|
if len(got) != 12 {
|
|
t.Errorf("expected 12 seeded GL accounts, 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", "description": "Revenue", "type": "revenue"})
|
|
testutil.Do(t, fn, http.MethodPost, "/",
|
|
map[string]any{"code": "5001", "description": "COGS", "type": "cogs"})
|
|
|
|
w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil)
|
|
testutil.AssertStatus(t, w, http.StatusOK)
|
|
|
|
var got []model.GLAccount
|
|
testutil.DecodeJSON(t, w, &got)
|
|
// 12 seeded + 2 created
|
|
if len(got) != 14 {
|
|
t.Errorf("expected 14 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-account/create",
|
|
"application/json",
|
|
mustJSON(t, map[string]any{"code": "9001", "description": "Rent", "type": "opex"}),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created model.GLAccount
|
|
json.NewDecoder(resp.Body).Decode(&created)
|
|
resp.Body.Close()
|
|
if created.ID == 0 {
|
|
t.Fatal("expected non-zero ID from create")
|
|
}
|
|
|
|
req, _ := http.NewRequest(
|
|
http.MethodDelete,
|
|
srv.URL+"/api/v1/gl-account/delete",
|
|
mustJSON(t, model.DeleteGLAccountRequest{ID: created.ID}),
|
|
)
|
|
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 TestDeleteGLAccount_NotFound(t *testing.T) {
|
|
srv := newReferenceServer(t)
|
|
|
|
req, _ := http.NewRequest(
|
|
http.MethodDelete,
|
|
srv.URL+"/api/v1/gl-account/delete",
|
|
mustJSON(t, model.DeleteGLAccountRequest{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)
|
|
}
|
|
}
|
|
|
|
// ── 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)
|
|
}
|