package main import ( "crypto/sha256" "strings" "testing" "time" "github.com/jackc/pgx/v5" "textmachine/platform/internal/auth" "textmachine/platform/internal/pgstore" ) // The whole of the non-browser door, end to end against a live Postgres: a token comes out, only its // DIGEST is stored, the session it names authenticates, the operator's own tool can revoke it, and // the account's journal shows it happened. // // ⚠ The digest assertion is the load-bearing one. "In the database only the hash" was TRUE and not // pinned once before in this zone, and the acceptance pack's mutation survived (ENGINEERING_STANDARDS // §3.4, the P0 lesson). A door that stored the plaintext would pass every other check here. func TestAnIssuedTokenAuthenticatesAndOnlyItsDigestIsStored(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-token', now())`); err != nil { t.Fatal(err) } out := capture(t, "token", "issue", "--user", "u-token", "--client", "smoke") lines := strings.Split(strings.TrimSpace(out), "\n") token := strings.TrimSpace(lines[0]) if token == "" || strings.HasPrefix(token, "#") { t.Fatalf("no token on the first line:\n%s", out) } // It is shown ONCE and the caller is told so — the property that makes the digest-only storage // survivable is that nobody expects to look it up later. if !strings.Contains(out, "shown ONCE") || !strings.Contains(out, "Bearer") { t.Errorf("the caller is not told what they are holding:\n%s", out) } // The PLAINTEXT is nowhere in the database. Checked against the column rather than by reading the // code that wrote it. var stored []byte if err := conn.QueryRow(ctx, `select token_sha256 from sessions where user_id = 'u-token'`).Scan(&stored); err != nil { t.Fatalf("no session row was written: %v", err) } want := sha256.Sum256([]byte(token)) if string(stored) != string(want[:]) { t.Errorf("what is stored is not the digest of what was printed") } var plaintextRows int if err := conn.QueryRow(ctx, `select count(*) from sessions where encode(token_sha256, 'escape') = $1`, token).Scan(&plaintextRows); err != nil { t.Fatal(err) } if plaintextRows != 0 { t.Errorf("the plaintext token is in the sessions table") } // It is a REAL session: the middleware's own lookup accepts it, and it carries the deployment's // own clocks rather than a policy of this command's invention. store, err := pgstore.Open(ctx, dsn) if err != nil { t.Fatal(err) } defer store.Close() sess, err := store.Lookup(ctx, auth.Digest(token), time.Now().UTC()) if err != nil { t.Fatalf("the issued token does not authenticate: %v", err) } if sess.UserID != "u-token" { t.Errorf("it authenticates as %q", sess.UserID) } idle := time.Until(sess.IdleExpiresAt).Round(time.Hour) if idle != 14*24*time.Hour { t.Errorf("idle window %s: not the deployment's own session policy", idle) } // ⚠ AND THE PROPERTY THE EXTRACTION OF config.SessionTTLs() EXISTS FOR — that this command reads // the DEPLOYMENT's policy rather than a private one. Asserting the default proves nothing: a // hard-coded 14d/30d inside the command would pass it. So the policy is MOVED and the token has // to move with it. t.Setenv("TM_PLATFORM_SESSION_IDLE", "48h") t.Setenv("TM_PLATFORM_SESSION_MAX_AGE", "96h") out2 := capture(t, "token", "issue", "--user", "u-token") moved := strings.TrimSpace(strings.Split(strings.TrimSpace(out2), "\n")[0]) sess2, err := store.Lookup(ctx, auth.Digest(moved), time.Now().UTC()) if err != nil { t.Fatalf("the second token does not authenticate: %v", err) } if got := time.Until(sess2.IdleExpiresAt).Round(time.Hour); got != 48*time.Hour { t.Errorf("idle window %s: the command did not read the deployment's own policy", got) } // The journal answers "was that me" for an operator-issued session, like it does for a sign-in. // // ⚠ Read off the COLUMN and not out of the rendered line: the word "operator" also appears in the // reason text on that same line, so a substring check over the table would stay green with the // provider mutated to anything at all. var provider string if err := conn.QueryRow(ctx, `select provider from login_events where user_id = 'u-token' order by at limit 1`).Scan(&provider); err != nil { t.Fatalf("the issue is not in the account's journal: %v", err) } if provider != "operator" { t.Errorf("the journal files this session under provider %q", provider) } if got := capture(t, "logins", "--user", "u-token"); !strings.Contains(got, "operator") { t.Errorf("the operator's own view does not show it:\n%s", got) } // And the ordinary revocation ends it — no second revocation path for a second credential model, // because there is no second credential model. if got := capture(t, "revoke", "--user", "u-token"); !strings.Contains(got, "revoked 2 sessions") { t.Errorf("revoke said:\n%s", got) } if _, err := store.Lookup(ctx, auth.Digest(token), time.Now().UTC()); err == nil { t.Error("a revoked token still authenticates") } } // A token for an account that does not exist is REFUSED and writes nothing: a session naming nobody // would authenticate as nobody, and the foreign key is what says so. func TestATokenForAnAccountThatDoesNotExistIsRefused(t *testing.T) { dsn := freshDB(t) t.Setenv("TM_PLATFORM_DSN", dsn) var out strings.Builder if err := run([]string{"token", "issue", "--user", "nobody"}, &out); err == nil { t.Fatalf("issued a token for an account that is not there:\n%s", out.String()) } if out.String() != "" { t.Errorf("a refused issue printed something that could be mistaken for a token: %q", out.String()) } } // Bad arguments are a USAGE error and print no outcome, like every other command here. // // ⚠ It runs against a live database on purpose. Without one the dispatcher refuses at the DSN gate // before it ever reaches the subcommand, so the test would pass without exercising anything — the // shape of green test that proves only that something went wrong somewhere. func TestTokenArgumentsAreRefused(t *testing.T) { t.Setenv("TM_PLATFORM_DSN", freshDB(t)) for _, args := range [][]string{ {"token"}, {"token", "revoke"}, {"token", "issue"}, // no --user } { var out strings.Builder err := run(args, &out) if err == nil { t.Errorf("%v was accepted", args) } if out.String() != "" { t.Errorf("%v printed an outcome: %q", args, out.String()) } } }