diff --git a/app/api/webhooks/polar/route.ts b/app/api/webhooks/polar/route.ts index 130f5f2a..003bdd7b 100644 --- a/app/api/webhooks/polar/route.ts +++ b/app/api/webhooks/polar/route.ts @@ -6,6 +6,7 @@ import { PolarService } from '@/app/api/_lib/polar/services/polar.service'; import { SubscriptionService } from '@/app/api/_lib/polar/services/subscription.service'; import { PolarWebhookProcessorService } from '@/app/api/_lib/polar/services/polar-webhook-processor.service'; import { getNextCorsHeaders } from '@/lib/utils/next-cors'; +import { invalidateUserAuthCache } from '@/lib/auth/cache-invalidation'; /** * API Route para webhooks de Polar @@ -108,6 +109,15 @@ async function processSubscriptionEvent(subscriptionId: string, payloadData?: an if (result.success) { console.log(`Successfully processed subscription ${subscriptionId}:`, result.message); + + // Invalidar cache de autenticación del usuario para asegurar datos actualizados + // Esto es crítico para que el middleware obtenga el plan correcto inmediatamente + if (result.userId) { + console.log(`Invalidating auth cache for user: ${result.userId} (plan: ${result.plan})`); + invalidateUserAuthCache(result.userId); + } else { + console.warn(`No userId found in subscription result for ${subscriptionId}`); + } } else { console.error(`Failed to process subscription ${subscriptionId}:`, result.message); } diff --git a/lib/auth/cache-invalidation.ts b/lib/auth/cache-invalidation.ts new file mode 100644 index 00000000..c5e1d15d --- /dev/null +++ b/lib/auth/cache-invalidation.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { invalidateUserCache } from '@/middlewares/auth/auth'; + +/** + * Invalida el cache de autenticación cuando se actualiza el plan del usuario + * + * @param userId - ID del usuario cuyo cache debe ser invalidado + * + * @example + * // Cuando se actualiza el plan del usuario + * await updateUserPlan(userId, 'Royal'); + * invalidateUserCache(userId); + * + * // Cuando se cambia el tipo de usuario + * await updateUserType(userId, 'store_owner'); + * invalidateUserCache(userId); + */ +export function invalidateUserAuthCache(userId: string): void { + console.log(`Invalidating auth cache for user: ${userId}`); + invalidateUserCache(userId); +} + +/** + * Invalida el cache de múltiples usuarios + * + * @param userIds - Array de IDs de usuarios + */ +export function invalidateMultipleUsersCache(userIds: string[]): void { + console.log(`Invalidating auth cache for ${userIds.length} users`); + userIds.forEach((userId) => { + invalidateUserCache(userId); + }); +} diff --git a/lib/debug/auth-debug.ts b/lib/debug/auth-debug.ts deleted file mode 100644 index f9e60620..00000000 --- a/lib/debug/auth-debug.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2025 Fasttify LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NextRequest } from 'next/server'; -import outputs from '@/amplify_outputs.json'; - -/** - * Función de debugging para problemas de autenticación en producción - */ -export function debugAuthIssues(request: NextRequest) { - const isProduction = process.env.NODE_ENV === 'production'; - - console.log('debugAuthIssues called', { isProduction, path: request.nextUrl.pathname }); - - if (!isProduction) { - return null; - } - - const cookies = request.headers?.get('cookie') || ''; - const userAgent = request.headers?.get('user-agent') || ''; - const referer = request.headers?.get('referer') || ''; - - // Verificar configuración de Amplify - const amplifyConfig = { - hasAuth: !!outputs.auth, - userPoolId: outputs.auth?.user_pool_id, - region: outputs.auth?.aws_region, - clientId: outputs.auth?.user_pool_client_id, - identityPoolId: outputs.auth?.identity_pool_id, - oauthDomain: outputs.auth?.oauth?.domain, - redirectUris: outputs.auth?.oauth?.redirect_sign_in_uri, - }; - - // Verificar cookies de Cognito - const cognitoCookies = { - hasAnyCognito: cookies.includes('CognitoIdentityServiceProvider'), - hasAmplify: cookies.includes('aws-amplify'), - cookieCount: cookies.split(';').length, - cognitoPatterns: [/CognitoIdentityServiceProvider[^=]*=([^;]+)/g, /aws-amplify[^=]*=([^;]+)/g] - .map((pattern) => { - const matches = [...cookies.matchAll(pattern)]; - return matches.map((match) => match[1]); - }) - .flat(), - }; - - // Verificar headers importantes - const headers = { - host: request.headers?.get('host'), - origin: request.headers?.get('origin'), - xForwardedFor: request.headers?.get('x-forwarded-for'), - xForwardedProto: request.headers?.get('x-forwarded-proto'), - }; - - const debugInfo = { - timestamp: new Date().toISOString(), - path: request.nextUrl.pathname, - amplifyConfig, - cognitoCookies, - headers, - userAgent: userAgent.substring(0, 100), - referer: referer.substring(0, 100), - }; - - console.log('Auth Debug Info:', JSON.stringify(debugInfo, null, 2)); - - return debugInfo; -} - -/** - * Verifica si la configuración de Amplify es válida para producción - */ -export function validateAmplifyConfig() { - const requiredFields = [ - 'auth.user_pool_id', - 'auth.aws_region', - 'auth.user_pool_client_id', - 'auth.identity_pool_id', - 'auth.oauth.domain', - 'auth.oauth.redirect_sign_in_uri', - ]; - - const missingFields = requiredFields.filter((field) => { - const keys = field.split('.'); - let value: any = outputs; - for (const key of keys) { - value = value?.[key]; - } - return !value; - }); - - if (missingFields.length > 0) { - console.error('Missing required Amplify config fields:', missingFields); - return false; - } - - // Verificar que las URLs de redirección incluyan el dominio de producción - const redirectUris = outputs.auth?.oauth?.redirect_sign_in_uri || []; - const hasProductionUrl = redirectUris.some((uri) => uri.includes('fasttify.com') || uri.includes('www.fasttify.com')); - - if (!hasProductionUrl) { - console.error('No production redirect URI found in Amplify config'); - return false; - } - - return true; -} diff --git a/middleware.ts b/middleware.ts index e268f55e..e09eaae1 100644 --- a/middleware.ts +++ b/middleware.ts @@ -348,15 +348,7 @@ async function handleLoginRedirect( request: NextRequest, next: () => Promise ): Promise { - const isProduction = process.env.NODE_ENV === 'production'; - if (request.nextUrl.pathname === PROTECTED_ROUTES.LOGIN) { - if (isProduction) { - console.log('Login redirect handler called:', { - path: request.nextUrl.pathname, - timestamp: new Date().toISOString(), - }); - } return await handleAuthenticatedRedirectMiddleware(request, NextResponse.next()); } @@ -402,17 +394,6 @@ async function executeHandlers( * @returns Respuesta procesada por los middlewares */ export async function middleware(request: NextRequest): Promise { - const isProduction = process.env.NODE_ENV === 'production'; - - // Log de entrada del middleware principal - if (isProduction) { - console.log('Main middleware called:', { - path: request.nextUrl.pathname, - method: request.method, - timestamp: new Date().toISOString(), - }); - } - // Definir la cadena de handlers en orden de ejecución const handlers: MiddlewareHandler[] = [ handleOAuthProtection, diff --git a/middlewares/auth/auth.ts b/middlewares/auth/auth.ts index 9999c357..7a1979c6 100644 --- a/middlewares/auth/auth.ts +++ b/middlewares/auth/auth.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { AuthGetCurrentUserServer } from '@/utils/client/AmplifyUtils'; +import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils'; import { getLastVisitedStore } from '@/lib/cookies/last-store'; -import { debugAuthIssues, validateAmplifyConfig } from '@/lib/debug/auth-debug'; import { NextRequest, NextResponse } from 'next/server'; import NodeCache from 'node-cache'; @@ -39,6 +38,13 @@ const sessionCache = new NodeCache({ useClones: false, }); +// Cache separado para userAttributes con TTL más corto (datos críticos) +const userAttributesCache = new NodeCache({ + stdTTL: 60, // 1 minuto para userAttributes + checkperiod: 30, // Verifica cada 30 segundos + useClones: false, +}); + /** * Limpia el caché de sesiones para un usuario específico * Útil cuando se detectan problemas de autenticación @@ -48,6 +54,37 @@ export function clearUserSessionCache(request: NextRequest): void { sessionCache.del(cacheKey); } +/** + * Limpia todo el caché de sesiones + * Útil para tests + */ +export function clearAllSessionCache(): void { + sessionCache.flushAll(); + userAttributesCache.flushAll(); +} + +/** + * Invalida el cache de un usuario específico + * Útil cuando se actualiza el plan del usuario + */ +export function invalidateUserCache(userId: string): void { + const sessionKeys = sessionCache.keys(); + const attributeKeys = userAttributesCache.keys(); + + // Buscar y eliminar todas las claves de cache para este usuario + sessionKeys.forEach((key) => { + if (key.includes(userId)) { + sessionCache.del(key); + } + }); + + attributeKeys.forEach((key) => { + if (key.includes(userId)) { + userAttributesCache.del(key); + } + }); +} + function getCacheKey(request: NextRequest): string { const cookies = request.headers?.get('cookie') || ''; @@ -84,63 +121,44 @@ function getCacheKey(request: NextRequest): string { return 'no-auth'; } -export async function getSession(request: NextRequest, response: NextResponse, forceRefresh = true) { +export async function getSession(request: NextRequest, _response: NextResponse) { const cacheKey = getCacheKey(request); - const isProduction = process.env.NODE_ENV === 'production'; - - if (isProduction) { - console.log('getSession called:', { - cacheKey, - forceRefresh, - path: request.nextUrl.pathname, - }); - } - // Verificar cache si no es forceRefresh - if (!forceRefresh) { - const cached = sessionCache.get(cacheKey); - if (cached) { - if (isProduction) { - console.log('Using cached session'); - } - return cached; - } + // Verificar cache + const cached = sessionCache.get(cacheKey); + if (cached) { + return cached; } try { - if (isProduction) { - console.log('Getting current user from Cognito...'); - } - const currentUser = await AuthGetCurrentUserServer(); - if (isProduction) { - console.log('AuthGetCurrentUserServer result:', { - hasUser: !!currentUser, - username: currentUser?.username, - userId: currentUser?.userId, - signInDetails: currentUser?.signInDetails?.loginId, - }); - } - // Si no hay usuario, limpiar caché if (!currentUser) { - if (isProduction) { - console.log('No current user found, clearing cache'); - } sessionCache.del(cacheKey); + userAttributesCache.del(cacheKey); return null; } + // Verificar cache de userAttributes primero (datos críticos) + let userAttributes: Record | undefined = userAttributesCache.get(cacheKey); + if (!userAttributes) { + const fetchedAttributes = await AuthFetchUserAttributesServer(); + if (fetchedAttributes) { + userAttributes = fetchedAttributes; + userAttributesCache.set(cacheKey, userAttributes); + } + } + // Crear objeto de sesión compatible con el formato esperado const result = { tokens: { idToken: { payload: { 'cognito:username': currentUser.username, - 'custom:plan': currentUser.signInDetails?.loginId ? 'free' : undefined, - email: currentUser.signInDetails?.loginId || '', - nickname: currentUser.username, + 'custom:plan': userAttributes?.['custom:plan'] || 'free', + email: userAttributes?.email || currentUser.signInDetails?.loginId || '', + nickname: userAttributes?.nickname || currentUser.username, }, }, }, @@ -149,15 +167,12 @@ export async function getSession(request: NextRequest, response: NextResponse, f // Guardar en cache solo si la sesión es válida sessionCache.set(cacheKey, result); - if (isProduction) { - console.log('User session cached successfully'); - } - return result; } catch (error) { console.error('Error fetching user session:', error); // En producción, ser más permisivo con errores de red/temporales + const isProduction = process.env.NODE_ENV === 'production'; const isNetworkError = error instanceof Error && (error.message.includes('network') || @@ -169,7 +184,6 @@ export async function getSession(request: NextRequest, response: NextResponse, f if (isProduction && isNetworkError) { const cached = sessionCache.get(cacheKey); if (cached) { - console.log('Using cached session due to network error'); return cached; } } @@ -181,62 +195,22 @@ export async function getSession(request: NextRequest, response: NextResponse, f } export async function handleAuthenticationMiddleware(request: NextRequest, response: NextResponse) { - const isProduction = process.env.NODE_ENV === 'production'; - - // Log de entrada para debugging - if (isProduction) { - console.log('Auth middleware called:', { - path: request.nextUrl.pathname, - method: request.method, - timestamp: new Date().toISOString(), - }); - } - - // Validar configuración de Amplify en producción - if (isProduction && !validateAmplifyConfig()) { - console.error('Invalid Amplify configuration detected'); - } - const session = await getSession(request, response); if (!session) { - // Debug detallado en producción - if (isProduction) { - console.log('No session found, running debug...'); - debugAuthIssues(request); - } - // Limpiar caché cuando no hay sesión válida clearUserSessionCache(request); return NextResponse.redirect(new URL('/login', request.url), { status: 302 }); } - if (isProduction) { - console.log('Session found, continuing...'); - } - return null; // Permitir que el middleware continúe } export async function handleAuthenticatedRedirectMiddleware(request: NextRequest, response: NextResponse) { - const isProduction = process.env.NODE_ENV === 'production'; - - if (isProduction) { - console.log('Authenticated redirect middleware called:', { - path: request.nextUrl.pathname, - timestamp: new Date().toISOString(), - }); - } - - // Siempre forzar refresh para verificar la sesión actual, especialmente importante - // cuando el usuario navega manualmente a /login - const session = await getSession(request, response, true); + // Verificar la sesión actual cuando el usuario navega manualmente a /login + const session = await getSession(request, response); if (session && typeof session === 'object' && 'tokens' in session && session.tokens) { - if (isProduction) { - console.log('User has valid session, redirecting...'); - } - // Verificar que la sesión tiene tokens válidos antes de redirigir const lastStoreId = getLastVisitedStore(request); @@ -247,10 +221,6 @@ export async function handleAuthenticatedRedirectMiddleware(request: NextRequest } } - if (isProduction) { - console.log('No valid session found, allowing login page'); - } - // Si no hay sesión válida, limpiar caché y permitir continuar (mostrar login) clearUserSessionCache(request); return response; diff --git a/middlewares/ownership/collectionOwnership.ts b/middlewares/ownership/collectionOwnership.ts index 30888370..8b17f4d7 100644 --- a/middlewares/ownership/collectionOwnership.ts +++ b/middlewares/ownership/collectionOwnership.ts @@ -45,7 +45,7 @@ export async function handleCollectionOwnershipMiddleware(request: NextRequest) } // Obtener la sesión del usuario (ya validada) - const session = await getSession(request, NextResponse.next(), false); + const session = await getSession(request, NextResponse.next()); const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; diff --git a/middlewares/ownership/pagesOwnership.ts b/middlewares/ownership/pagesOwnership.ts index 4fb5acdc..ab55496e 100644 --- a/middlewares/ownership/pagesOwnership.ts +++ b/middlewares/ownership/pagesOwnership.ts @@ -45,7 +45,7 @@ export async function handlePagesOwnershipMiddleware(request: NextRequest) { } // Obtener la sesión del usuario (ya validada) - const session = await getSession(request, NextResponse.next(), false); + const session = await getSession(request, NextResponse.next()); const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; diff --git a/middlewares/ownership/productOwnership.ts b/middlewares/ownership/productOwnership.ts index 48843429..2c053c60 100644 --- a/middlewares/ownership/productOwnership.ts +++ b/middlewares/ownership/productOwnership.ts @@ -45,7 +45,7 @@ export async function handleProductOwnershipMiddleware(request: NextRequest) { } // Obtener la sesión del usuario (ya validada) - const session = await getSession(request, NextResponse.next(), false); + const session = await getSession(request, NextResponse.next()); const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; diff --git a/middlewares/store-access/store.ts b/middlewares/store-access/store.ts index 7f3e15f1..a78e5fa7 100644 --- a/middlewares/store-access/store.ts +++ b/middlewares/store-access/store.ts @@ -60,8 +60,7 @@ export async function handleStoreMiddleware(request: NextRequest, response: Next return authResponse; // Si hay redirección de auth, retornarla } - // Obtener la sesión del usuario (ya validada) - sin refresh para rutas específicas - const session = await getSession(request, response, false); + const session = await getSession(request, response); const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; const userPlan = (session as AuthSession).tokens?.idToken?.payload?.['custom:plan']; diff --git a/middlewares/store-access/storeAccess.ts b/middlewares/store-access/storeAccess.ts index 761013fa..c2042ef3 100644 --- a/middlewares/store-access/storeAccess.ts +++ b/middlewares/store-access/storeAccess.ts @@ -39,7 +39,7 @@ export async function handleStoreAccessMiddleware(request: NextRequest) { } // Obtener la sesión del usuario (ya validada) - const session = await getSession(request, NextResponse.next(), false); + const session = await getSession(request, NextResponse.next()); // Obtener el ID del usuario y plan desde la sesión const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username']; diff --git a/middlewares/subscription/subscription.ts b/middlewares/subscription/subscription.ts index acc89e5d..356d9d4c 100644 --- a/middlewares/subscription/subscription.ts +++ b/middlewares/subscription/subscription.ts @@ -25,7 +25,7 @@ export async function handleSubscriptionMiddleware(request: NextRequest, respons } // Obtener la sesión del usuario (ya validada) - const session = await getSession(request, response, false); + const session = await getSession(request, response); const userPlan: string | undefined = (session as AuthSession).tokens?.idToken?.payload?.['custom:plan'] as | string diff --git a/test/unit/integration/auth-flow.test.ts b/test/unit/integration/auth-flow.test.ts index 7b68ae0f..a91d8dda 100644 --- a/test/unit/integration/auth-flow.test.ts +++ b/test/unit/integration/auth-flow.test.ts @@ -1,14 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getSession, type AuthSession } from '@/middlewares/auth/auth'; -import { runWithAmplifyServerContext } from '@/utils/client/AmplifyUtils'; -import { fetchAuthSession } from 'aws-amplify/auth/server'; - -jest.mock('aws-amplify/auth/server', () => ({ - fetchAuthSession: jest.fn(), -})); +import { getSession, type AuthSession, clearAllSessionCache } from '@/middlewares/auth/auth'; +import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils'; jest.mock('@/utils/client/AmplifyUtils', () => ({ - runWithAmplifyServerContext: jest.fn(), + AuthGetCurrentUserServer: jest.fn(), + AuthFetchUserAttributesServer: jest.fn(), })); jest.mock('next/server', () => ({ @@ -32,164 +28,117 @@ describe('Auth Flow Integration Tests', () => { let mockRequest: NextRequest; let mockResponse: NextResponse; - const mockSession: AuthSession = { + const mockUser = { + username: 'test-user-123', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', + }, + }; + + const mockUserAttributes = { + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', + }; + + const expectedSession: AuthSession = { tokens: { - idToken: { payload: { 'cognito:username': 'test-user-123', 'custom:plan': 'Royal' } }, + idToken: { + payload: { + 'cognito:username': 'test-user-123', + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', + }, + }, }, }; beforeEach(() => { jest.clearAllMocks(); jest.resetModules(); + clearAllSessionCache(); mockRequest = { - url: 'https://test-store.fasttify.com/admin', - nextUrl: { - pathname: '/admin', - }, + url: 'http://localhost:3000/test', headers: { - get: jest.fn().mockImplementation((header) => { - if (header === 'cookie') { - return 'CognitoIdentityServiceProvider.us-east-1.test123.userId=test-user-123; other-cookie=value'; - } - return null; - }), + get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser'), }, } as unknown as NextRequest; - mockResponse = NextResponse.next(); + mockResponse = {} as NextResponse; - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock; + + mockAuthGetCurrentUserServer.mockResolvedValue(mockUser); + mockAuthFetchUserAttributesServer.mockResolvedValue(mockUserAttributes); }); describe('Cache entre múltiples middlewares', () => { it('debe usar cache entre múltiples middlewares en el mismo flujo', async () => { - // Simular flujo: auth middleware → storeAccess → store - - // Primera llamada a getSession (auth middleware) - const authSession = await getSession(mockRequest, mockResponse, false); + const authSession = await getSession(mockRequest, mockResponse); + const storeAccessSession = await getSession(mockRequest, mockResponse); + const storeSession = await getSession(mockRequest, mockResponse); - // Segunda llamada a getSession (storeAccess middleware) - const storeAccessSession = await getSession(mockRequest, mockResponse, false); + expect(authSession).toEqual(expectedSession); + expect(storeAccessSession).toEqual(expectedSession); + expect(storeSession).toEqual(expectedSession); - // Tercera llamada a getSession (store middleware) - const storeSession = await getSession(mockRequest, mockResponse, false); - - // Todas las sesiones deberían ser iguales - expect(authSession).toEqual(mockSession); - expect(storeAccessSession).toEqual(mockSession); - expect(storeSession).toEqual(mockSession); - - // En un cache real, solo debería llamar fetchAuthSession una vez - // Pero como nuestro mock no implementa cache real, verificamos que todas funcionan - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); - }); - }); - - describe('Manejo de token revocado en el flujo', () => { - it('debe manejar token revocado correctamente en el flujo', async () => { - const revokedError = new Error('Refresh Token has been revoked'); - revokedError.name = 'NotAuthorizedException'; - - // Mock para que fetchAuthSession falle con token revocado - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockRejectedValueOnce(revokedError); - return await operation({}); - }); - - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - - // getSession debería retornar null cuando el token está revocado - const authSession = await getSession(mockRequest, mockResponse, false); - - expect(authSession).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching user session:', expect.any(Error)); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe('ForceRefresh en middlewares específicos', () => { - it('debe usar forceRefresh cuando se especifica', async () => { - // Primera llamada sin forceRefresh - await getSession(mockRequest, mockResponse, false); - - // Segunda llamada con forceRefresh - await getSession(mockRequest, mockResponse, true); - - // Verificar que se llamó con forceRefresh: true - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: true }); + expect(AuthGetCurrentUserServer).toHaveBeenCalled(); + expect(AuthFetchUserAttributesServer).toHaveBeenCalled(); }); }); describe('Reducción de llamadas a Cognito', () => { it('debe ejecutar múltiples middlewares en paralelo sin duplicar llamadas', async () => { - // Ejecutar múltiples llamadas a getSession en paralelo - const promises = [ - getSession(mockRequest, mockResponse, false), - getSession(mockRequest, mockResponse, false), - getSession(mockRequest, mockResponse, false), - ]; - + const promises = Array(3) + .fill(null) + .map(() => getSession(mockRequest, mockResponse)); const results = await Promise.all(promises); // Todas las llamadas deberían retornar la misma sesión results.forEach((result) => { - expect(result).toEqual(mockSession); + expect(result).toEqual(expectedSession); }); - // En un cache real, solo debería llamar fetchAuthSession una vez - // Pero como nuestro mock no implementa cache real, verificamos que todas funcionan - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); + // Verificar que AuthGetCurrentUserServer fue llamado + expect(AuthGetCurrentUserServer).toHaveBeenCalled(); + expect(AuthFetchUserAttributesServer).toHaveBeenCalled(); }); }); describe('Consistencia de datos entre middlewares', () => { it('debe mantener consistencia de datos entre middlewares', async () => { - // Simular múltiples middlewares que acceden a la misma sesión - const session1: AuthSession | null = await getSession(mockRequest, mockResponse, false); - const session2: AuthSession | null = await getSession(mockRequest, mockResponse, false); - const session3: AuthSession | null = await getSession(mockRequest, mockResponse, false); - - // Verificar que todos los middlewares ven los mismos datos - expect(session1).toEqual(session2); - expect(session2).toEqual(session3); + const session1 = await getSession(mockRequest, mockResponse); + const session2 = await getSession(mockRequest, mockResponse); // Verificar datos específicos del usuario - expect(session1?.tokens?.idToken?.payload?.['cognito:username']).toBe('test-user-123'); - expect(session1?.tokens?.idToken?.payload?.['custom:plan']).toBe('Royal'); - expect(session2?.tokens?.idToken?.payload?.['cognito:username']).toBe('test-user-123'); - expect(session2?.tokens?.idToken?.payload?.['custom:plan']).toBe('Royal'); + expect((session1 as AuthSession)?.tokens?.idToken?.payload?.['cognito:username']).toBe('test-user-123'); + expect((session1 as AuthSession)?.tokens?.idToken?.payload?.['custom:plan']).toBe('Royal'); + expect((session2 as AuthSession)?.tokens?.idToken?.payload?.['cognito:username']).toBe('test-user-123'); + expect((session2 as AuthSession)?.tokens?.idToken?.payload?.['custom:plan']).toBe('Royal'); }); }); describe('Diferentes formatos de cookies Cognito', () => { it('debe manejar diferentes regiones de Cognito correctamente', async () => { - const regions = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; + const regions = ['us-east-1', 'us-west-2', 'eu-west-1']; for (const region of regions) { - const cookies = `CognitoIdentityServiceProvider.${region}.abc123.userId=test-user-${region}; other=cookie`; - - const request = { - url: 'https://test-store.fasttify.com/admin', - nextUrl: { pathname: '/admin' }, + const regionRequest = { + ...mockRequest, headers: { - get: jest.fn().mockImplementation((header) => { - if (header === 'cookie') return cookies; - return null; - }), + get: jest.fn().mockReturnValue(`CognitoIdentityServiceProvider.${region}.abc123=testuser`), }, } as unknown as NextRequest; - const session = await getSession(request, mockResponse, false); + const session = await getSession(regionRequest, mockResponse); - expect(session).toEqual(mockSession); - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); + expect(session).toEqual(expectedSession); + expect(AuthGetCurrentUserServer).toHaveBeenCalled(); + expect(AuthFetchUserAttributesServer).toHaveBeenCalled(); } }); }); diff --git a/test/unit/middlewares/auth-cache.test.ts b/test/unit/middlewares/auth-cache.test.ts index 820ff44c..db8b74d2 100644 --- a/test/unit/middlewares/auth-cache.test.ts +++ b/test/unit/middlewares/auth-cache.test.ts @@ -1,3 +1,8 @@ +import { getSession, clearAllSessionCache } from '@/middlewares/auth/auth'; +import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils'; +import { NextRequest, NextResponse } from 'next/server'; + +// Mock de NodeCache jest.mock('node-cache', () => { return jest.fn().mockImplementation(() => ({ get: jest.fn(), @@ -7,12 +12,9 @@ jest.mock('node-cache', () => { })); }); -jest.mock('aws-amplify/auth/server', () => ({ - fetchAuthSession: jest.fn(), -})); - jest.mock('@/utils/client/AmplifyUtils', () => ({ - runWithAmplifyServerContext: jest.fn(), + AuthGetCurrentUserServer: jest.fn(), + AuthFetchUserAttributesServer: jest.fn(), })); jest.mock('next/server', () => ({ @@ -23,178 +25,80 @@ jest.mock('next/server', () => ({ }, })); -import { getSession } from '@/middlewares/auth/auth'; -import { runWithAmplifyServerContext } from '@/utils/client/AmplifyUtils'; -import { fetchAuthSession } from 'aws-amplify/auth/server'; -import { NextRequest, NextResponse } from 'next/server'; - describe('getSession with Caching', () => { let mockRequest: NextRequest; let mockResponse: NextResponse; - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, - idToken: { payload: { 'cognito:username': 'testuser', 'custom:plan': 'Basic' } }, + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, }; - beforeEach(() => { - jest.clearAllMocks(); - - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); - }); - - describe('Fetch y cache de sesión nueva', () => { - it('debe fetchear una nueva sesión y cachearla si no es force refresh y no hay cache hit', async () => { - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), - }, - } as unknown as NextRequest; - - mockResponse = {} as NextResponse; - - const result = await getSession(mockRequest, mockResponse, false); - - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); - expect(result).toEqual(mockSession); - }); - }); - - describe('Retorno de sesión cacheada', () => { - it('debe retornar sesión del cache si no es force refresh y hay cache hit', async () => { - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), - }, - } as unknown as NextRequest; - - mockResponse = {} as NextResponse; - - // Primera llamada - fetchea y cachea - const result1 = await getSession(mockRequest, mockResponse, false); - - // Limpiar mocks para la segunda llamada - jest.clearAllMocks(); - - // Configurar mock para la segunda llamada - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); - - // Segunda llamada - debería usar cache - const result2 = await getSession(mockRequest, mockResponse, false); - - // En un cache real, solo debería llamar fetchAuthSession una vez - // Pero como nuestro mock no implementa cache real, verificamos que ambas llamadas funcionan - expect(result1).toEqual(mockSession); - expect(result2).toEqual(mockSession); - }); - }); - - describe('Force refresh ignora cache', () => { - it('debe fetchear nueva sesión si force refresh es true', async () => { - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), - }, - } as unknown as NextRequest; - - mockResponse = {} as NextResponse; - - const result = await getSession(mockRequest, mockResponse, true); - - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: true }); - expect(result).toEqual(mockSession); - }); - }); - - describe('Generación de cache key estable', () => { - it('debe usar la misma cache key para las mismas cookies de Cognito', async () => { - const cookies = 'CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'; + const mockUserAttributes = { + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', + }; - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue(cookies), + const expectedSession = { + tokens: { + idToken: { + payload: { + 'cognito:username': 'testuser', + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', }, - } as unknown as NextRequest; - - mockResponse = {} as NextResponse; + }, + }, + }; - // Primera llamada - await getSession(mockRequest, mockResponse, false); + beforeEach(() => { + jest.clearAllMocks(); + clearAllSessionCache(); - // Segunda llamada con las mismas cookies - await getSession(mockRequest, mockResponse, false); + mockRequest = { + url: 'http://localhost:3000/test', + headers: { + get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), + }, + } as unknown as NextRequest; - // Ambas llamadas deberían usar la misma key de cache (user-testuser) - // Verificamos que fetchAuthSession fue llamado con forceRefresh: false en ambos casos - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); - }); + mockResponse = {} as NextResponse; }); - describe('Manejo de errores en fetch', () => { - it('debe retornar null si fetchAuthSession falla', async () => { - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), - }, - } as unknown as NextRequest; - - mockResponse = {} as NextResponse; + describe('Funcionalidad básica', () => { + it('debe fetchear una nueva sesión correctamente', async () => { + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock; - // Mock para que fetchAuthSession falle - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockRejectedValueOnce(new Error('Auth error')); - return await operation({}); - }); + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); + mockAuthFetchUserAttributesServer.mockResolvedValueOnce(mockUserAttributes); - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const result = await getSession(mockRequest, mockResponse); - const result = await getSession(mockRequest, mockResponse, false); - - expect(result).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching user session:', expect.any(Error)); - - consoleErrorSpy.mockRestore(); + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); + expect(mockAuthFetchUserAttributesServer).toHaveBeenCalled(); + expect(result).toEqual(expectedSession); }); - }); - - describe('No cachear sesiones inválidas', () => { - it('debe retornar null y no cachear si tokens es undefined', async () => { - mockRequest = { - url: 'http://localhost:3000/test', - headers: { - get: jest.fn().mockReturnValue('CognitoIdentityServiceProvider.abc123def456=testuser; other=cookie'), - }, - } as unknown as NextRequest; - mockResponse = {} as NextResponse; + it('debe retornar null cuando no hay usuario autenticado', async () => { + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock; - // Mock para que fetchAuthSession retorne sesión sin tokens - (runWithAmplifyServerContext as jest.Mock).mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce({ tokens: undefined }); - return await operation({}); - }); + mockAuthGetCurrentUserServer.mockResolvedValueOnce(null); + // No se llama a AuthFetchUserAttributesServer cuando no hay usuario + mockAuthFetchUserAttributesServer.mockResolvedValueOnce(null); - const result = await getSession(mockRequest, mockResponse, false); + const result = await getSession(mockRequest, mockResponse); + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); + // AuthFetchUserAttributesServer no se llama cuando no hay usuario + expect(mockAuthFetchUserAttributesServer).not.toHaveBeenCalled(); expect(result).toBeNull(); - expect(fetchAuthSession).toHaveBeenCalledWith(expect.any(Object), { forceRefresh: false }); }); }); }); diff --git a/test/unit/middlewares/auth.test.ts b/test/unit/middlewares/auth.test.ts index 6dbe2cdb..8a5c7092 100644 --- a/test/unit/middlewares/auth.test.ts +++ b/test/unit/middlewares/auth.test.ts @@ -1,20 +1,16 @@ import { getSession, - type AuthSession, handleAuthenticatedRedirectMiddleware, handleAuthenticationMiddleware, + clearAllSessionCache, } from '@/middlewares/auth/auth'; -import { runWithAmplifyServerContext } from '@/utils/client/AmplifyUtils'; -import { fetchAuthSession } from 'aws-amplify/auth/server'; +import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils'; import { NextRequest, NextResponse } from 'next/server'; // Mock de los módulos externos -jest.mock('aws-amplify/auth/server', () => ({ - fetchAuthSession: jest.fn(), -})); - jest.mock('@/utils/client/AmplifyUtils', () => ({ - runWithAmplifyServerContext: jest.fn(), + AuthGetCurrentUserServer: jest.fn(), + AuthFetchUserAttributesServer: jest.fn(), })); jest.mock('next/server', () => ({ @@ -38,100 +34,106 @@ describe('Auth Middleware', () => { beforeEach(() => { jest.clearAllMocks(); jest.resetModules(); + + // Limpiar caché entre tests + clearAllSessionCache(); }); describe('getSession', () => { it('should return session when user is authenticated', async () => { - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, - idToken: { toString: () => 'mock-id-token' }, + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, - } as AuthSession; + }; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockUserAttributes = { + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', + }; + + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock; + + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); + mockAuthFetchUserAttributesServer.mockResolvedValueOnce(mockUserAttributes); const result = await getSession(mockRequest, mockResponse); - expect(mockRunWithAmplifyServerContext).toHaveBeenCalledWith({ - nextServerContext: { request: mockRequest, response: mockResponse }, - operation: expect.any(Function), + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); + expect(mockAuthFetchUserAttributesServer).toHaveBeenCalled(); + expect(result).toEqual({ + tokens: { + idToken: { + payload: { + 'cognito:username': 'testuser', + 'custom:plan': 'Royal', + email: 'test@example.com', + nickname: 'Test User', + }, + }, + }, }); - expect(result).toEqual(mockSession); }); it('should return null when user is not authenticated', async () => { - const mockSession = { tokens: undefined }; - - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(null); const result = await getSession(mockRequest, mockResponse); + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); expect(result).toBeNull(); }); - it('should return null when fetchAuthSession throws an error', async () => { + it('should return null when AuthGetCurrentUserServer throws an error', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockRejectedValueOnce(new Error('Auth error')); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockRejectedValueOnce(new Error('Auth error')); const result = await getSession(mockRequest, mockResponse); + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); expect(result).toBeNull(); expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching user session:', expect.any(Error)); consoleErrorSpy.mockRestore(); }); - it('should call fetchAuthSession with forceRefresh: true', async () => { - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, + it('should call AuthGetCurrentUserServer', async () => { + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, }; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({ contextSpec: 'mock-context' }); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); await getSession(mockRequest, mockResponse); - expect(fetchAuthSession).toHaveBeenCalledWith({ contextSpec: 'mock-context' }, { forceRefresh: true }); + expect(mockAuthGetCurrentUserServer).toHaveBeenCalled(); }); }); describe('handleAuthenticationMiddleware', () => { it('should return the original response when user is authenticated', async () => { - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, }; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); const result = await handleAuthenticationMiddleware(mockRequest, mockResponse); @@ -142,12 +144,8 @@ describe('Auth Middleware', () => { it('should redirect to login when user is not authenticated', async () => { const mockRedirectResponse = { type: 'redirect', url: '/login' }; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce({ tokens: undefined }); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(null); const mockNextResponseRedirect = NextResponse.redirect as jest.Mock; mockNextResponseRedirect.mockReturnValueOnce(mockRedirectResponse); @@ -163,12 +161,8 @@ describe('Auth Middleware', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockRejectedValueOnce(new Error('Network error')); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockRejectedValueOnce(new Error('Network error')); const mockNextResponseRedirect = NextResponse.redirect as jest.Mock; mockNextResponseRedirect.mockReturnValueOnce(mockRedirectResponse); @@ -185,9 +179,11 @@ describe('Auth Middleware', () => { describe('handleAuthenticatedRedirectMiddleware', () => { it('should redirect to home when user is already authenticated', async () => { const mockRedirectResponse = { type: 'redirect', url: '/' }; - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, }; @@ -199,12 +195,8 @@ describe('Auth Middleware', () => { }, } as unknown as NextRequest; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); const mockNextResponseRedirect = NextResponse.redirect as jest.Mock; mockNextResponseRedirect.mockReturnValueOnce(mockRedirectResponse); @@ -236,12 +228,8 @@ describe('Auth Middleware', () => { url: 'https://example.com/custom-path', } as NextRequest; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce({ tokens: undefined }); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(null); await handleAuthenticationMiddleware(customRequest, mockResponse); @@ -256,18 +244,16 @@ describe('Auth Middleware', () => { }, } as unknown as NextRequest; - const mockSession = { - tokens: { - accessToken: { toString: () => 'mock-access-token' }, + const mockUser = { + username: 'testuser', + userId: 'test-user-123', + signInDetails: { + loginId: 'test@example.com', }, }; - const mockRunWithAmplifyServerContext = runWithAmplifyServerContext as jest.Mock; - mockRunWithAmplifyServerContext.mockImplementation(async ({ operation }) => { - const mockFetchAuthSession = fetchAuthSession as jest.Mock; - mockFetchAuthSession.mockResolvedValueOnce(mockSession); - return await operation({}); - }); + const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock; + mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser); await handleAuthenticatedRedirectMiddleware(customRequest, mockResponse); diff --git a/utils/client/AmplifyUtils.ts b/utils/client/AmplifyUtils.ts index 593b4357..2bbc7560 100644 --- a/utils/client/AmplifyUtils.ts +++ b/utils/client/AmplifyUtils.ts @@ -17,7 +17,7 @@ import { type StoreSchema } from '@/data-schema'; import outputs from '@/amplify_outputs.json'; import { createServerRunner } from '@aws-amplify/adapter-nextjs'; import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/api'; -import { getCurrentUser } from 'aws-amplify/auth/server'; +import { getCurrentUser, fetchUserAttributes, fetchAuthSession } from 'aws-amplify/auth/server'; import { cookies } from 'next/headers'; export const { runWithAmplifyServerContext } = createServerRunner({ @@ -41,3 +41,29 @@ export async function AuthGetCurrentUserServer() { console.error(error); } } + +export async function AuthFetchUserAttributesServer() { + try { + const userAttributes = await runWithAmplifyServerContext({ + nextServerContext: { cookies }, + operation: (contextSpec) => fetchUserAttributes(contextSpec), + }); + return userAttributes; + } catch (error) { + console.error('Error fetching user attributes:', error); + return null; + } +} + +export async function AuthFetchAuthSessionServer() { + try { + const authSession = await runWithAmplifyServerContext({ + nextServerContext: { cookies }, + operation: (contextSpec) => fetchAuthSession(contextSpec), + }); + return authSession; + } catch (error) { + console.error('Error fetching auth session:', error); + return null; + } +}