From 2afbd73a3167bc6c41a58f77da413197eecb9b1c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 18 Mar 2026 09:10:17 +0300 Subject: [PATCH] fix(auth): fix circular import problem by changing the way the authHttpClient gets accessToken; replace individual calls mocks with the common ones --- .../auth/api/calls/login/constants.ts | 6 -- src/features/auth/api/calls/login/index.ts | 4 +- .../auth/api/calls/login/login.mock.ts | 13 +--- .../auth/api/calls/login/login.spec.ts | 16 +++-- src/features/auth/api/calls/login/login.ts | 9 ++- .../auth/api/calls/logout/constants.ts | 1 - src/features/auth/api/calls/logout/index.ts | 2 +- .../auth/api/calls/logout/logout.mock.ts | 2 +- src/features/auth/api/calls/logout/logout.ts | 7 ++- src/features/auth/api/calls/mocks.ts | 23 +++++++ .../auth/api/calls/refresh/constants.ts | 3 - src/features/auth/api/calls/refresh/index.ts | 2 +- .../auth/api/calls/refresh/refresh.mock.ts | 5 +- .../auth/api/calls/refresh/refresh.spec.ts | 4 +- .../auth/api/calls/refresh/refresh.ts | 7 ++- .../auth/api/calls/register/constants.ts | 6 -- src/features/auth/api/calls/register/index.ts | 2 +- .../auth/api/calls/register/register.mock.ts | 3 +- .../auth/api/calls/register/register.spec.ts | 15 ++++- .../auth/api/calls/register/register.ts | 9 ++- .../auth/api/config/authApi/authApi.ts | 61 ++++++++++++------- src/features/auth/api/config/index.ts | 2 +- .../auth/model/stores/authStore/authStore.ts | 7 ++- src/features/auth/model/types/store.ts | 11 ++-- 24 files changed, 135 insertions(+), 85 deletions(-) delete mode 100644 src/features/auth/api/calls/login/constants.ts delete mode 100644 src/features/auth/api/calls/logout/constants.ts create mode 100644 src/features/auth/api/calls/mocks.ts delete mode 100644 src/features/auth/api/calls/refresh/constants.ts delete mode 100644 src/features/auth/api/calls/register/constants.ts diff --git a/src/features/auth/api/calls/login/constants.ts b/src/features/auth/api/calls/login/constants.ts deleted file mode 100644 index fe1c15c..0000000 --- a/src/features/auth/api/calls/login/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const LOGIN_API_ROUTE = "auth/login"; - -// MOCKS -export const MOCK_EMAIL = "test@test.com"; -export const MOCK_PASSWORD = "password"; -export const MOCK_TOKEN = "mock.access.token"; diff --git a/src/features/auth/api/calls/login/index.ts b/src/features/auth/api/calls/login/index.ts index 31b1f9b..b84ef70 100644 --- a/src/features/auth/api/calls/login/index.ts +++ b/src/features/auth/api/calls/login/index.ts @@ -1,2 +1,2 @@ -export { login } from './login'; -export { LOGIN_API_ROUTE } from './constants'; \ No newline at end of file +export { login } from "./login"; +export { loginMock } from "./login.mock"; diff --git a/src/features/auth/api/calls/login/login.mock.ts b/src/features/auth/api/calls/login/login.mock.ts index 4d3b5c1..4bcce0b 100644 --- a/src/features/auth/api/calls/login/login.mock.ts +++ b/src/features/auth/api/calls/login/login.mock.ts @@ -1,12 +1,8 @@ import { http, HttpResponse } from "msw"; import type { AuthData } from "../../../model/types/service"; import { BASE_URL } from "shared/config"; -import { - LOGIN_API_ROUTE, - MOCK_EMAIL, - MOCK_PASSWORD, - MOCK_TOKEN, -} from "./constants"; +import { LOGIN_API_ROUTE } from "./login"; +import { MOCK_AUTH_RESPONSE, MOCK_EMAIL, MOCK_PASSWORD } from "../mocks"; const LOGIN_URL = `${BASE_URL}/${LOGIN_API_ROUTE}`; @@ -17,10 +13,7 @@ export const loginMock = http.post(LOGIN_URL, async ({ request }) => { const { email, password } = (await request.json()) as AuthData; if (email === MOCK_EMAIL && password === MOCK_PASSWORD) { - return HttpResponse.json({ - accessToken: MOCK_TOKEN, - user: { id: "1", email }, - }); + return HttpResponse.json(MOCK_AUTH_RESPONSE); } return HttpResponse.json({ message: "Invalid credentials" }, { status: 401 }); diff --git a/src/features/auth/api/calls/login/login.spec.ts b/src/features/auth/api/calls/login/login.spec.ts index 851e0ea..7db683b 100644 --- a/src/features/auth/api/calls/login/login.spec.ts +++ b/src/features/auth/api/calls/login/login.spec.ts @@ -1,7 +1,12 @@ import { setupServer } from "msw/node"; import { login } from "./login"; import { loginMock } from "./login.mock"; -import { MOCK_EMAIL, MOCK_PASSWORD } from "./constants"; +import { + MOCK_EMAIL, + MOCK_EXISTING_USER, + MOCK_PASSWORD, + MOCK_TOKEN, +} from "../mocks"; const server = setupServer(loginMock); @@ -12,10 +17,13 @@ describe("login", () => { describe("happy path", () => { it("returns access token and user on valid credentials", async () => { - const result = await login({ email: MOCK_EMAIL, password: MOCK_PASSWORD }); + const result = await login({ + email: MOCK_EMAIL, + password: MOCK_PASSWORD, + }); - expect(result.accessToken).toBe("mock.access.token"); - expect(result.user).toEqual({ id: "1", email: MOCK_EMAIL }); + expect(result.accessToken).toBe(MOCK_TOKEN); + expect(result.user).toEqual(MOCK_EXISTING_USER); }); }); diff --git a/src/features/auth/api/calls/login/login.ts b/src/features/auth/api/calls/login/login.ts index 42b8fab..1bfffd4 100644 --- a/src/features/auth/api/calls/login/login.ts +++ b/src/features/auth/api/calls/login/login.ts @@ -1,6 +1,7 @@ -import { api } from "../../../api"; import type { AuthData, AuthResponse } from "../../../model/types/service"; -import { LOGIN_API_ROUTE } from "./constants"; +import { authHttpClient } from "../../config/authApi/authApi"; + +export const LOGIN_API_ROUTE = "auth/login"; /** * Logs in a user with the given email and password. @@ -9,5 +10,7 @@ import { LOGIN_API_ROUTE } from "./constants"; * @returns A promise that resolves to the authentication response. */ export function login(loginData: AuthData) { - return api.post(LOGIN_API_ROUTE, { json: loginData }).json(); + return authHttpClient + .post(LOGIN_API_ROUTE, { json: loginData }) + .json(); } diff --git a/src/features/auth/api/calls/logout/constants.ts b/src/features/auth/api/calls/logout/constants.ts deleted file mode 100644 index 24ee95e..0000000 --- a/src/features/auth/api/calls/logout/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const LOGOUT_API_ROUTE = "auth/logout"; diff --git a/src/features/auth/api/calls/logout/index.ts b/src/features/auth/api/calls/logout/index.ts index a81b5b9..1dbcf67 100644 --- a/src/features/auth/api/calls/logout/index.ts +++ b/src/features/auth/api/calls/logout/index.ts @@ -1,2 +1,2 @@ export { logout } from "./logout"; -export { LOGOUT_API_ROUTE } from "./constants"; +export { logoutMock } from "./logout.mock"; diff --git a/src/features/auth/api/calls/logout/logout.mock.ts b/src/features/auth/api/calls/logout/logout.mock.ts index 1515d04..68007b3 100644 --- a/src/features/auth/api/calls/logout/logout.mock.ts +++ b/src/features/auth/api/calls/logout/logout.mock.ts @@ -1,6 +1,6 @@ import { http, HttpResponse } from "msw"; import { BASE_URL } from "shared/config"; -import { LOGOUT_API_ROUTE } from "./constants"; +import { LOGOUT_API_ROUTE } from "./logout"; const LOGOUT_URL = `${BASE_URL}/${LOGOUT_API_ROUTE}`; diff --git a/src/features/auth/api/calls/logout/logout.ts b/src/features/auth/api/calls/logout/logout.ts index f418d80..478f4ff 100644 --- a/src/features/auth/api/calls/logout/logout.ts +++ b/src/features/auth/api/calls/logout/logout.ts @@ -1,5 +1,6 @@ -import { api } from "../../../api"; -import { LOGOUT_API_ROUTE } from "./constants"; +import { authHttpClient } from "../../config"; + +export const LOGOUT_API_ROUTE = "auth/logout"; /** * Logs out the currently authenticated user. @@ -8,5 +9,5 @@ import { LOGOUT_API_ROUTE } from "./constants"; * @returns A promise that resolves when the session is terminated. */ export async function logout(): Promise { - await api.post(LOGOUT_API_ROUTE); + await authHttpClient.post(LOGOUT_API_ROUTE); } diff --git a/src/features/auth/api/calls/mocks.ts b/src/features/auth/api/calls/mocks.ts new file mode 100644 index 0000000..3100d7d --- /dev/null +++ b/src/features/auth/api/calls/mocks.ts @@ -0,0 +1,23 @@ +import type { User } from "entities/User"; +import type { AuthResponse } from "../../model"; + +export const MOCK_TOKEN = "mock.access.token"; +export const MOCK_FRESH_TOKEN = "mock.fresh.access.token"; +export const MOCK_EMAIL = "test@test.com"; +export const MOCK_NEW_EMAIL = "new@test.com"; +export const MOCK_PASSWORD = "password"; + +export const MOCK_EXISTING_USER: User = { + id: "1", + email: MOCK_EMAIL, +}; + +export const MOCK_NEW_USER: User = { + id: "2", + email: MOCK_NEW_EMAIL, +}; + +export const MOCK_AUTH_RESPONSE: AuthResponse = { + accessToken: MOCK_TOKEN, + user: MOCK_EXISTING_USER, +}; diff --git a/src/features/auth/api/calls/refresh/constants.ts b/src/features/auth/api/calls/refresh/constants.ts deleted file mode 100644 index 99cadcb..0000000 --- a/src/features/auth/api/calls/refresh/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const REFRESH_API_ROUTE = "auth/refresh"; -export const MOCK_TOKEN = "mock.access.token"; -export const MOCK_FRESH_TOKEN = "mock.fresh.access.token"; diff --git a/src/features/auth/api/calls/refresh/index.ts b/src/features/auth/api/calls/refresh/index.ts index 1f69e73..7defc6e 100644 --- a/src/features/auth/api/calls/refresh/index.ts +++ b/src/features/auth/api/calls/refresh/index.ts @@ -1,2 +1,2 @@ export { refresh } from "./refresh"; -export { REFRESH_API_ROUTE } from "./constants"; +export { refreshMock } from "./refresh.mock"; diff --git a/src/features/auth/api/calls/refresh/refresh.mock.ts b/src/features/auth/api/calls/refresh/refresh.mock.ts index ca4cc7a..590e17e 100644 --- a/src/features/auth/api/calls/refresh/refresh.mock.ts +++ b/src/features/auth/api/calls/refresh/refresh.mock.ts @@ -1,6 +1,7 @@ import { http, HttpResponse } from "msw"; import { BASE_URL } from "shared/config"; -import { MOCK_FRESH_TOKEN, MOCK_TOKEN, REFRESH_API_ROUTE } from "./constants"; +import { REFRESH_API_ROUTE } from "./refresh"; +import { MOCK_EXISTING_USER, MOCK_FRESH_TOKEN, MOCK_TOKEN } from "../mocks"; const REFRESH_URL = `${BASE_URL}/${REFRESH_API_ROUTE}`; @@ -13,8 +14,8 @@ export const refreshMock = http.post(REFRESH_URL, ({ request }) => { if (authHeader === `Bearer ${MOCK_TOKEN}`) { return HttpResponse.json({ + user: MOCK_EXISTING_USER, accessToken: MOCK_FRESH_TOKEN, - user: { id: "1", email: "test@test.com" }, }); } diff --git a/src/features/auth/api/calls/refresh/refresh.spec.ts b/src/features/auth/api/calls/refresh/refresh.spec.ts index f8e6ea7..f979c86 100644 --- a/src/features/auth/api/calls/refresh/refresh.spec.ts +++ b/src/features/auth/api/calls/refresh/refresh.spec.ts @@ -2,7 +2,7 @@ import { setupServer } from "msw/node"; import { useAuthStore } from "../../../model"; import { refresh } from "./refresh"; import { refreshMock } from "./refresh.mock"; -import { MOCK_FRESH_TOKEN, MOCK_TOKEN } from "./constants"; +import { MOCK_EXISTING_USER, MOCK_FRESH_TOKEN, MOCK_TOKEN } from "../mocks"; const server = setupServer(refreshMock); @@ -21,7 +21,7 @@ describe("refresh", () => { const result = await refresh(); expect(result.accessToken).toBe(MOCK_FRESH_TOKEN); - expect(result.user).toEqual({ id: "1", email: "test@test.com" }); + expect(result.user).toEqual(MOCK_EXISTING_USER); }); }); diff --git a/src/features/auth/api/calls/refresh/refresh.ts b/src/features/auth/api/calls/refresh/refresh.ts index d69d7d6..c5bc1dc 100644 --- a/src/features/auth/api/calls/refresh/refresh.ts +++ b/src/features/auth/api/calls/refresh/refresh.ts @@ -1,7 +1,8 @@ -import { api } from "../../../api"; import type { AuthResponse } from "../../../model/types/service"; -import { REFRESH_API_ROUTE } from "./constants"; +import { authHttpClient } from "../../config/authApi/authApi"; + +export const REFRESH_API_ROUTE = "auth/refresh"; export function refresh() { - return api.post(REFRESH_API_ROUTE).json(); + return authHttpClient.post(REFRESH_API_ROUTE).json(); } diff --git a/src/features/auth/api/calls/register/constants.ts b/src/features/auth/api/calls/register/constants.ts deleted file mode 100644 index 1931ebc..0000000 --- a/src/features/auth/api/calls/register/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const REGISTER_API_ROUTE = "auth/register"; - -// MOCKS -export const MOCK_EMAIL = "test@test.com"; -export const MOCK_PASSWORD = "password"; -export const MOCK_TOKEN = "mock.access.token"; diff --git a/src/features/auth/api/calls/register/index.ts b/src/features/auth/api/calls/register/index.ts index 15ae88b..0038110 100644 --- a/src/features/auth/api/calls/register/index.ts +++ b/src/features/auth/api/calls/register/index.ts @@ -1,2 +1,2 @@ export { register } from "./register"; -export { REGISTER_API_ROUTE } from "./constants"; +export { registerMock } from "./register.mock"; diff --git a/src/features/auth/api/calls/register/register.mock.ts b/src/features/auth/api/calls/register/register.mock.ts index 84bff2f..44e7ec1 100644 --- a/src/features/auth/api/calls/register/register.mock.ts +++ b/src/features/auth/api/calls/register/register.mock.ts @@ -1,7 +1,8 @@ import { http, HttpResponse } from "msw"; import type { AuthData } from "../../../model/types/service"; import { BASE_URL } from "shared/config"; -import { REGISTER_API_ROUTE, MOCK_EMAIL, MOCK_TOKEN } from "./constants"; +import { REGISTER_API_ROUTE } from "./register"; +import { MOCK_EMAIL, MOCK_TOKEN } from "../mocks"; const REGISTER_URL = `${BASE_URL}/${REGISTER_API_ROUTE}`; diff --git a/src/features/auth/api/calls/register/register.spec.ts b/src/features/auth/api/calls/register/register.spec.ts index 0baf51b..4ffc572 100644 --- a/src/features/auth/api/calls/register/register.spec.ts +++ b/src/features/auth/api/calls/register/register.spec.ts @@ -1,7 +1,13 @@ import { setupServer } from "msw/node"; import { register } from "./register"; import { registerMock } from "./register.mock"; -import { MOCK_EMAIL, MOCK_PASSWORD, MOCK_TOKEN } from "./constants"; +import { + MOCK_EMAIL, + MOCK_NEW_EMAIL, + MOCK_NEW_USER, + MOCK_PASSWORD, + MOCK_TOKEN, +} from "../mocks"; const server = setupServer(registerMock); @@ -12,10 +18,13 @@ describe("register", () => { describe("happy path", () => { it("returns access token and user for a new email", async () => { - const result = await register({ email: "new@test.com", password: MOCK_PASSWORD }); + const result = await register({ + email: MOCK_NEW_EMAIL, + password: MOCK_PASSWORD, + }); expect(result.accessToken).toBe(MOCK_TOKEN); - expect(result.user).toEqual({ id: "2", email: "new@test.com" }); + expect(result.user).toEqual(MOCK_NEW_USER); }); }); diff --git a/src/features/auth/api/calls/register/register.ts b/src/features/auth/api/calls/register/register.ts index 293acc3..47da66b 100644 --- a/src/features/auth/api/calls/register/register.ts +++ b/src/features/auth/api/calls/register/register.ts @@ -1,6 +1,7 @@ -import { api } from "../../../api"; import type { AuthData, AuthResponse } from "../../../model/types/service"; -import { REGISTER_API_ROUTE } from "./constants"; +import { authHttpClient } from "../../config/authApi/authApi"; + +export const REGISTER_API_ROUTE = "auth/register"; /** * Registers a new user with the given email and password. @@ -9,5 +10,7 @@ import { REGISTER_API_ROUTE } from "./constants"; * @returns A promise that resolves to the authentication response. */ export function register(registerData: AuthData) { - return api.post(REGISTER_API_ROUTE, { json: registerData }).json(); + return authHttpClient + .post(REGISTER_API_ROUTE, { json: registerData }) + .json(); } diff --git a/src/features/auth/api/config/authApi/authApi.ts b/src/features/auth/api/config/authApi/authApi.ts index e445473..039bafc 100644 --- a/src/features/auth/api/config/authApi/authApi.ts +++ b/src/features/auth/api/config/authApi/authApi.ts @@ -1,25 +1,44 @@ -import { useAuthStore } from "../../../model"; import { api as baseApi } from "shared/config"; -// Extend base API with authentication hooks -export const api = baseApi.extend({ - hooks: { - beforeRequest: [ - (request) => { - const token = useAuthStore.getState().accessToken; +type TokenGetter = () => string | null | undefined; - if (token) { - request.headers.set("Authorization", `Bearer ${token}`); - } +class HttpClient { + private getToken: TokenGetter = () => null; - return request; - }, - ], - afterResponse: [ - async (request, options, response) => { - // Refresh token logic - return response; - }, - ], - }, -}); + setTokenGetter(fn: TokenGetter) { + this.getToken = fn; + } + + // Extend base API with authentication hooks + private instance = baseApi.extend({ + hooks: { + beforeRequest: [ + (request) => { + const token = this.getToken(); + + if (token) { + request.headers.set("Authorization", `Bearer ${token}`); + } + + return request; + }, + ], + afterResponse: [ + async (request, options, response) => { + // Refresh token logic + return response; + }, + ], + }, + }); + + get = (url: string, options?: Parameters[1]) => { + return this.instance.get(url, options); + }; + + post = (url: string, options?: Parameters[1]) => { + return this.instance.post(url, options); + }; +} + +export const authHttpClient = new HttpClient(); diff --git a/src/features/auth/api/config/index.ts b/src/features/auth/api/config/index.ts index 08b45d6..b795732 100644 --- a/src/features/auth/api/config/index.ts +++ b/src/features/auth/api/config/index.ts @@ -1 +1 @@ -export * from "./authApi/authApi"; +export { authHttpClient } from "./authApi/authApi"; diff --git a/src/features/auth/model/stores/authStore/authStore.ts b/src/features/auth/model/stores/authStore/authStore.ts index 6722972..11351e5 100644 --- a/src/features/auth/model/stores/authStore/authStore.ts +++ b/src/features/auth/model/stores/authStore/authStore.ts @@ -3,8 +3,9 @@ import type { AuthStore, AuthStoreState } from "../../types/store"; import { login, logout, refresh, register } from "../../../api"; import { callApi } from "shared/utils"; import { UNEXPECTED_ERROR_MESSAGE } from "shared/config"; +import { authHttpClient } from "../../../api/config/authApi/authApi"; -const defaultStoreState: Readonly = { +export const defaultStoreState: Readonly = { user: undefined, status: "idle", accessToken: undefined, @@ -13,7 +14,7 @@ const defaultStoreState: Readonly = { export const useAuthStore = create()((set) => ({ ...defaultStoreState, - + reset: () => set({ ...defaultStoreState }), login: async (loginData) => { set({ status: "loading" }); try { @@ -108,3 +109,5 @@ export const useAuthStore = create()((set) => ({ } }, })); + +authHttpClient.setTokenGetter(() => useAuthStore.getState().accessToken); diff --git a/src/features/auth/model/types/store.ts b/src/features/auth/model/types/store.ts index f0faab8..dc05b0e 100644 --- a/src/features/auth/model/types/store.ts +++ b/src/features/auth/model/types/store.ts @@ -21,13 +21,14 @@ export interface AuthStoreState { error: ApiError | Error | null; } -export type LoginAction = (data: AuthData) => void; -export type RegisterAction = (data: AuthData) => void; -export type LogoutAction = () => void; -export type RefreshAction = () => void; +export type ResetAction = () => void; +export type LoginAction = (data: AuthData) => Promise; +export type RegisterAction = (data: AuthData) => Promise; +export type LogoutAction = () => Promise; +export type RefreshAction = () => Promise; export interface AuthStoreActions { - // Async actions + reset: ResetAction; login: LoginAction; register: RegisterAction; logout: LogoutAction;