11import assert from "node:assert/strict" ;
2- import { chmodSync , mkdirSync , mkdtempSync , statSync , writeFileSync } from "node:fs" ;
2+ import { chmodSync , mkdirSync , mkdtempSync , readFileSync , statSync , writeFileSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
55import test from "node:test" ;
@@ -19,19 +19,27 @@ const { saveCreds, whoami } = await import("../src/auth.mjs");
1919const posixMode = process . platform === "win32" ? { skip : "POSIX permission bits" } : { } ;
2020
2121/** Run whoami against a canned app response and collect what it printed. */
22- async function whoamiAgainst ( { status , body } ) {
22+ async function whoamiWithFetch ( fetchImpl , options ) {
2323 const realFetch = globalThis . fetch ;
2424 const realLog = console . log ;
2525 const lines = [ ] ;
26- globalThis . fetch = async ( ) => ( { status , ok : status >= 200 && status < 300 , json : async ( ) => body } ) ;
26+ globalThis . fetch = fetchImpl ;
2727 console . log = ( ...args ) => lines . push ( args . join ( " " ) ) ;
28- try { await whoami ( ) ; } finally {
28+ try { await whoami ( options ) ; } finally {
2929 globalThis . fetch = realFetch ;
3030 console . log = realLog ;
3131 }
3232 return lines . join ( "\n" ) ;
3333}
3434
35+ /** Run whoami against a canned app response and collect what it printed. */
36+ function whoamiAgainst ( { status, body } , options ) {
37+ return whoamiWithFetch (
38+ async ( ) => ( { status, ok : status >= 200 && status < 300 , json : async ( ) => body } ) ,
39+ options ,
40+ ) ;
41+ }
42+
3543test ( "whoami does not report an account when the app refuses the token" , async ( ) => {
3644 const out = await whoamiAgainst ( { status : 403 , body : { error : "token revoked" } } ) ;
3745 assert . doesNotMatch ( out , / c r e d i t s / ) ;
@@ -57,6 +65,83 @@ test("whoami still calls out an expired session on 401", async () => {
5765 assert . match ( out , / s e s s i o n e x p i r e d / ) ;
5866} ) ;
5967
68+ test ( "whoami JSON exposes verified account status without credentials" , async ( ) => {
69+ const out = await whoamiAgainst (
70+ { status : 200 , body : { id : "user_1" , email : "me@example.test" , name : "Me" , credits : 42 } } ,
71+ { json : true } ,
72+ ) ;
73+ const result = JSON . parse ( out ) ;
74+
75+ assert . deepEqual ( result , {
76+ status : "authenticated" ,
77+ verified : true ,
78+ api : "https://app.example.test" ,
79+ user : { id : "user_1" , email : "me@example.test" , name : "Me" , credits : 42 } ,
80+ } ) ;
81+ assert . doesNotMatch ( out , / t o k _ r e v o k e d / ) ;
82+ } ) ;
83+
84+ test ( "whoami JSON stays machine-readable when verification fails" , async ( ) => {
85+ const out = await whoamiAgainst (
86+ { status : 403 , body : { error : "token revoked" } } ,
87+ { json : true } ,
88+ ) ;
89+ const result = JSON . parse ( out ) ;
90+
91+ assert . equal ( result . status , "unverified" ) ;
92+ assert . equal ( result . verified , false ) ;
93+ assert . equal ( result . error . status , 403 ) ;
94+ assert . equal ( result . user . email , "me@example.test" ) ;
95+ assert . doesNotMatch ( out , / t o k _ r e v o k e d / ) ;
96+ } ) ;
97+
98+ test ( "whoami JSON reports when no credentials are available" , async ( ) => {
99+ const credentials = join ( home , ".moshcode" , "credentials.json" ) ;
100+ const original = readFileSync ( credentials ) ;
101+ writeFileSync ( credentials , JSON . stringify ( { api : "https://app.example.test" , token : "" } ) ) ;
102+
103+ let out ;
104+ try {
105+ out = await whoamiWithFetch (
106+ async ( ) => { throw new Error ( "fetch should not be called without a token" ) ; } ,
107+ { json : true } ,
108+ ) ;
109+ } finally {
110+ writeFileSync ( credentials , original ) ;
111+ }
112+
113+ const result = JSON . parse ( out ) ;
114+ assert . equal ( result . status , "not_logged_in" ) ;
115+ assert . equal ( result . verified , false ) ;
116+ assert . equal ( result . user , null ) ;
117+ } ) ;
118+
119+ test ( "whoami JSON reports an expired session" , async ( ) => {
120+ const out = await whoamiAgainst (
121+ { status : 401 , body : { error : "unauthorized" } } ,
122+ { json : true } ,
123+ ) ;
124+ const result = JSON . parse ( out ) ;
125+
126+ assert . equal ( result . status , "expired" ) ;
127+ assert . equal ( result . verified , false ) ;
128+ assert . deepEqual ( result . error , { type : "auth" , status : 401 } ) ;
129+ assert . doesNotMatch ( out , / t o k _ r e v o k e d / ) ;
130+ } ) ;
131+
132+ test ( "whoami JSON stays machine-readable when the app is unreachable" , async ( ) => {
133+ const out = await whoamiWithFetch (
134+ async ( ) => { throw new Error ( "network failed with tok_revoked" ) ; } ,
135+ { json : true } ,
136+ ) ;
137+ const result = JSON . parse ( out ) ;
138+
139+ assert . equal ( result . status , "unreachable" ) ;
140+ assert . equal ( result . verified , false ) ;
141+ assert . deepEqual ( result . error , { type : "network" } ) ;
142+ assert . doesNotMatch ( out , / t o k _ r e v o k e d / ) ;
143+ } ) ;
144+
60145test ( "saving credentials tightens a world-readable existing file" , posixMode , ( ) => {
61146 chmodSync ( join ( home , ".moshcode" , "credentials.json" ) , 0o644 ) ;
62147
0 commit comments