Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 71 additions & 68 deletions components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
'use client';

import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from 'react-hook-form';

import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
Expand Down Expand Up @@ -33,8 +43,8 @@ const FormField = <
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();

const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);

if (!fieldContext) {
Expand All @@ -59,77 +69,70 @@ type FormItemContextValue = {

const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);

const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId();

return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
}
);
FormItem.displayName = 'FormItem';
return (
<FormItemContext.Provider value={{ id }}>
<div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
</FormItemContext.Provider>
);
}

const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();

return <Label ref={ref} className={cn(error && 'text-destructive', className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = 'FormLabel';

const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();

return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
);
FormControl.displayName = 'FormControl';
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
}

const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();

return (
<p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
);
}
);
FormDescription.displayName = 'FormDescription';

const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;

if (!body) {
return null;
}

return (
<p
ref={ref}
id={formMessageId}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}>
{body}
</p>
);
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}

function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField();

return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}

function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;

if (!body) {
return null;
}
);
FormMessage.displayName = 'FormMessage';

return (
<p data-slot="form-message" id={formMessageId} className={cn('text-destructive text-sm', className)} {...props}>
{body}
</p>
);
}

export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
9 changes: 7 additions & 2 deletions middlewares/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
* limitations under the License.
*/

import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils';
import {
AuthGetCurrentUserServer,
AuthFetchUserAttributesServer,
AuthFetchAuthSessionServer,
} from '@/utils/client/AmplifyUtils';
import { getLastVisitedStore } from '@/lib/cookies/last-store';
import { NextRequest, NextResponse } from 'next/server';
import NodeCache from 'node-cache';
Expand Down Expand Up @@ -131,10 +135,11 @@ export async function getSession(request: NextRequest, _response: NextResponse)
}

try {
const currentSession = await AuthFetchAuthSessionServer();
const currentUser = await AuthGetCurrentUserServer();

// Si no hay usuario, limpiar caché
if (!currentUser) {
if (!currentSession || !currentUser) {
sessionCache.del(cacheKey);
userAttributesCache.del(cacheKey);
return null;
Expand Down
24 changes: 22 additions & 2 deletions test/unit/integration/auth-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSession, type AuthSession, clearAllSessionCache } from '@/middlewares/auth/auth';
import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils';
import {
AuthGetCurrentUserServer,
AuthFetchUserAttributesServer,
AuthFetchAuthSessionServer,
} from '@/utils/client/AmplifyUtils';

jest.mock('@/utils/client/AmplifyUtils', () => ({
AuthGetCurrentUserServer: jest.fn(),
AuthFetchUserAttributesServer: jest.fn(),
AuthFetchAuthSessionServer: jest.fn(),
}));

jest.mock('next/server', () => ({
Expand Down Expand Up @@ -71,7 +76,19 @@ describe('Auth Flow Integration Tests', () => {

const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;

const mockAuthSession = {
tokens: {
idToken: {
payload: {
'cognito:username': 'test-user-123',
},
},
},
};

mockAuthFetchAuthSessionServer.mockResolvedValue(mockAuthSession);
mockAuthGetCurrentUserServer.mockResolvedValue(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValue(mockUserAttributes);
});
Expand All @@ -86,6 +103,7 @@ describe('Auth Flow Integration Tests', () => {
expect(storeAccessSession).toEqual(expectedSession);
expect(storeSession).toEqual(expectedSession);

expect(AuthFetchAuthSessionServer).toHaveBeenCalled();
expect(AuthGetCurrentUserServer).toHaveBeenCalled();
expect(AuthFetchUserAttributesServer).toHaveBeenCalled();
});
Expand All @@ -103,7 +121,8 @@ describe('Auth Flow Integration Tests', () => {
expect(result).toEqual(expectedSession);
});

// Verificar que AuthGetCurrentUserServer fue llamado
// Verificar que las funciones fueron llamadas
expect(AuthFetchAuthSessionServer).toHaveBeenCalled();
expect(AuthGetCurrentUserServer).toHaveBeenCalled();
expect(AuthFetchUserAttributesServer).toHaveBeenCalled();
});
Expand Down Expand Up @@ -137,6 +156,7 @@ describe('Auth Flow Integration Tests', () => {
const session = await getSession(regionRequest, mockResponse);

expect(session).toEqual(expectedSession);
expect(AuthFetchAuthSessionServer).toHaveBeenCalled();
expect(AuthGetCurrentUserServer).toHaveBeenCalled();
expect(AuthFetchUserAttributesServer).toHaveBeenCalled();
}
Expand Down
23 changes: 22 additions & 1 deletion test/unit/middlewares/auth-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { getSession, clearAllSessionCache } from '@/middlewares/auth/auth';
import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils';
import {
AuthGetCurrentUserServer,
AuthFetchUserAttributesServer,
AuthFetchAuthSessionServer,
} from '@/utils/client/AmplifyUtils';
import { NextRequest, NextResponse } from 'next/server';

// Mock de NodeCache
Expand All @@ -15,6 +19,7 @@ jest.mock('node-cache', () => {
jest.mock('@/utils/client/AmplifyUtils', () => ({
AuthGetCurrentUserServer: jest.fn(),
AuthFetchUserAttributesServer: jest.fn(),
AuthFetchAuthSessionServer: jest.fn(),
}));

jest.mock('next/server', () => ({
Expand Down Expand Up @@ -74,12 +79,25 @@ describe('getSession with Caching', () => {
it('debe fetchear una nueva sesión correctamente', async () => {
const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;

const mockAuthSession = {
tokens: {
idToken: {
payload: {
'cognito:username': 'testuser',
},
},
},
};

mockAuthFetchAuthSessionServer.mockResolvedValueOnce(mockAuthSession);
mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValueOnce(mockUserAttributes);

const result = await getSession(mockRequest, mockResponse);

expect(mockAuthFetchAuthSessionServer).toHaveBeenCalled();
expect(mockAuthGetCurrentUserServer).toHaveBeenCalled();
expect(mockAuthFetchUserAttributesServer).toHaveBeenCalled();
expect(result).toEqual(expectedSession);
Expand All @@ -88,13 +106,16 @@ describe('getSession with Caching', () => {
it('debe retornar null cuando no hay usuario autenticado', async () => {
const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;

mockAuthFetchAuthSessionServer.mockResolvedValueOnce(null);
mockAuthGetCurrentUserServer.mockResolvedValueOnce(null);
// No se llama a AuthFetchUserAttributesServer cuando no hay usuario
mockAuthFetchUserAttributesServer.mockResolvedValueOnce(null);

const result = await getSession(mockRequest, mockResponse);

expect(mockAuthFetchAuthSessionServer).toHaveBeenCalled();
expect(mockAuthGetCurrentUserServer).toHaveBeenCalled();
// AuthFetchUserAttributesServer no se llama cuando no hay usuario
expect(mockAuthFetchUserAttributesServer).not.toHaveBeenCalled();
Expand Down
29 changes: 28 additions & 1 deletion test/unit/middlewares/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import {
handleAuthenticationMiddleware,
clearAllSessionCache,
} from '@/middlewares/auth/auth';
import { AuthGetCurrentUserServer, AuthFetchUserAttributesServer } from '@/utils/client/AmplifyUtils';
import {
AuthGetCurrentUserServer,
AuthFetchUserAttributesServer,
AuthFetchAuthSessionServer,
} from '@/utils/client/AmplifyUtils';
import { NextRequest, NextResponse } from 'next/server';

// Mock de los módulos externos
jest.mock('@/utils/client/AmplifyUtils', () => ({
AuthGetCurrentUserServer: jest.fn(),
AuthFetchUserAttributesServer: jest.fn(),
AuthFetchAuthSessionServer: jest.fn(),
}));

jest.mock('next/server', () => ({
Expand Down Expand Up @@ -57,12 +62,16 @@ describe('Auth Middleware', () => {

const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;

// Mock AuthFetchAuthSessionServer para que retorne una sesión válida
mockAuthFetchAuthSessionServer.mockResolvedValueOnce({ tokens: { idToken: { payload: {} } } });
mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValueOnce(mockUserAttributes);

const result = await getSession(mockRequest, mockResponse);

expect(mockAuthFetchAuthSessionServer).toHaveBeenCalled();
expect(mockAuthGetCurrentUserServer).toHaveBeenCalled();
expect(mockAuthFetchUserAttributesServer).toHaveBeenCalled();
expect(result).toEqual({
Expand Down Expand Up @@ -133,7 +142,13 @@ describe('Auth Middleware', () => {
};

const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;

// Mock AuthFetchAuthSessionServer para que retorne una sesión válida
mockAuthFetchAuthSessionServer.mockResolvedValueOnce({ tokens: { idToken: { payload: {} } } });
mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValueOnce({});

const result = await handleAuthenticationMiddleware(mockRequest, mockResponse);

Expand Down Expand Up @@ -196,7 +211,13 @@ describe('Auth Middleware', () => {
} as unknown as NextRequest;

const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;

// Mock AuthFetchAuthSessionServer para que retorne una sesión válida
mockAuthFetchAuthSessionServer.mockResolvedValueOnce({ tokens: { idToken: { payload: {} } } });
mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValueOnce({});

const mockNextResponseRedirect = NextResponse.redirect as jest.Mock;
mockNextResponseRedirect.mockReturnValueOnce(mockRedirectResponse);
Expand Down Expand Up @@ -253,7 +274,13 @@ describe('Auth Middleware', () => {
};

const mockAuthGetCurrentUserServer = AuthGetCurrentUserServer as jest.Mock;
const mockAuthFetchAuthSessionServer = AuthFetchAuthSessionServer as jest.Mock;
const mockAuthFetchUserAttributesServer = AuthFetchUserAttributesServer as jest.Mock;

// Mock AuthFetchAuthSessionServer para que retorne una sesión válida
mockAuthFetchAuthSessionServer.mockResolvedValueOnce({ tokens: { idToken: { payload: {} } } });
mockAuthGetCurrentUserServer.mockResolvedValueOnce(mockUser);
mockAuthFetchUserAttributesServer.mockResolvedValueOnce({});

await handleAuthenticatedRedirectMiddleware(customRequest, mockResponse);

Expand Down