fix(frontend): URL-Normalisierung gegen Doppel-Präfix — Workspace-UI & Permission-Refresh in Produktion repariert
Problem: Axios baseURL '/api/v1' + 16 apiX-Calls mit vollem Präfix
('/api/v1/workspaces/...') ergaben '/api/v1/api/v1/...' → 404 live
(bewiesen per curl + Node). Betroffen: komplette Workspace-UI
(Switcher, Manager, Phase-N-Scope-Editor) + Permission-Refresh.
Fix (Defense-in-Depth):
- normalizeApiUrl() in client.ts: alle apiX-Wrapper strippen redundanten
'/api/v1'-Präfix — deckt auch DYNAMISCHE Backend-Contract-Endpoints
(N2-Scope-Editor-Wertquellen) ab, die Call-Sites nicht umschreiben können
- workspaces.ts + useUserPermissions.ts auf relative Pfade gesäubert (16 Calls)
Tests: clientUrl 8/8 neu (Normalisierung + Wrapper-Beweis), tsc clean,
Build OK, Workspace-Regression 3 Dateien grün
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Runtime-Bug fix test: URL normalization in the API client (Phase N hotfix).
|
||||
*
|
||||
* The axios client has baseURL '/api/v1'. Calls that pass a FULL prefix
|
||||
* ('/api/v1/workspaces') would resolve to '/api/v1/api/v1/...' → 404 in
|
||||
* production (proven via curl + node). The client must normalize URLs by
|
||||
* stripping a redundant leading '/api/v1' — this also covers DYNAMIC
|
||||
* endpoints from backend contracts (scope value sources like
|
||||
* '/api/v1/contact-folders') that the frontend cannot rewrite.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
normalizeApiUrl,
|
||||
apiGet,
|
||||
apiClient,
|
||||
} from '@/api/client';
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
logError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('normalizeApiUrl', () => {
|
||||
it('strips a redundant leading /api/v1 (baseURL is already /api/v1)', () => {
|
||||
expect(normalizeApiUrl('/api/v1/workspaces')).toBe('/workspaces');
|
||||
expect(normalizeApiUrl('/api/v1/auth/me/permissions')).toBe('/auth/me/permissions');
|
||||
expect(normalizeApiUrl('/api/v1/workspaces/scope-definitions')).toBe(
|
||||
'/workspaces/scope-definitions',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps query strings intact', () => {
|
||||
expect(normalizeApiUrl('/api/v1/miniapps?host=dashboard')).toBe(
|
||||
'/miniapps?host=dashboard',
|
||||
);
|
||||
expect(normalizeApiUrl('/api/v1/saved-views?entity_type=contact')).toBe(
|
||||
'/saved-views?entity_type=contact',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves relative paths untouched', () => {
|
||||
expect(normalizeApiUrl('/workspaces')).toBe('/workspaces');
|
||||
expect(normalizeApiUrl('/contacts?page=1')).toBe('/contacts?page=1');
|
||||
});
|
||||
|
||||
it('leaves other api versions untouched (only v1 is the baseURL)', () => {
|
||||
expect(normalizeApiUrl('/api/v2/other')).toBe('/api/v2/other');
|
||||
});
|
||||
|
||||
it('does not strip /api/v1 in the middle of a path', () => {
|
||||
expect(normalizeApiUrl('/workspaces/api/v1')).toBe('/workspaces/api/v1');
|
||||
});
|
||||
|
||||
it('handles empty and root paths', () => {
|
||||
expect(normalizeApiUrl('/')).toBe('/');
|
||||
expect(normalizeApiUrl('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('apiX wrappers normalize full-prefix URLs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('apiGet strips the double prefix before axios sees it', async () => {
|
||||
const spy = vi.spyOn(apiClient, 'get').mockResolvedValue({ data: { ok: true } });
|
||||
await apiGet('/api/v1/workspaces');
|
||||
expect(spy).toHaveBeenCalledWith('/workspaces', undefined);
|
||||
expect(spy.mock.calls[0][0]).not.toContain('api/v1/api');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('dynamic contract endpoints (scope value sources) are normalized too', async () => {
|
||||
const spy = vi.spyOn(apiClient, 'get').mockResolvedValue({ data: [] });
|
||||
// This is what useScopeValues does: endpoint comes from the backend contract
|
||||
await apiGet('/api/v1/contact-folders');
|
||||
expect(spy).toHaveBeenCalledWith('/contact-folders', undefined);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user