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()) } } // 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"}, } { 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 }