@@ -29,6 +29,8 @@ import { generateShortId } from '../../src/ids';
2929import { usersFixture } from '../fixture' ;
3030import { ioRedisPool } from '../../src/redis' ;
3131import * as betterAuthModule from '../../src/betterAuth' ;
32+ import { rewriteOAuthErrorRedirect } from '../../src/routes/betterAuth' ;
33+ import type { FastifyRequest } from 'fastify' ;
3234
3335jest . mock ( '../../src/common/paddle/index.ts' , ( ) => ( {
3436 ...( jest . requireActual ( '../../src/common/paddle/index.ts' ) as Record <
@@ -395,4 +397,182 @@ describe('betterAuth routes', () => {
395397 getBetterAuthSpy . mockRestore ( ) ;
396398 } ) ;
397399 } ) ;
400+
401+ describe ( 'rewriteOAuthErrorRedirect helper' , ( ) => {
402+ const makeRequest = ( url : string ) : FastifyRequest =>
403+ ( {
404+ url,
405+ protocol : 'http' ,
406+ host : 'localhost' ,
407+ } ) as FastifyRequest ;
408+
409+ const makeRedirectResponse = (
410+ location : string | null ,
411+ status = 302 ,
412+ ) : Response => {
413+ const headers = new Headers ( ) ;
414+ if ( location !== null ) {
415+ headers . set ( 'location' , location ) ;
416+ }
417+ return new Response ( null , { status, headers } ) ;
418+ } ;
419+
420+ it ( 'should return payload with all fields for error redirect' , ( ) => {
421+ const result = rewriteOAuthErrorRedirect (
422+ makeRequest ( '/auth/callback/google' ) ,
423+ makeRedirectResponse (
424+ '/api/auth/error?error=access_denied&error_description=cancelled&state=abc123' ,
425+ ) ,
426+ ) ;
427+
428+ expect ( result ) . toEqual ( {
429+ url : `${ process . env . COMMENTS_PREFIX } /callback?error=access_denied&error_description=cancelled&state=abc123` ,
430+ provider : 'google' ,
431+ error : 'access_denied' ,
432+ errorDescription : 'cancelled' ,
433+ state : 'abc123' ,
434+ } ) ;
435+ } ) ;
436+
437+ it ( 'should return payload when state is state_not_found even without error param' , ( ) => {
438+ const result = rewriteOAuthErrorRedirect (
439+ makeRequest ( '/auth/callback/github' ) ,
440+ makeRedirectResponse ( '/api/auth/error?state=state_not_found' ) ,
441+ ) ;
442+
443+ expect ( result ) . toEqual ( {
444+ url : `${ process . env . COMMENTS_PREFIX } /callback?state=state_not_found` ,
445+ provider : 'github' ,
446+ error : undefined ,
447+ errorDescription : undefined ,
448+ state : 'state_not_found' ,
449+ } ) ;
450+ } ) ;
451+
452+ it ( 'should return undefined for non-callback paths' , ( ) => {
453+ const result = rewriteOAuthErrorRedirect (
454+ makeRequest ( '/auth/sign-in/social' ) ,
455+ makeRedirectResponse ( '/api/auth/error?error=access_denied' ) ,
456+ ) ;
457+
458+ expect ( result ) . toBeUndefined ( ) ;
459+ } ) ;
460+
461+ it ( 'should return undefined for non-3xx responses' , ( ) => {
462+ const result = rewriteOAuthErrorRedirect (
463+ makeRequest ( '/auth/callback/google' ) ,
464+ makeRedirectResponse ( '/api/auth/error?error=access_denied' , 200 ) ,
465+ ) ;
466+
467+ expect ( result ) . toBeUndefined ( ) ;
468+ } ) ;
469+
470+ it ( 'should return undefined when location header is missing' , ( ) => {
471+ const result = rewriteOAuthErrorRedirect (
472+ makeRequest ( '/auth/callback/google' ) ,
473+ makeRedirectResponse ( null ) ,
474+ ) ;
475+
476+ expect ( result ) . toBeUndefined ( ) ;
477+ } ) ;
478+
479+ it ( 'should return undefined when redirect has no error params' , ( ) => {
480+ const result = rewriteOAuthErrorRedirect (
481+ makeRequest ( '/auth/callback/google' ) ,
482+ makeRedirectResponse ( '/some-other-page?state=success' ) ,
483+ ) ;
484+
485+ expect ( result ) . toBeUndefined ( ) ;
486+ } ) ;
487+
488+ it ( 'should return undefined when location already points at webapp callback' , ( ) => {
489+ const result = rewriteOAuthErrorRedirect (
490+ makeRequest ( '/auth/callback/google' ) ,
491+ makeRedirectResponse (
492+ `${ process . env . COMMENTS_PREFIX } /callback?error=access_denied` ,
493+ ) ,
494+ ) ;
495+
496+ expect ( result ) . toBeUndefined ( ) ;
497+ } ) ;
498+ } ) ;
499+
500+ describe ( 'OAuth callback error rewrite' , ( ) => {
501+ const mockBetterAuthRedirect = ( location : string ) =>
502+ jest . spyOn ( betterAuthModule , 'getBetterAuth' ) . mockReturnValue ( {
503+ handler : async ( ) =>
504+ new Response ( null , {
505+ status : 302 ,
506+ headers : { location } ,
507+ } ) ,
508+ api : {
509+ getSession : async ( ) => null ,
510+ setPassword : async ( ) => ( { status : true } ) ,
511+ } ,
512+ } as ReturnType < typeof betterAuthModule . getBetterAuth > ) ;
513+
514+ it ( 'should rewrite error redirect to webapp callback with params forwarded' , async ( ) => {
515+ const spy = mockBetterAuthRedirect (
516+ '/api/auth/error?error=access_denied&error_description=cancelled&state=abc123' ,
517+ ) ;
518+
519+ const res = await request ( app . server ) . get ( '/auth/callback/google' ) ;
520+
521+ expect ( res . status ) . toBe ( 302 ) ;
522+ expect ( res . headers . location ) . toBe (
523+ `${ process . env . COMMENTS_PREFIX } /callback?error=access_denied&error_description=cancelled&state=abc123` ,
524+ ) ;
525+
526+ spy . mockRestore ( ) ;
527+ } ) ;
528+
529+ it ( 'should rewrite when state is state_not_found even without error param' , async ( ) => {
530+ const spy = mockBetterAuthRedirect (
531+ '/api/auth/error?state=state_not_found' ,
532+ ) ;
533+
534+ const res = await request ( app . server ) . get ( '/auth/callback/github' ) ;
535+
536+ expect ( res . status ) . toBe ( 302 ) ;
537+ expect ( res . headers . location ) . toBe (
538+ `${ process . env . COMMENTS_PREFIX } /callback?state=state_not_found` ,
539+ ) ;
540+
541+ spy . mockRestore ( ) ;
542+ } ) ;
543+
544+ it ( 'should pass through redirect without error params' , async ( ) => {
545+ const spy = mockBetterAuthRedirect ( '/some-other-page?state=success' ) ;
546+
547+ const res = await request ( app . server ) . get ( '/auth/callback/google' ) ;
548+
549+ expect ( res . status ) . toBe ( 302 ) ;
550+ expect ( res . headers . location ) . toBe ( '/some-other-page?state=success' ) ;
551+
552+ spy . mockRestore ( ) ;
553+ } ) ;
554+
555+ it ( 'should not rewrite when location already points at webapp callback' , async ( ) => {
556+ const originalLocation = `${ process . env . COMMENTS_PREFIX } /callback?error=access_denied` ;
557+ const spy = mockBetterAuthRedirect ( originalLocation ) ;
558+
559+ const res = await request ( app . server ) . get ( '/auth/callback/google' ) ;
560+
561+ expect ( res . status ) . toBe ( 302 ) ;
562+ expect ( res . headers . location ) . toBe ( originalLocation ) ;
563+
564+ spy . mockRestore ( ) ;
565+ } ) ;
566+
567+ it ( 'should not rewrite non-callback paths' , async ( ) => {
568+ const spy = mockBetterAuthRedirect ( '/api/auth/error?error=access_denied' ) ;
569+
570+ const res = await request ( app . server ) . get ( '/auth/sign-in/social' ) ;
571+
572+ expect ( res . status ) . toBe ( 302 ) ;
573+ expect ( res . headers . location ) . toBe ( '/api/auth/error?error=access_denied' ) ;
574+
575+ spy . mockRestore ( ) ;
576+ } ) ;
577+ } ) ;
398578} ) ;
0 commit comments