Solucionados los problemas al hacer login.
This commit is contained in:
parent
de00a87c7b
commit
612355bcec
@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
100
client/src/components/ProtectedRoute/ProtectedRoute.test.tsx
Normal file
100
client/src/components/ProtectedRoute/ProtectedRoute.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
16
client/src/lib/axios/createAxiosAuthActions.test.ts
Normal file
16
client/src/lib/axios/createAxiosAuthActions.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -89,7 +89,7 @@ export const createAxiosAuthActions = (
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
} catch (error) {
|
||||
return Promise.resolve(null);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
42
client/src/lib/hooks/useAuth/useLogin.test.tsx
Normal file
42
client/src/lib/hooks/useAuth/useLogin.test.tsx
Normal 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();
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user