textmachine/platform/internal/ingest/money_test.go

64 lines
2.5 KiB
Go

package ingest
import (
"encoding/json"
"testing"
"textmachine/platform/internal/money"
)
// Money crosses the seam exactly once, here. The cases below are the ones a float64 gets wrong or
// gets right only by luck. Mutation caught: binding committed_usd to a float64 and multiplying, or
// rounding to nearest instead of away from zero.
func TestSpendConvertsExactlyAndRoundsUp(t *testing.T) {
usd := func(v money.MicroUSD) *money.MicroUSD { return &v }
cases := map[string]*money.MicroUSD{
`{"committed_usd":0}`: usd(0),
`{"committed_usd":1.25}`: usd(1_250_000),
`{"committed_usd":0.000001}`: usd(1),
`{"committed_usd":0.0000001}`: usd(1), // a tenth of a micro-dollar still costs one
`{"committed_usd":0.1}`: usd(100_000), // the classic float64 case: 0.1 is not 0.1
`{"committed_usd":8.7}`: usd(8_700_000),
`{"committed_usd":29.7}`: usd(29_700_000),
`{"committed_usd":1e-6}`: usd(1),
`{"committed_usd":"1.25"}`: usd(1_250_000), // a quoted decimal is still a decimal
// JSON null never reaches UnmarshalJSON for a pointer: it IS the absence.
`{"committed_usd":null}`: nil,
`{}`: nil,
`{"committed_usd":123456.789012}`: usd(123_456_789_012),
`{"committed_usd":123456.7890121}`: usd(123_456_789_013),
}
for raw, want := range cases {
var r StatusReport
if err := json.Unmarshal([]byte(raw), &r); err != nil {
t.Fatalf("%s: %v", raw, err)
}
switch {
case r.Spend == nil && want != nil:
t.Fatalf("%s: spend is absent, want %d", raw, *want)
case r.Spend != nil && want == nil:
t.Fatalf("%s: spend = %d, want absent", raw, *r.Spend)
case r.Spend != nil && *r.Spend != *want:
t.Fatalf("%s: spend = %d, want %d", raw, *r.Spend, *want)
}
}
}
func TestSpendRefusesNonsense(t *testing.T) {
for _, raw := range []string{
`{"committed_usd":"free"}`,
`{"committed_usd":1e30}`, // beyond int64 micro-USD
`{"committed_usd":""}`, // an empty figure is not a figure
// PD-79. The STRING "null" — a sender that formatted its own absence. It used to be read as a
// real zero in a non-nil pointer, and on the settlement path a zero means "the attempt cost
// nothing": the hold is released and nothing is charged. Only the JSON literal above is
// absence, and the literal never reaches this code.
`{"committed_usd":"null"}`,
`{"committed_usd":"NULL"}`,
} {
var r StatusReport
if err := json.Unmarshal([]byte(raw), &r); err == nil {
t.Fatalf("%s: accepted", raw)
}
}
}