Solucionados los problemas al hacer login.

This commit is contained in:
David Arranz 2026-09-16 16:34:03 +02:00
parent de00a87c7b
commit 612355bcec
7 changed files with 178 additions and 6 deletions

View File

@ -4,10 +4,13 @@ export default {
preset: "ts-jest",
testEnvironment: "jest-environment-jsdom",
transform: {
"^.+\\.tsx?$": "ts-jest",
"^.+\\.tsx?$": ["ts-jest", { tsconfig: { esModuleInterop: true } }],
// process `*.tsx` files with `ts-jest`
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"^@shared/(.*)$": "<rootDir>/../shared/lib/$1",
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"\\.(gif|ttf|eot|svg|png)$": "<rootDir>/test/__ mocks __/fileMock.js",
},
};

View File

@ -0,0 +1,100 @@
import { act } from "react-dom/test-utils";
import { render as renderDom } from "react-dom";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { ProtectedRoute } from "./ProtectedRoute";
const useIsLoggedIn = jest.fn();
const useGetProfile = jest.fn();
jest.mock("@/lib/hooks", () => ({
useIsLoggedIn: () => useIsLoggedIn(),
useGetProfile: () => useGetProfile(),
}));
jest.mock("@/components", () => ({
ErrorOverlay: ({ subtitle }: { subtitle: string }) => <div data-testid='error'>{subtitle}</div>,
LoadingOverlay: () => <div data-testid='loading' />,
}));
const renderProtectedRoute = () => {
const container = document.createElement("div");
document.body.appendChild(container);
act(() => {
renderDom(
<MemoryRouter initialEntries={["/quotes"]}>
<Routes>
<Route
path='/quotes'
element={
<ProtectedRoute>
<div data-testid='quotes'>Quotes</div>
</ProtectedRoute>
}
/>
<Route path='/login' element={<div data-testid='login'>Login</div>} />
</Routes>
</MemoryRouter>,
container
);
});
return container;
};
describe("ProtectedRoute", () => {
afterEach(() => {
document.body.innerHTML = "";
});
beforeEach(() => {
useIsLoggedIn.mockReturnValue({
isSuccess: true,
isLoading: false,
data: { authenticated: true },
});
});
it("waits for a refetch when the only cached profile is null", () => {
useGetProfile.mockReturnValue({
isSuccess: true,
isLoading: false,
isFetching: true,
data: null,
});
const container = renderProtectedRoute();
expect(container.querySelector("[data-testid='loading']")).not.toBeNull();
expect(container.querySelector("[data-testid='login']")).toBeNull();
});
it("renders the protected route after resolving a valid profile", () => {
useGetProfile.mockReturnValue({
isSuccess: true,
isLoading: false,
isFetching: false,
data: { id: "current-user", lang_code: "es" },
});
const container = renderProtectedRoute();
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
});
it("renders an error instead of treating a profile request failure as a logout", () => {
useGetProfile.mockReturnValue({
isSuccess: false,
isLoading: false,
isFetching: false,
error: new Error("Profile unavailable"),
});
const container = renderProtectedRoute();
expect(container.querySelector("[data-testid='error']")?.textContent).toContain(
"Profile unavailable"
);
expect(container.querySelector("[data-testid='login']")).toBeNull();
});
});

View File

@ -21,6 +21,7 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
const {
isSuccess: isProfileSuccess,
isLoading: isProfileLoading,
isFetching: isProfileFetching,
error: profileError,
data: profile,
} = useGetProfile({
@ -34,7 +35,7 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
}
}, [isProfileSuccess, profile?.lang_code, i18n]);*/
if (isLoggedInLoading || isProfileLoading) {
if (isLoggedInLoading || isProfileLoading || (isProfileSuccess && profile === null && isProfileFetching)) {
return <LoadingOverlay />;
}

View File

@ -0,0 +1,16 @@
import { createAxiosAuthActions } from "./createAxiosAuthActions";
describe("createAxiosAuthActions.getProfile", () => {
it("propagates an HTTP failure instead of resolving it as a null profile", async () => {
const httpClient = {
request: jest.fn().mockRejectedValue(new Error("Profile unavailable")),
} as never;
const authActions = createAxiosAuthActions("/api", httpClient);
if (!authActions.getProfile) {
throw new Error("getProfile action is not configured");
}
await expect(authActions.getProfile()).rejects.toThrow("Profile unavailable");
});
});

View File

@ -89,7 +89,7 @@ export const createAxiosAuthActions = (
}
return Promise.resolve(null);
} catch (error) {
return Promise.resolve(null);
throw error;
}
},

View File

@ -0,0 +1,42 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook } from "@testing-library/react-hooks";
import { PropsWithChildren } from "react";
import { AuthActionResponse } from "./AuthActions";
import { AuthContext } from "./AuthContext";
import { useLogin } from "./useLogin";
const createWrapper = (queryClient: QueryClient, login: () => Promise<AuthActionResponse>) => {
return ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>
<AuthContext.Provider value={{ login }}>{children}</AuthContext.Provider>
</QueryClientProvider>
);
};
describe("useLogin", () => {
it.each(["fresh", "stale"])(
"removes %s identity cache before running the successful login callback",
async (cacheState) => {
const queryClient = new QueryClient();
const login = jest.fn().mockResolvedValue({ success: true });
const onSuccess = jest.fn();
queryClient.setQueryData(["auth", "profile"], null, {
updatedAt: Date.now() - (cacheState === "stale" ? 20_000 : 0),
});
queryClient.setQueryData(["auth", "check"], { authenticated: false });
const { result } = renderHook(() => useLogin({ onSuccess }), {
wrapper: createWrapper(queryClient, login),
});
await act(async () => {
await result.current.mutateAsync({ email: "user@example.com", password: "secret" });
});
expect(onSuccess).toHaveBeenCalled();
expect(queryClient.getQueryData(["auth", "profile"])).toBeUndefined();
expect(queryClient.getQueryData(["auth", "check"])).toBeUndefined();
}
);
});

View File

@ -1,16 +1,26 @@
import { AuthActionResponse, useAuth } from "@/lib/hooks";
import { ILogin_DTO } from "@shared/contexts";
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
import { UseMutationOptions, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQueryKey } from "../useQueryKey";
import { AuthActionResponse } from "./AuthActions";
import { useAuth } from "./useAuth";
export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error, ILogin_DTO>) => {
const { onSuccess, ...restParams } = params || {};
const keys = useQueryKey();
const { login } = useAuth();
const queryClient = useQueryClient();
return useMutation({
mutationKey: keys().auth().action("login").get(),
mutationFn: login,
...params,
onSuccess: async (data, variables, context) => {
if (data.success) {
queryClient.removeQueries({ queryKey: keys().auth().get() });
}
await onSuccess?.(data, variables, context);
},
...restParams,
});
};