textmachine/platform/cmd/tmplatformctl/main_test.go

301 lines
12 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"textmachine/platform/internal/money"
"textmachine/platform/internal/pgstore"
)
// brokenBalance is a store whose write commits and whose read-back fails.
type brokenBalance struct{}
func (brokenBalance) Balance(context.Context, string) (money.MicroUSD, error) {
return 0, errors.New("connection reset by peer")
}
// The rule this command calls load-bearing, and the one it had no test for (PD-106): once the ledger
// row is committed, NOTHING downstream may report failure. An operator who reads an error retries,
// and a retry without --key mints a fresh idempotency key — so a failed BALANCE READ would buy a
// double credit out of a write that already succeeded.
// Mutation caught: returning the balance error from write instead of noting it.
func TestACommittedWriteNeverReportsFailure(t *testing.T) {
var out strings.Builder
err := write(t.Context(), brokenBalance{}, &out, "u1", "key-1",
func(string, time.Time) (bool, error) { return true, nil }, "granted 5.000000 to u1")
if err != nil {
t.Fatalf("a committed grant reported failure: %v", err)
}
if !strings.Contains(out.String(), "granted 5.000000 to u1") {
t.Fatalf("the operator was not told the write happened:\n%s", out.String())
}
// The failure is not swallowed either — it is a warning on the same output.
if !strings.Contains(out.String(), "warning: could not read the balance back") {
t.Fatalf("the failed read-back left no trace:\n%s", out.String())
}
}
// A write that did NOT happen must say so. "Applied" and "that key was already spent" are different
// outcomes, and reporting the second as the first is how an operator believes they credited twice.
// Mutation caught: dropping the !applied branch.
func TestASpentKeyIsReportedAsANoOp(t *testing.T) {
var out strings.Builder
err := write(t.Context(), brokenBalance{}, &out, "u1", "key-1",
func(string, time.Time) (bool, error) { return false, nil }, "granted 5.000000 to u1")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), "no-op") || strings.Contains(out.String(), "granted") {
t.Fatalf("a no-op was reported as a grant:\n%s", out.String())
}
}
// The failure BEFORE the commit is the opposite case: it must reach the operator as an error, or a
// grant that never happened reads as one that did.
func TestAFailedWriteIsAnError(t *testing.T) {
var out strings.Builder
err := write(t.Context(), brokenBalance{}, &out, "u1", "",
func(string, time.Time) (bool, error) { return false, errors.New("deadlock detected") },
"granted 5.000000 to u1")
if err == nil {
t.Fatalf("a failed grant reported success:\n%s", out.String())
}
if out.String() != "" {
t.Fatalf("a failed grant printed an outcome:\n%s", out.String())
}
}
// The failure path carries the KEY (PD-89). An ambiguous break on the commit — the connection drops
// while Postgres is committing — reads as a failure, the operator retries, and a retry without --key
// mints a fresh key: the second run credits again. The only thing that makes the retry safe is
// knowing the key the first run used, so the error names it, and names it as the flag to repeat with.
// Mutation caught: returning the operation's error unwrapped.
func TestAFailedWriteNamesTheKeyItUsedSoTheRetryCannotCreditTwice(t *testing.T) {
for _, explicit := range []string{"", "invoice-42"} {
var out strings.Builder
var used string
err := write(t.Context(), brokenBalance{}, &out, "u1", explicit,
func(id string, _ time.Time) (bool, error) {
used = id
return false, errors.New("connection reset by peer")
},
"granted 5.000000 to u1")
if err == nil {
t.Fatalf("a failed grant (key %q) reported success", explicit)
}
if used == "" || !strings.Contains(err.Error(), "--key "+used) {
t.Fatalf("the error does not tell the operator to repeat with --key %q:\n%v", used, err)
}
if explicit != "" && used != explicit {
t.Fatalf("explicit key became %q", used)
}
if !strings.Contains(err.Error(), "connection reset by peer") {
t.Fatalf("the operation's own error was lost on the way out:\n%v", err)
}
if out.String() != "" {
t.Fatalf("a failed grant printed an outcome:\n%s", out.String())
}
}
}
// Without --key every invocation is its own intent: two deliberate grants on one day are two grants.
// Mutation caught: deriving the default key from the account or the date.
func TestEachInvocationWithoutAKeyIsItsOwnIntent(t *testing.T) {
seen := map[string]bool{}
for range 100 {
var out strings.Builder
var used string
if err := write(t.Context(), brokenBalance{}, &out, "u1", "",
func(id string, _ time.Time) (bool, error) { used = id; return true, nil }, "granted"); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(used, "cli-") {
t.Fatalf("generated key = %q, want it marked as the CLI's", used)
}
if seen[used] {
t.Fatalf("key %q was minted twice: the second grant would silently collapse", used)
}
seen[used] = true
}
// An explicit key is used verbatim — that is the whole point of offering one.
var out strings.Builder
var used string
if err := write(t.Context(), brokenBalance{}, &out, "u1", "invoice-42",
func(id string, _ time.Time) (bool, error) { used = id; return true, nil }, "granted"); err != nil {
t.Fatal(err)
}
if used != "invoice-42" {
t.Fatalf("explicit key became %q", used)
}
}
// Argument handling, which no test saw either. Every one of these must be refused on its own terms,
// BEFORE any database work.
//
// The expected message is asserted, not merely "an error came back": the DSN below points at a port
// nothing listens on, so a command that got past its own checks fails anyway on connect — and a test
// that only asked for non-nil would pass against a build with no argument checks at all. Measured:
// two of these mutations survived the first version of this test for exactly that reason.
func TestBadArgumentsAreRefused(t *testing.T) {
t.Setenv("TM_PLATFORM_DSN", "postgres://nobody@127.0.0.1:1/none?sslmode=disable&connect_timeout=1")
for name, tc := range map[string]struct {
args []string
want string
wantUsage bool
}{
"no command at all": {args: nil, want: "usage", wantUsage: true},
"unknown command": {args: []string{"delete-everything"}, want: `unknown command "delete-everything"`, wantUsage: true},
"grant without a user": {args: []string{"grant", "--usd", "5"}, want: "grant needs --user and --usd"},
"grant without amount": {args: []string{"grant", "--user", "u1"}, want: "grant needs --user and --usd"},
"grant of a non-amount": {args: []string{"grant", "--user", "u1", "--usd", "five"}, want: "not a decimal amount"},
"adjust without a note": {args: []string{"adjust", "--user", "u1", "--usd", "-1"}, want: "adjust needs --user, --usd and --note"},
"balance without user": {args: []string{"balance"}, want: "balance needs --user"},
"logins without user": {args: []string{"logins"}, want: "logins needs --user"},
"revoke without user": {args: []string{"revoke"}, want: "revoke needs --user"},
"unknown flag": {args: []string{"grant", "--userr", "u1"}, want: "not defined"},
"run without a verb": {args: []string{"run"}, want: "run takes a subcommand", wantUsage: true},
"run with a wrong verb": {args: []string{"run", "explode"}, want: "run takes a subcommand", wantUsage: true},
"unquarantine, no run": {args: []string{"run", "unquarantine"}, want: "run unquarantine needs --run"},
} {
t.Run(name, func(t *testing.T) {
var out strings.Builder
err := run(tc.args, &out)
if err == nil {
t.Fatalf("accepted %v; output was %q", tc.args, out.String())
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("%v failed with %q, want it refused for %q", tc.args, err, tc.want)
}
if errors.Is(err, errUsage) != tc.wantUsage {
t.Fatalf("usage=%v for %v: %v", errors.Is(err, errUsage), tc.args, err)
}
if out.String() != "" {
t.Fatalf("a refused command printed an outcome: %q", out.String())
}
})
}
}
// The DSN is required and its absence is an error, not a silent default to localhost.
func TestMissingDSNIsRefused(t *testing.T) {
t.Setenv("TM_PLATFORM_DSN", "")
t.Setenv("TM_PLATFORM_DSN_FILE", "")
var out strings.Builder
if err := run([]string{"balance", "--user", "u1"}, &out); err == nil {
t.Fatal("ran without a DSN")
}
}
// End to end against a live Postgres: the four commands an operator actually types, through run(),
// with the money crossing the real ledger. Skips loudly without a database, like the pgstore battery.
func TestCommandsAgainstALiveDatabase(t *testing.T) {
dsn := freshDB(t)
t.Setenv("TM_PLATFORM_DSN", dsn)
ctx := t.Context()
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer conn.Close(ctx)
if _, err := conn.Exec(ctx, `insert into users (id, created_at) values ('u-cli', now())`); err != nil {
t.Fatal(err)
}
run1 := capture(t, "grant", "--user", "u-cli", "--usd", "5", "--note", "free tier", "--key", "k1")
if !strings.Contains(run1, "granted 5.000000 to u-cli") || !strings.Contains(run1, "balance is 5.000000") {
t.Fatalf("grant said:\n%s", run1)
}
// The same key again is a no-op that says so, and the balance does not move.
run2 := capture(t, "grant", "--user", "u-cli", "--usd", "5", "--note", "free tier", "--key", "k1")
if !strings.Contains(run2, "no-op") || !strings.Contains(run2, "balance is 5.000000") {
t.Fatalf("the repeat said:\n%s", run2)
}
if got := capture(t, "adjust", "--user", "u-cli", "--usd", "-2.50", "--note", "refund"); !strings.Contains(got, "balance is 2.500000") {
t.Fatalf("adjust said:\n%s", got)
}
if got := capture(t, "balance", "--user", "u-cli"); !strings.Contains(got, "balance 2.500000") {
t.Fatalf("balance said:\n%s", got)
}
// The journal renders with its header even when the account has no sign-ins yet.
if got := capture(t, "logins", "--user", "u-cli"); !strings.Contains(got, "WHEN") || !strings.Contains(got, "PROVIDER") {
t.Fatalf("logins said:\n%s", got)
}
if got := capture(t, "revoke", "--user", "u-cli"); !strings.Contains(got, "revoked 0 sessions") {
t.Fatalf("revoke said:\n%s", got)
}
// A grant against an account that does not exist is refused, not silently written: the ledger
// would otherwise carry rows nobody owns.
var out strings.Builder
if err := run([]string{"grant", "--user", "nobody", "--usd", "5"}, &out); err == nil {
t.Fatalf("credited an account that does not exist:\n%s", out.String())
}
}
func capture(t *testing.T, args ...string) string {
t.Helper()
var out strings.Builder
if err := run(args, &out); err != nil {
t.Fatalf("%v: %v (output %q)", args, err, out.String())
}
return out.String()
}
// freshDB creates a database of its own and migrates it, the same contract as the pgstore battery's
// helper: a test that leaves rows behind passes once and then lies.
//
// ⚠ It is a COPY of that helper, deliberately. Sharing it would mean a package both of them import,
// and pgstore's own battery lives in `package pgstore` — so such a package could not import pgstore
// for Migrate without a cycle, and a version that took migrate as a parameter would be more
// machinery than the twenty lines it saves.
func freshDB(t *testing.T) string {
t.Helper()
admin := os.Getenv("TM_PLATFORM_TEST_DSN")
if admin == "" {
t.Skip("TM_PLATFORM_TEST_DSN not set: the end-to-end commands need a live Postgres")
}
ctx := context.Background()
var suffix [6]byte
if _, err := rand.Read(suffix[:]); err != nil {
t.Fatal(err)
}
name := "tm_ctl_test_" + hex.EncodeToString(suffix[:])
conn, err := pgx.Connect(ctx, admin)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer conn.Close(ctx)
if _, err := conn.Exec(ctx, `create database `+pgx.Identifier{name}.Sanitize()); err != nil {
t.Fatalf("create database: %v", err)
}
t.Cleanup(func() {
c, err := pgx.Connect(context.Background(), admin)
if err != nil {
return
}
defer c.Close(context.Background())
_, _ = c.Exec(context.Background(), `drop database `+pgx.Identifier{name}.Sanitize()+` with (force)`)
})
u, err := url.Parse(admin)
if err != nil {
t.Fatalf("TM_PLATFORM_TEST_DSN must be a URL: %v", err)
}
u.Path = "/" + name
dsn := u.String()
if err := pgstore.Migrate(ctx, dsn); err != nil {
t.Fatalf("migrate: %v", err)
}
return dsn
}