@@ -67,126 +67,59 @@ describe('KernelAuth — edge cases (input validation + ambiguity)', () => {
6767 }
6868 } ) ;
6969
70- // Round-4 NF3-2: presence of `oauthClientId` signals M2M intent.
71- // A blank/reserved-literal `oauthClientSecret` is then a missing-secret
72- // typo, not a request to fall back to U2M. Surface the M2M "secret
73- // required" AuthenticationError so the user fixes the real problem
74- // rather than swap class to a HiveDriverError pointing at a flow
75- // they didn't intend to use.
76- it ( 'rejects mixed-case reserved-literal oauthClientSecret with AuthenticationError when id is set' , ( ) => {
77- const opts : ConnectionOptions = {
78- host : 'example.cloud.databricks.com' ,
79- path : '/sql/1.0/warehouses/abc' ,
80- authType : 'databricks-oauth' ,
81- oauthClientId : 'client-uuid' ,
82- oauthClientSecret : 'NULL' ,
83- } ;
84-
85- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw (
86- AuthenticationError ,
87- / o a u t h C l i e n t S e c r e t .* n o n - e m p t y .* O A u t h M 2 M / ,
88- ) ;
89- } ) ;
90-
91- it ( 'rejects whitespace-only oauthClientId on M2M' , ( ) => {
92- const opts : ConnectionOptions = {
93- host : 'example.cloud.databricks.com' ,
94- path : '/sql/1.0/warehouses/abc' ,
95- authType : 'databricks-oauth' ,
96- oauthClientId : ' ' ,
97- oauthClientSecret : 'dose-fake-secret' ,
98- } ;
99-
100- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw ( AuthenticationError , / o a u t h C l i e n t I d .* r e q u i r e d / ) ;
101- } ) ;
102-
103- it ( 'rejects whitespace-only oauthClientSecret with AuthenticationError when oauthClientId is set (M2M intent)' , ( ) => {
104- const opts : ConnectionOptions = {
105- host : 'example.cloud.databricks.com' ,
106- path : '/sql/1.0/warehouses/abc' ,
107- authType : 'databricks-oauth' ,
108- oauthClientId : 'client-uuid' ,
109- oauthClientSecret : '\n\t' ,
110- } ;
111-
112- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw (
113- AuthenticationError ,
114- / o a u t h C l i e n t S e c r e t .* n o n - e m p t y .* O A u t h M 2 M / ,
115- ) ;
116- } ) ;
117-
118- it ( 'rejects literal "undefined" as oauthClientId on M2M' , ( ) => {
119- const opts : ConnectionOptions = {
120- host : 'example.cloud.databricks.com' ,
121- path : '/sql/1.0/warehouses/abc' ,
122- authType : 'databricks-oauth' ,
123- oauthClientId : 'undefined' ,
124- oauthClientSecret : 'dose-fake-secret' ,
125- } ;
126-
127- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw ( AuthenticationError , / o a u t h C l i e n t I d .* r e q u i r e d / ) ;
128- } ) ;
129-
130- it ( 'rejects literal "undefined" as oauthClientSecret with AuthenticationError when id is set (M2M intent)' , ( ) => {
131- const opts : ConnectionOptions = {
132- host : 'example.cloud.databricks.com' ,
133- path : '/sql/1.0/warehouses/abc' ,
134- authType : 'databricks-oauth' ,
135- oauthClientId : 'client-uuid' ,
136- oauthClientSecret : 'undefined' ,
137- } ;
138-
139- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw (
140- AuthenticationError ,
141- / o a u t h C l i e n t S e c r e t .* n o n - e m p t y .* O A u t h M 2 M / ,
142- ) ;
143- } ) ;
144-
145- // Round-4 NF3-2: pin the exact class against the round-3 NF-N3
146- // regression where M2M-with-empty-secret was routed through the U2M
147- // arm and raised a bare `HiveDriverError`. `instanceof
148- // AuthenticationError` correctly returns `false` for a bare
149- // `HiveDriverError` instance (instanceof is a one-way subclass
150- // check), so the subclass check IS sufficient to catch the
151- // regression. We don't add an `error.name` or `constructor.name`
152- // belt — the former requires `this.name` on the subclass (LE4-1
153- // handles that separately for downstream-consumer benefit, not for
154- // this test), and the latter is bundler-fragile (terser/esbuild
155- // strip class names without `keep_classnames`).
156- it ( 'M2M-with-empty-secret throws AuthenticationError, not bare HiveDriverError (class pin)' , ( ) => {
157- const opts : ConnectionOptions = {
158- host : 'example.cloud.databricks.com' ,
159- path : '/sql/1.0/warehouses/abc' ,
160- authType : 'databricks-oauth' ,
161- oauthClientId : 'x' ,
162- oauthClientSecret : '' ,
163- } ;
70+ // Strict Thrift parity: flow = `oauthClientSecret === undefined ? U2M : M2M`,
71+ // and OAuth fields are forwarded VERBATIM (no blank/reserved normalization),
72+ // exactly as the Thrift driver does. A present-but-degenerate secret
73+ // (`""` / whitespace / `"undefined"`) therefore counts as a real secret ⇒ M2M
74+ // — byte-for-byte with Thrift (which only routes to U2M when the secret is
75+ // strictly `undefined`). The id rides through as `oauthClientId ?? default`.
76+ const m2mDegenerateSecret = [
77+ { label : 'reserved-literal "NULL"' , secret : 'NULL' } ,
78+ { label : 'whitespace-only' , secret : '\n\t' } ,
79+ { label : 'literal "undefined"' , secret : 'undefined' } ,
80+ { label : 'empty string' , secret : '' } ,
81+ ] ;
82+ for ( const { label, secret } of m2mDegenerateSecret ) {
83+ it ( `routes id + ${ label } secret to M2M (secret !== undefined ⇒ M2M, like Thrift)` , ( ) => {
84+ const native = buildKernelConnectionOptions ( {
85+ host : 'example.cloud.databricks.com' ,
86+ path : '/sql/1.0/warehouses/abc' ,
87+ authType : 'databricks-oauth' ,
88+ oauthClientId : 'client-uuid' ,
89+ oauthClientSecret : secret ,
90+ } as ConnectionOptions ) ;
91+ expect ( native . authMode ) . to . equal ( 'OAuthM2m' ) ;
92+ expect ( ( native as { oauthClientId ?: string } ) . oauthClientId ) . to . equal ( 'client-uuid' ) ;
93+ } ) ;
94+ }
16495
165- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw (
166- AuthenticationError ,
167- / o a u t h C l i e n t S e c r e t .* n o n - e m p t y .* O A u t h M 2 M / ,
168- ) ;
169- } ) ;
96+ // Degenerate ids on M2M are forwarded verbatim (`oauthClientId ?? default`
97+ // keeps a non-nullish value), NOT rejected — matching Thrift's getClientId().
98+ for ( const id of [ ' ' , 'undefined' ] ) {
99+ it ( `forwards a degenerate oauthClientId (${ JSON . stringify ( id ) } ) verbatim on M2M` , ( ) => {
100+ const native = buildKernelConnectionOptions ( {
101+ host : 'example.cloud.databricks.com' ,
102+ path : '/sql/1.0/warehouses/abc' ,
103+ authType : 'databricks-oauth' ,
104+ oauthClientId : id ,
105+ oauthClientSecret : 'dose-fake-secret' ,
106+ } as ConnectionOptions ) ;
107+ expect ( native . authMode ) . to . equal ( 'OAuthM2m' ) ;
108+ expect ( ( native as { oauthClientId ?: string } ) . oauthClientId ) . to . equal ( id ) ;
109+ } ) ;
110+ }
170111
171- // Round-5 DA4-2: the round-3 → round-4 test flips left the U2M-arm
172- // defense-in-depth U2M+id rejection without coverage. It's still
173- // reachable: when `oauthClientId` is a blank-reserved literal
174- // (whitespace, `"null"`, `"undefined"`) AND `oauthClientSecret` is
175- // absent/blank, BOTH `idIsBlank` and `secretIsBlank` are true so
176- // U2M wins routing — but a non-undefined id signals ambiguity that
177- // U2M cannot honor (the kernel hardcodes `databricks-cli`).
178- it ( 'routes a whitespace oauthClientId with no oauthClientSecret to the U2M defense-in-depth rejection' , ( ) => {
179- const opts : ConnectionOptions = {
112+ // No secret ⇒ U2M, and a degenerate id is forwarded verbatim too (Thrift's
113+ // `oauthClientId ?? default` keeps a non-nullish whitespace id).
114+ it ( 'routes a whitespace oauthClientId with no secret to U2M, forwarding the id verbatim' , ( ) => {
115+ const native = buildKernelConnectionOptions ( {
180116 host : 'example.cloud.databricks.com' ,
181117 path : '/sql/1.0/warehouses/abc' ,
182118 authType : 'databricks-oauth' ,
183119 oauthClientId : ' ' ,
184- } as unknown as ConnectionOptions ;
185-
186- expect ( ( ) => buildKernelConnectionOptions ( opts ) ) . to . throw (
187- HiveDriverError ,
188- / o a u t h C l i e n t I d .* n o t s u p p o r t e d o n t h e O A u t h U 2 M f l o w / ,
189- ) ;
120+ } as unknown as ConnectionOptions ) ;
121+ expect ( native . authMode ) . to . equal ( 'OAuthU2m' ) ;
122+ expect ( ( native as { oauthClientId ?: string } ) . oauthClientId ) . to . equal ( ' ' ) ;
190123 } ) ;
191124 } ) ;
192125
@@ -260,41 +193,21 @@ describe('KernelAuth — edge cases (input validation + ambiguity)', () => {
260193 // `process.env.MY_SECRET || ''` shape) should route to U2M, not
261194 // to the M2M arm with an "empty secret" rejection. M2M's error
262195 // message would never mention U2M, leaving the user stuck.
263- it ( 'routes blank oauthClientSecret to U2M (not to an M2M-blank-secret rejection)' , ( ) => {
264- const opts : ConnectionOptions = {
265- host : 'example.cloud.databricks.com' ,
266- path : '/sql/1.0/warehouses/abc' ,
267- authType : 'databricks-oauth' ,
268- oauthClientSecret : '' ,
269- } ;
270-
271- const native = buildKernelConnectionOptions ( opts ) ;
272- expect ( native . authMode ) . to . equal ( 'OAuthU2m' ) ;
273- } ) ;
274-
275- it ( 'routes whitespace-only oauthClientSecret to U2M too' , ( ) => {
276- const opts : ConnectionOptions = {
277- host : 'example.cloud.databricks.com' ,
278- path : '/sql/1.0/warehouses/abc' ,
279- authType : 'databricks-oauth' ,
280- oauthClientSecret : ' \t ' ,
281- } ;
282-
283- const native = buildKernelConnectionOptions ( opts ) ;
284- expect ( native . authMode ) . to . equal ( 'OAuthU2m' ) ;
285- } ) ;
286-
287- it ( 'routes literal-"undefined" oauthClientSecret to U2M too' , ( ) => {
288- const opts : ConnectionOptions = {
289- host : 'example.cloud.databricks.com' ,
290- path : '/sql/1.0/warehouses/abc' ,
291- authType : 'databricks-oauth' ,
292- oauthClientSecret : 'undefined' ,
293- } ;
294-
295- const native = buildKernelConnectionOptions ( opts ) ;
296- expect ( native . authMode ) . to . equal ( 'OAuthU2m' ) ;
297- } ) ;
196+ // Strict Thrift parity: a present-but-degenerate secret (no id) is still a
197+ // defined secret ⇒ M2M (only a strictly-`undefined` secret routes to U2M).
198+ // With no id, the client defaults via `oauthClientId ?? default`.
199+ for ( const secret of [ '' , ' \t ' , 'undefined' ] ) {
200+ it ( `routes a degenerate-but-present oauthClientSecret (${ JSON . stringify ( secret ) } , no id) to M2M` , ( ) => {
201+ const native = buildKernelConnectionOptions ( {
202+ host : 'example.cloud.databricks.com' ,
203+ path : '/sql/1.0/warehouses/abc' ,
204+ authType : 'databricks-oauth' ,
205+ oauthClientSecret : secret ,
206+ } as ConnectionOptions ) ;
207+ expect ( native . authMode ) . to . equal ( 'OAuthM2m' ) ;
208+ expect ( ( native as { oauthClientId ?: string } ) . oauthClientId ) . to . equal ( 'databricks-sql-connector' ) ;
209+ } ) ;
210+ }
298211 } ) ;
299212
300213 describe ( 'explicit-undefined vs missing for Azure-direct discriminants' , ( ) => {
0 commit comments