From c7d1312f897bee2441171d9b016d3a989e6cabab Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 3 Jun 2024 12:53:23 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20frontend=20envir?= =?UTF-8?q?onment=20free?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now, the front had to know at build time the url of the backend and the webrtc server to be able to communicate with them. It is not optimal because it means that we need multiple docker image (1 per environment) to have the app working, it is not very flexible. This commit will make the frontend "environment free" by determining these urls at runtime. --- src/frontend/apps/desk/.env.development | 2 +- src/frontend/apps/desk/.env.production | 1 - src/frontend/apps/desk/.env.test | 2 +- .../desk/src/api/__tests__/fetchApi.test.tsx | 11 +++--- src/frontend/apps/desk/src/api/conf.ts | 7 ++++ src/frontend/apps/desk/src/api/fetchApi.ts | 12 +++++-- src/frontend/apps/desk/src/api/index.ts | 1 + .../apps/desk/src/core/auth/useAuthStore.tsx | 14 +++----- src/frontend/apps/desk/src/custom-next.d.ts | 2 +- .../members/__tests__/MemberGrid.test.tsx | 36 ++++++++++--------- .../members/__tests__/ModalRole.test.tsx | 6 ++-- .../teams/__tests__/PanelTeams.test.tsx | 14 ++++---- src/helm/env.d/dev/values.desk.yaml.gotmpl | 2 +- 13 files changed, 58 insertions(+), 52 deletions(-) delete mode 100644 src/frontend/apps/desk/.env.production create mode 100644 src/frontend/apps/desk/src/api/conf.ts diff --git a/src/frontend/apps/desk/.env.development b/src/frontend/apps/desk/.env.development index 96383c2e0..45ee2324f 100644 --- a/src/frontend/apps/desk/.env.development +++ b/src/frontend/apps/desk/.env.development @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/ +NEXT_PUBLIC_API_ORIGIN=http://localhost:8071 diff --git a/src/frontend/apps/desk/.env.production b/src/frontend/apps/desk/.env.production deleted file mode 100644 index ba16e0cbd..000000000 --- a/src/frontend/apps/desk/.env.production +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_API_URL=https://desk-staging.beta.numerique.gouv.fr/api/v1.0/ diff --git a/src/frontend/apps/desk/.env.test b/src/frontend/apps/desk/.env.test index eda49a1b2..9a4d51422 100644 --- a/src/frontend/apps/desk/.env.test +++ b/src/frontend/apps/desk/.env.test @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=/api/ +NEXT_PUBLIC_API_ORIGIN=http://test.jest diff --git a/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx index 1be911b6d..6626aa8c9 100644 --- a/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx +++ b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx @@ -5,22 +5,19 @@ import { useAuthStore } from '@/core/auth'; describe('fetchAPI', () => { beforeEach(() => { - process.env.NEXT_PUBLIC_API_URL = 'http://some.api.url/api/v1.0/'; fetchMock.restore(); }); it('adds correctly the basename', () => { - fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + fetchMock.mock('http://test.jest/api/v1.0/some/url', 200); void fetchAPI('some/url'); - expect(fetchMock.lastUrl()).toEqual( - 'http://some.api.url/api/v1.0/some/url', - ); + expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v1.0/some/url'); }); it('adds the credentials automatically', () => { - fetchMock.mock('http://some.api.url/api/v1.0/some/url', 200); + fetchMock.mock('http://test.jest/api/v1.0/some/url', 200); void fetchAPI('some/url', { body: 'some body' }); @@ -39,7 +36,7 @@ describe('fetchAPI', () => { .spyOn(useAuthStore.getState(), 'logout') .mockImplementation(logoutMock); - fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401); + fetchMock.mock('http://test.jest/api/v1.0/some/url', 401); await fetchAPI('some/url'); expect(logoutMock).toHaveBeenCalled(); diff --git a/src/frontend/apps/desk/src/api/conf.ts b/src/frontend/apps/desk/src/api/conf.ts new file mode 100644 index 000000000..d4c183884 --- /dev/null +++ b/src/frontend/apps/desk/src/api/conf.ts @@ -0,0 +1,7 @@ +export const baseApiUrl = (apiVersion: string = '1.0') => { + const origin = + process.env.NEXT_PUBLIC_API_ORIGIN || + (typeof window !== 'undefined' ? window.location.origin : ''); + + return `${origin}/api/v${apiVersion}/`; +}; diff --git a/src/frontend/apps/desk/src/api/fetchApi.ts b/src/frontend/apps/desk/src/api/fetchApi.ts index 48b381b13..1b84bbe3f 100644 --- a/src/frontend/apps/desk/src/api/fetchApi.ts +++ b/src/frontend/apps/desk/src/api/fetchApi.ts @@ -1,5 +1,7 @@ import { useAuthStore } from '@/core/auth'; +import { baseApiUrl } from './conf'; + /** * Retrieves the CSRF token from the document's cookies. * @@ -13,9 +15,12 @@ function getCSRFToken() { .pop(); } -export const fetchAPI = async (input: string, init?: RequestInit) => { - const apiUrl = `${process.env.NEXT_PUBLIC_API_URL}${input}`; - const { logout } = useAuthStore.getState(); +export const fetchAPI = async ( + input: string, + init?: RequestInit, + apiVersion = '1.0', +) => { + const apiUrl = `${baseApiUrl(apiVersion)}${input}`; const csrfToken = getCSRFToken(); @@ -30,6 +35,7 @@ export const fetchAPI = async (input: string, init?: RequestInit) => { }); if (response.status === 401) { + const { logout } = useAuthStore.getState(); logout(); } diff --git a/src/frontend/apps/desk/src/api/index.ts b/src/frontend/apps/desk/src/api/index.ts index c8c14e470..c79db68c4 100644 --- a/src/frontend/apps/desk/src/api/index.ts +++ b/src/frontend/apps/desk/src/api/index.ts @@ -1,4 +1,5 @@ export * from './APIError'; +export * from './conf'; export * from './fetchApi'; export * from './types'; export * from './utils'; diff --git a/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx b/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx index 27174dbe2..3de45eacd 100644 --- a/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx +++ b/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx @@ -1,12 +1,8 @@ import { create } from 'zustand'; -import { User, getMe } from './api'; +import { baseApiUrl } from '@/api'; -export const login = () => { - window.location.replace( - new URL('authenticate/', process.env.NEXT_PUBLIC_API_URL).href, - ); -}; +import { User, getMe } from './api'; interface AuthStore { authenticated: boolean; @@ -30,12 +26,10 @@ export const useAuthStore = create((set) => ({ set({ authenticated: true, userData: data }); }) .catch(() => { - login(); + window.location.replace(new URL('authenticate/', baseApiUrl()).href); }); }, logout: () => { - window.location.replace( - new URL('logout/', process.env.NEXT_PUBLIC_API_URL).href, - ); + window.location.replace(new URL('logout/', baseApiUrl()).href); }, })); diff --git a/src/frontend/apps/desk/src/custom-next.d.ts b/src/frontend/apps/desk/src/custom-next.d.ts index 5919942a6..a54bc2a16 100644 --- a/src/frontend/apps/desk/src/custom-next.d.ts +++ b/src/frontend/apps/desk/src/custom-next.d.ts @@ -19,6 +19,6 @@ declare module '*.svg?url' { namespace NodeJS { interface ProcessEnv { - NEXT_PUBLIC_API_URL?: string; + NEXT_PUBLIC_API_ORIGIN?: string; } } diff --git a/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx b/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx index 04e70d304..8c001a331 100644 --- a/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx +++ b/src/frontend/apps/desk/src/features/members/__tests__/MemberGrid.test.tsx @@ -20,7 +20,7 @@ describe('MemberGrid', () => { }); it('renders with no member to display', async () => { - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { + fetchMock.mock(`end:/teams/123456/accesses/?page=1`, { count: 0, results: [], }); @@ -76,7 +76,7 @@ describe('MemberGrid', () => { }, ]; - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { + fetchMock.mock(`end:/teams/123456/accesses/?page=1`, { count: 3, results: accesses, }); @@ -99,7 +99,8 @@ describe('MemberGrid', () => { }); it('checks the pagination', async () => { - fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, { + const regexp = new RegExp(/.*\/teams\/123456\/accesses\/\?page=.*/); + fetchMock.get(regexp, { count: 40, results: Array.from({ length: 20 }, (_, i) => ({ id: i, @@ -119,7 +120,7 @@ describe('MemberGrid', () => { expect(screen.getByRole('status')).toBeInTheDocument(); - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1'); + expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=1'); expect( await screen.findByLabelText('You are currently on page 1'), @@ -131,7 +132,7 @@ describe('MemberGrid', () => { await screen.findByLabelText('You are currently on page 2'), ).toBeInTheDocument(); - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=2'); + expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=2'); }); [ @@ -149,7 +150,8 @@ describe('MemberGrid', () => { }, ].forEach(({ role, expected }) => { it(`checks action button when ${role}`, async () => { - fetchMock.get(`begin:/api/teams/123456/accesses/?page=`, { + const regexp = new RegExp(/.*\/teams\/123456\/accesses\/\?page=.*/); + fetchMock.get(regexp, { count: 1, results: [ { @@ -190,7 +192,7 @@ describe('MemberGrid', () => { }); it('controls the render when api error', async () => { - fetchMock.mock(`/api/teams/123456/accesses/?page=1`, { + fetchMock.mock(`end:/teams/123456/accesses/?page=1`, { status: 500, body: { cause: 'All broken :(', @@ -207,7 +209,7 @@ describe('MemberGrid', () => { }); it('cannot add members when current role is member', () => { - fetchMock.get(`/api/teams/123456/accesses/?page=1`, 200); + fetchMock.get(`end:/teams/123456/accesses/?page=1`, 200); render(, { wrapper: AppWrapper, @@ -261,17 +263,17 @@ describe('MemberGrid', () => { ); const reversedMockedData = [...sortedMockedData].reverse(); - fetchMock.get(`/api/teams/123456/accesses/?page=1`, { + fetchMock.get(`end:/teams/123456/accesses/?page=1`, { count: 3, results: mockedData, }); - fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=${ordering}`, { + fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=${ordering}`, { count: 3, results: sortedMockedData, }); - fetchMock.get(`/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, { + fetchMock.get(`end:/teams/123456/accesses/?page=1&ordering=-${ordering}`, { count: 3, results: reversedMockedData, }); @@ -282,7 +284,7 @@ describe('MemberGrid', () => { expect(screen.getByRole('status')).toBeInTheDocument(); - expect(fetchMock.lastUrl()).toBe(`/api/teams/123456/accesses/?page=1`); + expect(fetchMock.lastUrl()).toContain(`/teams/123456/accesses/?page=1`); await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument(); @@ -298,8 +300,8 @@ describe('MemberGrid', () => { await userEvent.click(screen.getByText(header_name)); - expect(fetchMock.lastUrl()).toBe( - `/api/teams/123456/accesses/?page=1&ordering=${ordering}`, + expect(fetchMock.lastUrl()).toContain( + `/teams/123456/accesses/?page=1&ordering=${ordering}`, ); await waitFor(() => { @@ -315,8 +317,8 @@ describe('MemberGrid', () => { await userEvent.click(screen.getByText(header_name)); - expect(fetchMock.lastUrl()).toBe( - `/api/teams/123456/accesses/?page=1&ordering=-${ordering}`, + expect(fetchMock.lastUrl()).toContain( + `/teams/123456/accesses/?page=1&ordering=-${ordering}`, ); await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument(); @@ -331,7 +333,7 @@ describe('MemberGrid', () => { await userEvent.click(screen.getByText(header_name)); - expect(fetchMock.lastUrl()).toBe('/api/teams/123456/accesses/?page=1'); + expect(fetchMock.lastUrl()).toContain('/teams/123456/accesses/?page=1'); await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument(); diff --git a/src/frontend/apps/desk/src/features/members/__tests__/ModalRole.test.tsx b/src/frontend/apps/desk/src/features/members/__tests__/ModalRole.test.tsx index c1d52ab7a..24316a08b 100644 --- a/src/frontend/apps/desk/src/features/members/__tests__/ModalRole.test.tsx +++ b/src/frontend/apps/desk/src/features/members/__tests__/ModalRole.test.tsx @@ -66,7 +66,7 @@ describe('ModalRole', () => { }); it('updates the role successfully', async () => { - fetchMock.patchOnce(`/api/teams/123/accesses/789/`, { + fetchMock.mock(`end:/teams/123/accesses/789/`, { status: 200, ok: true, }); @@ -110,13 +110,13 @@ describe('ModalRole', () => { ); }); - expect(fetchMock.lastUrl()).toBe(`/api/teams/123/accesses/789/`); + expect(fetchMock.lastUrl()).toContain(`/teams/123/accesses/789/`); expect(onClose).toHaveBeenCalled(); }); it('fails to update the role', async () => { - fetchMock.patchOnce(`/api/teams/123/accesses/789/`, { + fetchMock.patchOnce(`end:/teams/123/accesses/789/`, { status: 500, body: { detail: 'The server is totally broken', diff --git a/src/frontend/apps/desk/src/features/teams/__tests__/PanelTeams.test.tsx b/src/frontend/apps/desk/src/features/teams/__tests__/PanelTeams.test.tsx index 585ac30f8..4614df557 100644 --- a/src/frontend/apps/desk/src/features/teams/__tests__/PanelTeams.test.tsx +++ b/src/frontend/apps/desk/src/features/teams/__tests__/PanelTeams.test.tsx @@ -23,7 +23,7 @@ describe('PanelTeams', () => { }); it('renders with no team to display', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { count: 0, results: [], }); @@ -40,7 +40,7 @@ describe('PanelTeams', () => { }); it('renders an empty team', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { count: 1, results: [ { @@ -61,7 +61,7 @@ describe('PanelTeams', () => { }); it('renders a team with only 1 member', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { count: 1, results: [ { @@ -87,7 +87,7 @@ describe('PanelTeams', () => { }); it('renders a non-empty team', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { count: 1, results: [ { @@ -115,7 +115,7 @@ describe('PanelTeams', () => { }); it('renders the error', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { status: 500, }); @@ -131,7 +131,7 @@ describe('PanelTeams', () => { }); it('renders with team panel open', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.mock(`end:/teams/?page=1&ordering=-created_at`, { count: 1, results: [], }); @@ -146,7 +146,7 @@ describe('PanelTeams', () => { }); it('closes and opens the team panel', async () => { - fetchMock.mock(`/api/teams/?page=1&ordering=-created_at`, { + fetchMock.get(`end:/teams/?page=1&ordering=-created_at`, { count: 1, results: [], }); diff --git a/src/helm/env.d/dev/values.desk.yaml.gotmpl b/src/helm/env.d/dev/values.desk.yaml.gotmpl index bad00204e..6977b4bdf 100644 --- a/src/helm/env.d/dev/values.desk.yaml.gotmpl +++ b/src/helm/env.d/dev/values.desk.yaml.gotmpl @@ -60,7 +60,7 @@ backend: frontend: envVars: PORT: 8080 - NEXT_PUBLIC_API_URL: https://desk.127.0.0.1.nip.io/api/v1.0/ + NEXT_PUBLIC_API_ORIGIN: https://desk.127.0.0.1.nip.io replicas: 1 command: