@@ -86,10 +86,10 @@ describe('SessionGuard', () => {
8686 expect ( request [ 'authMethod' ] ) . toBe ( 'session' ) ;
8787 } ) ;
8888
89- it ( 'returns true even when no session found' , async ( ) => {
89+ it ( 'returns true even when no session found (non-local mode) ' , async ( ) => {
9090 jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
9191 ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( null ) ;
92- const { context, request } = createMockContext ( { } ) ;
92+ const { context, request } = createMockContext ( { ip : '203.0.113.1' } ) ;
9393
9494 const result = await guard . canActivate ( context ) ;
9595
@@ -98,15 +98,146 @@ describe('SessionGuard', () => {
9898 expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
9999 } ) ;
100100
101- it ( 'returns true and leaves user undefined when getSession throws' , async ( ) => {
101+ it ( 'returns true and leaves user undefined when getSession throws (non-local mode) ' , async ( ) => {
102102 jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
103103 ( auth . api . getSession as jest . Mock ) . mockRejectedValue ( new Error ( 'DB connection lost' ) ) ;
104- const { context, request } = createMockContext ( { } ) ;
104+ const { context, request } = createMockContext ( { ip : '203.0.113.1' } ) ;
105105
106106 const result = await guard . canActivate ( context ) ;
107107
108108 expect ( result ) . toBe ( true ) ;
109109 expect ( request [ 'user' ] ) . toBeUndefined ( ) ;
110110 expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
111111 } ) ;
112+
113+ describe ( 'cloud mode (default) — no loopback fallback' , ( ) => {
114+ const originalEnv = process . env [ 'MANIFEST_MODE' ] ;
115+
116+ beforeEach ( ( ) => {
117+ delete process . env [ 'MANIFEST_MODE' ] ;
118+ } ) ;
119+
120+ afterEach ( ( ) => {
121+ if ( originalEnv === undefined ) delete process . env [ 'MANIFEST_MODE' ] ;
122+ else process . env [ 'MANIFEST_MODE' ] = originalEnv ;
123+ } ) ;
124+
125+ it ( 'does NOT inject synthetic user for loopback IP without session' , async ( ) => {
126+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
127+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( null ) ;
128+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
129+
130+ await guard . canActivate ( context ) ;
131+
132+ expect ( request [ 'user' ] ) . toBeUndefined ( ) ;
133+ expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
134+ } ) ;
135+
136+ it ( 'does NOT inject synthetic user for loopback IP when getSession throws' , async ( ) => {
137+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
138+ ( auth . api . getSession as jest . Mock ) . mockRejectedValue ( new Error ( 'DB error' ) ) ;
139+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
140+
141+ await guard . canActivate ( context ) ;
142+
143+ expect ( request [ 'user' ] ) . toBeUndefined ( ) ;
144+ expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
145+ } ) ;
146+
147+ it ( 'attaches real user when Better Auth session is valid (loopback IP)' , async ( ) => {
148+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
149+ const mockSession = {
150+ user : { id : 'cloud-user-1' , name : 'Cloud User' , email : 'cloud@example.com' } ,
151+ session : { id : 'session-cloud' } ,
152+ } ;
153+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( mockSession ) ;
154+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
155+
156+ await guard . canActivate ( context ) ;
157+
158+ expect ( request [ 'user' ] ) . toEqual ( mockSession . user ) ;
159+ expect ( request [ 'authMethod' ] ) . toBe ( 'session' ) ;
160+ } ) ;
161+
162+ it ( 'does NOT inject synthetic user when MANIFEST_MODE is "cloud"' , async ( ) => {
163+ process . env [ 'MANIFEST_MODE' ] = 'cloud' ;
164+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
165+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( null ) ;
166+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
167+
168+ await guard . canActivate ( context ) ;
169+
170+ expect ( request [ 'user' ] ) . toBeUndefined ( ) ;
171+ expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
172+ } ) ;
173+ } ) ;
174+
175+ describe ( 'local mode loopback fallback' , ( ) => {
176+ const originalEnv = process . env [ 'MANIFEST_MODE' ] ;
177+
178+ beforeEach ( ( ) => {
179+ process . env [ 'MANIFEST_MODE' ] = 'local' ;
180+ } ) ;
181+
182+ afterEach ( ( ) => {
183+ if ( originalEnv === undefined ) delete process . env [ 'MANIFEST_MODE' ] ;
184+ else process . env [ 'MANIFEST_MODE' ] = originalEnv ;
185+ } ) ;
186+
187+ it ( 'uses real session when Better Auth session exists (preserves per-user isolation)' , async ( ) => {
188+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
189+ const mockSession = {
190+ user : { id : 'real-user-1' , name : 'Real User' , email : 'real@test.com' } ,
191+ session : { id : 'session-1' } ,
192+ } ;
193+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( mockSession ) ;
194+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
195+
196+ await guard . canActivate ( context ) ;
197+
198+ expect ( request [ 'user' ] ) . toEqual ( mockSession . user ) ;
199+ expect ( request [ 'authMethod' ] ) . toBe ( 'session' ) ;
200+ } ) ;
201+
202+ it ( 'falls back to synthetic local user when no session on loopback' , async ( ) => {
203+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
204+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( null ) ;
205+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
206+
207+ await guard . canActivate ( context ) ;
208+
209+ expect ( request [ 'user' ] ) . toEqual ( {
210+ id : 'local' ,
211+ name : 'Local User' ,
212+ email : 'local@localhost' ,
213+ } ) ;
214+ expect ( request [ 'authMethod' ] ) . toBe ( 'session' ) ;
215+ } ) ;
216+
217+ it ( 'falls back to synthetic local user when getSession throws on loopback' , async ( ) => {
218+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
219+ ( auth . api . getSession as jest . Mock ) . mockRejectedValue ( new Error ( 'DB error' ) ) ;
220+ const { context, request } = createMockContext ( { ip : '127.0.0.1' } ) ;
221+
222+ await guard . canActivate ( context ) ;
223+
224+ expect ( request [ 'user' ] ) . toEqual ( {
225+ id : 'local' ,
226+ name : 'Local User' ,
227+ email : 'local@localhost' ,
228+ } ) ;
229+ expect ( request [ 'authMethod' ] ) . toBe ( 'session' ) ;
230+ } ) ;
231+
232+ it ( 'does not apply loopback fallback for non-loopback IPs' , async ( ) => {
233+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockReturnValue ( false ) ;
234+ ( auth . api . getSession as jest . Mock ) . mockResolvedValue ( null ) ;
235+ const { context, request } = createMockContext ( { ip : '203.0.113.1' } ) ;
236+
237+ await guard . canActivate ( context ) ;
238+
239+ expect ( request [ 'user' ] ) . toBeUndefined ( ) ;
240+ expect ( request [ 'authMethod' ] ) . toBeUndefined ( ) ;
241+ } ) ;
242+ } ) ;
112243} ) ;
0 commit comments