- money_field_test.go: compile gates (moved from the smoke file and extended), model column gate, save-time value check, list projection - settings secret projection and put without a database; tabs, settings richeditor sanitize and the danger flag as internal unit tests - Postgres: TestMoneyFieldSave, TestSettingsSecretPostgres and the Phase 18 payload cases of TestPhase10OpenAPIConformance - vitest: money.ts helpers spec, MoneyField blur-only and server-error cases - gate arrays and the cabana README Testing section name every test
110 lines
4.8 KiB
Go
110 lines
4.8 KiB
Go
package cabana_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
)
|
|
|
|
// TestMoneyFieldSave extends the plan 01 money smoke on Postgres (Phase 18
|
|
// D-07, D-10): an untouched value sent back with another change, or left
|
|
// out of the body, is re-saved byte-identical; a negative amount and the
|
|
// scale-2 field round-trip exactly; null clears; a 422 on update writes
|
|
// nothing; and the list payload serves the stored strings with each row's
|
|
// own currency.
|
|
func TestMoneyFieldSave(t *testing.T) {
|
|
e := newPaymentsEnv(t)
|
|
load := func(t *testing.T, id uint) paymentsInvoice {
|
|
t.Helper()
|
|
var row paymentsInvoice
|
|
if err := e.db.First(&row, id).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return row
|
|
}
|
|
rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-1", "currency": "PLN", "total": "85.4193", "fee": "12.5"}, true)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
id := dataID(t, rec.Body.Bytes())
|
|
if row := load(t, id); row.Total.text != "85.4193" || row.Fee.text != "12.5000" {
|
|
t.Fatalf("stored = %+v", row)
|
|
}
|
|
|
|
// Another field changes, the untouched amount comes back as loaded.
|
|
rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1b", "currency": "EUR", "total": "85.4193", "fee": "12.5000"}, true)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("update status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if row := load(t, id); row.Number != "S-1b" || row.Currency != "EUR" || row.Total.text != "85.4193" || row.Fee.text != "12.5000" {
|
|
t.Fatalf("after update = %+v", row)
|
|
}
|
|
// Left out of the body, the amount is not touched.
|
|
rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c"}, true)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("partial update status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if row := load(t, id); row.Total.text != "85.4193" || row.Fee.text != "12.5000" {
|
|
t.Fatalf("after partial update = %+v", row)
|
|
}
|
|
// Negative amount and the scale-2 field, exactly.
|
|
rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c", "total": "-0.5000", "fee": "0.01"}, true)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("negative status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if row := load(t, id); row.Total.text != "-0.5000" || row.Fee.text != "0.0100" {
|
|
t.Fatalf("after negative = %+v", row)
|
|
}
|
|
// A 422 on update writes nothing.
|
|
for _, body := range []map[string]any{
|
|
{"number": "S-X", "total": "1.00001"},
|
|
{"number": "S-X", "fee": "0.001"},
|
|
{"number": "S-X", "total": "1e2"},
|
|
{"number": "S-X", "total": "1000000.0000"},
|
|
} {
|
|
rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), body, true)
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("%v status=%d body=%s", body, rec.Code, rec.Body.String())
|
|
}
|
|
if row := load(t, id); row.Number != "S-1c" || row.Total.text != "-0.5000" || row.Fee.text != "0.0100" {
|
|
t.Fatalf("a refused update wrote: %+v", row)
|
|
}
|
|
}
|
|
// null clears.
|
|
rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c", "fee": nil}, true)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("clear status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if row := load(t, id); row.Fee.valid || row.Total.text != "-0.5000" {
|
|
t.Fatalf("after clear = %+v", row)
|
|
}
|
|
|
|
// The list payload: stored strings and each row's own currency.
|
|
if rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-2", "currency": "PLN", "total": "99999999.9999"}, true); rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("above the column max status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-2", "currency": "PLN", "total": "999999.9999", "fee": "1"}, true); rec.Code != http.StatusCreated {
|
|
t.Fatalf("create S-2 status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
rec = e.send(t, http.MethodGet, "/acme/payments/invoices?sort=number&dir=asc", nil, true)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var list cabana.ListEnvelope[[]cabana.AdminRecord]
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list.Data) != 2 {
|
|
t.Fatalf("rows = %+v", list.Data)
|
|
}
|
|
if row := list.Data[0]; row["number"] != "S-1c" || row["total"] != "-0.5000" || row["currency"] != "EUR" || row["fee"] != nil {
|
|
t.Fatalf("row 1 = %+v", row)
|
|
}
|
|
if row := list.Data[1]; row["number"] != "S-2" || row["total"] != "999999.9999" || row["currency"] != "PLN" || row["fee"] != "1.0000" {
|
|
t.Fatalf("row 2 = %+v", row)
|
|
}
|
|
}
|