removal of python, uses golang testing
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"Engine/internal/model"
|
||||
"Engine/internal/service"
|
||||
@@ -23,6 +24,13 @@ func (h *BudgetHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
errs := req.Valid()
|
||||
if len(errs) > 0 {
|
||||
writeError(w, http.StatusBadRequest, strings.Join(errs, "; "))
|
||||
return
|
||||
}
|
||||
|
||||
budget, err := h.svc.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -42,6 +50,13 @@ func (h *BudgetHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
errs := req.Valid()
|
||||
if len(errs) > 0 {
|
||||
writeError(w, http.StatusBadRequest, strings.Join(errs, "; "))
|
||||
return
|
||||
}
|
||||
|
||||
budget, err := h.svc.Update(r.Context(), id, req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -62,12 +62,79 @@ type CreateBudgetRequest struct {
|
||||
CreatedBy string `json:"created_by"`
|
||||
}
|
||||
|
||||
func (cBudget *CreateBudgetRequest) Valid() []string {
|
||||
var errs []string
|
||||
|
||||
if cBudget.FiscalYear < 1 || cBudget.FiscalYear > 2200 {
|
||||
errs = append(errs, "fiscal_year must be between 1 and 2200")
|
||||
}
|
||||
if cBudget.FiscalPeriod < 1 || cBudget.FiscalPeriod > 12 {
|
||||
errs = append(errs, "fiscal_period must be between 1 and 12")
|
||||
}
|
||||
if cBudget.Version == "" {
|
||||
errs = append(errs, "version is required")
|
||||
}
|
||||
if cBudget.DepartmentID == 0 {
|
||||
errs = append(errs, "department_id is required")
|
||||
}
|
||||
if cBudget.GLAccountID == 0 {
|
||||
errs = append(errs, "gl_account_id is required")
|
||||
}
|
||||
if cBudget.Amount <= 0 {
|
||||
errs = append(errs, "amount must be greater than 0")
|
||||
}
|
||||
if cBudget.Currency == "" {
|
||||
errs = append(errs, "currency is required")
|
||||
}
|
||||
if cBudget.CreatedBy == "" {
|
||||
errs = append(errs, "created_by is required")
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type UpdateBudgetRequest struct {
|
||||
ID int `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Notes string `json:"notes"`
|
||||
Version BudgetVersion `json:"version"`
|
||||
ChangedBy string `json:"created_by"`
|
||||
ID int `json:"id"`
|
||||
FiscalYear int `json:"fiscal_year"`
|
||||
FiscalPeriod int `json:"fiscal_period"`
|
||||
Version BudgetVersion `json:"version"`
|
||||
DepartmentID int `json:"department_id"`
|
||||
GLAccountID int `json:"gl_account_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Notes string `json:"notes"`
|
||||
ChangedBy string `json:"created_by"`
|
||||
}
|
||||
|
||||
func (uBudget *UpdateBudgetRequest) Valid() []string {
|
||||
var errs []string
|
||||
|
||||
if uBudget.FiscalYear < 1 || uBudget.FiscalYear > 2200 {
|
||||
errs = append(errs, "fiscal_year must be between 1 and 2200")
|
||||
}
|
||||
if uBudget.FiscalPeriod < 1 || uBudget.FiscalPeriod > 12 {
|
||||
errs = append(errs, "fiscal_period must be between 1 and 12")
|
||||
}
|
||||
if uBudget.Version == "" {
|
||||
errs = append(errs, "version is required")
|
||||
}
|
||||
if uBudget.DepartmentID == 0 {
|
||||
errs = append(errs, "department_id is required")
|
||||
}
|
||||
if uBudget.GLAccountID == 0 {
|
||||
errs = append(errs, "gl_account_id is required")
|
||||
}
|
||||
if uBudget.Amount <= 0 {
|
||||
errs = append(errs, "amount must be greater than 0")
|
||||
}
|
||||
if uBudget.Currency == "" {
|
||||
errs = append(errs, "currency is required")
|
||||
}
|
||||
if uBudget.ChangedBy == "" {
|
||||
errs = append(errs, "created_by is required")
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type Actual struct {
|
||||
@@ -93,6 +160,34 @@ type IngestActualsRequest struct {
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
func (i *IngestActualsRequest) Valid() []string {
|
||||
var errs []string
|
||||
|
||||
if i.FiscalYear < 1 || i.FiscalYear > 2200 {
|
||||
errs = append(errs, "fiscal_year must be between 1 and 2200")
|
||||
}
|
||||
if i.FiscalPeriod < 1 || i.FiscalPeriod > 12 {
|
||||
errs = append(errs, "fiscal_period must be between 1 and 12")
|
||||
}
|
||||
if i.DeptCode == "" {
|
||||
errs = append(errs, "dept_code is required")
|
||||
}
|
||||
if i.GLCode == "" {
|
||||
errs = append(errs, "gl_code is required")
|
||||
}
|
||||
if i.Amount <= 0 {
|
||||
errs = append(errs, "amount must be greater than 0")
|
||||
}
|
||||
if i.Currency == "" {
|
||||
errs = append(errs, "currency is required")
|
||||
}
|
||||
if i.Source == "" {
|
||||
errs = append(errs, "source is required")
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type VarianceStatus string
|
||||
|
||||
const (
|
||||
|
||||
334
internal/test/refrence_test.go
Normal file
334
internal/test/refrence_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user