use tempfile::TempDir;
use super::cli_runner::
{
run_cs_with_env, assert_exit, stdout, stderr,
write_credentials, write_account,
FAR_FUTURE_MS,
};
fn mtime_ms( path : &std::path::Path ) -> u64
{
path.metadata().ok()
.and_then( | m | m.modified().ok() )
.and_then( | t | t.duration_since( std::time::UNIX_EPOCH ).ok() )
.map_or( 0, | d | u64::try_from( d.as_millis() ).unwrap_or( 0 ) )
}
#[ test ]
fn account_nc1_full_lifecycle_roundtrip()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "bob@acme.com", "max", "default", FAR_FUTURE_MS, true );
let cred_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
let alice_cred = cred_store.join( "alice@acme.com.credentials.json" );
let out1 = run_cs_with_env(
&[ ".account.save", "name::alice@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out1, 0 );
assert!( alice_cred.exists(), "step 1: alice credentials file must exist after save" );
let out2 = run_cs_with_env(
&[ ".account.use", "name::alice@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out2, 0 );
let out3 = run_cs_with_env(
&[ ".account.use", "name::bob@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out3, 0 );
assert!( alice_cred.exists(), "step 3: alice cred file must remain after switching to bob" );
let out4 = run_cs_with_env(
&[ ".account.delete", "name::alice@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out4, 0 );
assert!( !alice_cred.exists(), "step 4: alice credentials file must be removed after delete" );
}
#[ test ]
fn account_nc2_json_output_schema_valid()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@acme.com", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env(
&[ ".accounts", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let parsed : serde_json::Value = serde_json::from_str( &text )
.expect( "`.accounts format::json` must produce valid JSON" );
let array = parsed.as_array()
.expect( "`.accounts format::json` must produce a JSON array" );
assert!( !array.is_empty(), "array must contain at least one account" );
let entry = &array[ 0 ];
assert!( entry.get( "name" ).is_some(), "each entry must have a 'name' field" );
assert!( entry.get( "is_active" ).is_some(), "each entry must have an 'is_active' field" );
assert!( entry.get( "subscription_type" ).is_some(), "each entry must have a 'subscription_type' field" );
assert!( entry.get( "rate_limit_tier" ).is_some(), "each entry must have a 'rate_limit_tier' field" );
assert!( entry.get( "expires_at_ms" ).is_some(), "each entry must have an 'expires_at_ms' field" );
}
#[ test ]
fn account_nc3_error_codes_match_documented()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@acme.com", "max", "default", FAR_FUTURE_MS, true );
let out_a = run_cs_with_env(
&[ ".account.use", "name::alice/work@example.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_a, 1 );
let out_b = run_cs_with_env(
&[ ".account.use", "name::nobody@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_b, 2 );
let out_c = run_cs_with_env(
&[ ".account.renewal", "name::alice@acme.com" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_c, 1 );
}
#[ test ]
fn token_nc1_status_is_stateless()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
let creds_path = dir.path().join( ".claude" ).join( ".credentials.json" );
let mtime_before = mtime_ms( &creds_path );
let out = run_cs_with_env( &[ ".token.status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
assert_eq!(
mtime_before, mtime_ms( &creds_path ),
".token.status must not modify ~/.claude/.credentials.json",
);
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
let file_count = std::fs::read_dir( &store ).map_or( 0, core::iter::Iterator::count );
assert_eq!( file_count, 0, ".token.status must not create files in credential store" );
}
#[ test ]
fn token_nc2_json_output_schema_valid()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
let out = run_cs_with_env(
&[ ".token.status", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let parsed : serde_json::Value = serde_json::from_str( &text )
.expect( "`.token.status format::json` must produce valid JSON" );
let obj = parsed.as_object()
.expect( "`.token.status format::json` must produce a JSON object" );
assert!( obj.contains_key( "status" ), "output must have 'status' field" );
assert!( obj.contains_key( "expires_in_secs" ), "output must have 'expires_in_secs' field" );
let status_val = obj[ "status" ].as_str().expect( "status field must be a string" );
assert!(
[ "valid", "expiring_soon", "expired" ].contains( &status_val ),
"status must be one of: valid, expiring_soon, expired; got: {status_val}",
);
}
#[ test ]
fn token_nc3_missing_credentials_exits_2()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".token.status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 2 );
let err_text = stderr( &out );
assert!( !err_text.is_empty(), "error message must be non-empty on missing credentials" );
}
#[ test ]
fn credentials_nc1_status_is_stateless()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@acme.com", "max", "default", FAR_FUTURE_MS, true );
let creds_path = dir.path().join( ".claude" ).join( ".credentials.json" );
let cred_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
let alice_file = cred_store.join( "alice@acme.com.credentials.json" );
let mtime_live = mtime_ms( &creds_path );
let mtime_alice = mtime_ms( &alice_file );
let out = run_cs_with_env( &[ ".credentials.status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
assert_eq!(
mtime_live, mtime_ms( &creds_path ),
".credentials.status must not modify ~/.claude/.credentials.json",
);
assert_eq!(
mtime_alice, mtime_ms( &alice_file ),
".credentials.status must not modify stored account credential file",
);
}
#[ test ]
fn credentials_nc2_json_output_schema_valid()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@acme.com", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env(
&[ ".credentials.status", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let parsed : serde_json::Value = serde_json::from_str( &text )
.expect( "`.credentials.status format::json` must produce valid JSON" );
let obj = parsed.as_object()
.expect( "`.credentials.status format::json` must produce a JSON object" );
assert!( obj.contains_key( "subscription" ), "output must have 'subscription' field" );
assert!( obj.contains_key( "tier" ), "output must have 'tier' field" );
assert!( obj.contains_key( "token" ), "output must have 'token' field" );
assert!( obj.contains_key( "expires_in_secs" ), "output must have 'expires_in_secs' field" );
assert!( obj.contains_key( "file" ), "output must have 'file' field" );
}
#[ test ]
fn credentials_nc3_missing_credentials_exits_2()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".credentials.status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 2 );
}