11import { afterEach , describe , expect , test } from "bun:test"
22import { createHash } from "node:crypto"
33
4- import { loginWithPassword } from "../src/auth/login.js"
4+ import { loginWithPassword , loginWithPat } from "../src/auth/login.js"
55
66function base64Url ( input : Buffer ) : string {
77 return input . toString ( "base64" ) . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / g, "" )
@@ -13,8 +13,58 @@ afterEach(() => {
1313 globalThis . fetch = originalFetch
1414} )
1515
16- describe ( "OAuth login" , ( ) => {
17- test ( "loginWithPassword sends oauthLoginParam and exchanges authorizationCode" , async ( ) => {
16+ describe ( "login OAuth opt-in" , ( ) => {
17+ test ( "password login stays a plain credential exchange by default (no oauthLoginParam)" , async ( ) => {
18+ let loginPayload : Record < string , unknown > | undefined
19+ let tokenExchangeCalls = 0
20+ globalThis . fetch = ( async ( input : RequestInfo | URL , init ?: RequestInit ) => {
21+ const url = new URL ( String ( input ) )
22+ if ( url . pathname === "/clickzetta-portal/user/loginSingle" && init ?. method === "POST" ) {
23+ loginPayload = JSON . parse ( String ( init . body ) ) as Record < string , unknown >
24+ return new Response ( JSON . stringify ( {
25+ code : 0 ,
26+ data : { token : "legacy-token" , userId : 7 , instanceId : 9 , expireTime : 123 } ,
27+ } ) , { status : 200 , headers : { "content-type" : "application/json" } } )
28+ }
29+ if ( url . pathname === "/clickzetta-hornhub/oauth2/token" ) tokenExchangeCalls += 1
30+ return new Response ( "not found" , { status : 404 } )
31+ } ) as typeof fetch
32+
33+ const token = await loginWithPassword ( "https://service.example.com" , "user" , "pass" , "inst" )
34+
35+ // The whole payload is just credentials — no OAuth upgrade fields leak in.
36+ expect ( loginPayload ?. username ) . toBe ( "user" )
37+ expect ( loginPayload ?. password ) . toBe ( "pass" )
38+ expect ( loginPayload ?. instanceName ) . toBe ( "inst" )
39+ expect ( loginPayload ?. oauthLoginParam ) . toBeUndefined ( )
40+ expect ( token . token ) . toBe ( "legacy-token" )
41+ expect ( token . refreshToken ) . toBeUndefined ( )
42+ expect ( tokenExchangeCalls ) . toBe ( 0 )
43+ } )
44+
45+ test ( "PAT login stays a plain credential exchange by default (no oauthLoginParam)" , async ( ) => {
46+ let loginPayload : Record < string , unknown > | undefined
47+ globalThis . fetch = ( async ( input : RequestInfo | URL , init ?: RequestInit ) => {
48+ const url = new URL ( String ( input ) )
49+ if ( url . pathname === "/clickzetta-portal/user/loginSingle" && init ?. method === "POST" ) {
50+ loginPayload = JSON . parse ( String ( init . body ) ) as Record < string , unknown >
51+ return new Response ( JSON . stringify ( {
52+ code : 0 ,
53+ data : { token : "legacy-token" , userId : 7 , instanceId : 9 , expireTime : 123 } ,
54+ } ) , { status : 200 , headers : { "content-type" : "application/json" } } )
55+ }
56+ return new Response ( "not found" , { status : 404 } )
57+ } ) as typeof fetch
58+
59+ const token = await loginWithPat ( "https://service.example.com" , "my-pat" , "inst" )
60+
61+ expect ( loginPayload ?. accessToken ) . toBe ( "my-pat" )
62+ expect ( loginPayload ?. instanceName ) . toBe ( "inst" )
63+ expect ( loginPayload ?. oauthLoginParam ) . toBeUndefined ( )
64+ expect ( token . token ) . toBe ( "legacy-token" )
65+ } )
66+
67+ test ( "explicit oauth opt-in sends oauthLoginParam and exchanges authorizationCode" , async ( ) => {
1868 let loginPayload : Record < string , unknown > | undefined
1969 let tokenPayload : URLSearchParams | undefined
2070 globalThis . fetch = ( async ( input : RequestInfo | URL , init ?: RequestInit ) => {
@@ -44,12 +94,9 @@ describe("OAuth login", () => {
4494 return new Response ( "not found" , { status : 404 } )
4595 } ) as typeof fetch
4696
47- const token = await loginWithPassword ( "https://service.example.com" , "user" , "pass" , "inst" )
97+ const token = await loginWithPassword ( "https://service.example.com" , "user" , "pass" , "inst" , true )
4898
4999 const oauthLoginParam = loginPayload ?. oauthLoginParam as Record < string , unknown >
50- expect ( loginPayload ?. username ) . toBe ( "user" )
51- expect ( loginPayload ?. password ) . toBe ( "pass" )
52- expect ( loginPayload ?. instanceName ) . toBe ( "inst" )
53100 expect ( oauthLoginParam . oauthLogin ) . toBe ( true )
54101 expect ( oauthLoginParam . clientId ) . toBe ( "official-cli" )
55102 expect ( oauthLoginParam . redirectUri ) . toBe ( "http://127.0.0.1/callback" )
@@ -67,28 +114,21 @@ describe("OAuth login", () => {
67114 expect ( token . expireTimeMs ) . toBe ( 900_000 )
68115 } )
69116
70- test ( "loginWithPassword keeps legacy token when authorizationCode is absent " , async ( ) => {
117+ test ( "oauth opt-in without a returned authorizationCode keeps the legacy token " , async ( ) => {
71118 let tokenExchangeCalls = 0
72119 globalThis . fetch = ( async ( input : RequestInfo | URL , init ?: RequestInit ) => {
73120 const url = new URL ( String ( input ) )
74121 if ( url . pathname === "/clickzetta-portal/user/loginSingle" && init ?. method === "POST" ) {
75122 return new Response ( JSON . stringify ( {
76123 code : 0 ,
77- data : {
78- token : "legacy-token" ,
79- userId : 7 ,
80- instanceId : 9 ,
81- expireTime : 123 ,
82- } ,
124+ data : { token : "legacy-token" , userId : 7 , instanceId : 9 , expireTime : 123 } ,
83125 } ) , { status : 200 , headers : { "content-type" : "application/json" } } )
84126 }
85- if ( url . pathname === "/clickzetta-hornhub/oauth2/token" ) {
86- tokenExchangeCalls += 1
87- }
127+ if ( url . pathname === "/clickzetta-hornhub/oauth2/token" ) tokenExchangeCalls += 1
88128 return new Response ( "not found" , { status : 404 } )
89129 } ) as typeof fetch
90130
91- const token = await loginWithPassword ( "https://service.example.com" , "user" , "pass" , "inst" )
131+ const token = await loginWithPassword ( "https://service.example.com" , "user" , "pass" , "inst" , true )
92132
93133 expect ( token . token ) . toBe ( "legacy-token" )
94134 expect ( tokenExchangeCalls ) . toBe ( 0 )
0 commit comments