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 96383c2..45ee232 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 ba16e0c..0000000 --- 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 eda49a1..9a4d514 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 1be911b..6626aa8 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 0000000..d4c1838 --- /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 48b381b..1b84bbe 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 c8c14e4..c79db68 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 27174db..3de45ea 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 5919942..a54bc2a 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 04e70d3..8c001a3 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 c1d52ab..24316a0 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 585ac30..4614df5 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 bad0020..6977b4b 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: