Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/next-rwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
"@auth0/nextjs-auth0": "^4.20.0",
"@auth0/nextjs-auth0": "^4.21.0",
"@auth0/universal-components-react": "workspace:*",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/postcss": "^4.1.17",
Expand Down
196 changes: 181 additions & 15 deletions packages/core/src/api/__tests__/api-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,25 @@ import {
createMockContextInterface,
TEST_DOMAIN,
} from '../../internals/__mocks__/shared/api-service.mocks';
import { AUTH0_SCOPE_HEADER, createProxyFetcher, createSpaFetcher } from '../api-utils';
import {
AUTH0_CLIENT_HEADER,
AUTH0_SCOPE_HEADER,
createProxyFetcher,
createSpaFetcher,
} from '../api-utils';
import { ContentType, HeaderName } from '../http-constants';
import type { TelemetryConfig } from '../telemetry';

import { stubFetch } from './__mocks__/api-utils.mocks';

const defaultTelemetry: TelemetryConfig = {
css: 'unknown',
distribution: 'npm',
framework: 'react',
};

const mockGetComponent = () => 'test-component';

describe('api-utils', () => {
describe('createProxyFetcher', () => {
afterEach(() => {
Expand All @@ -18,7 +32,10 @@ describe('api-utils', () => {

it('sets content-type header to application/json', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});

await fetcher('https://example.com/api', { method: 'POST' }, undefined);

Expand All @@ -28,7 +45,10 @@ describe('api-utils', () => {

it('sets auth0-scope header when scope array is provided', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});

await fetcher(
'https://example.com/api',
Expand All @@ -45,7 +65,10 @@ describe('api-utils', () => {

it('does not set auth0-scope header when scope array is empty', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});

await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] });

Expand All @@ -55,7 +78,10 @@ describe('api-utils', () => {

it('does not set auth0-scope header when authParams is undefined', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});

await fetcher('https://example.com/api', { method: 'GET' }, undefined);

Expand All @@ -65,7 +91,10 @@ describe('api-utils', () => {

it('preserves existing headers from init', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});
const customHeaders = new Headers({ 'X-Custom': 'value' });

await fetcher(
Expand All @@ -82,7 +111,10 @@ describe('api-utils', () => {

it('preserves other init options', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher();
const fetcher = createProxyFetcher({
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});
const body = JSON.stringify({ data: 'test' });

await fetcher(
Expand All @@ -96,6 +128,62 @@ describe('api-utils', () => {
expect(requestInit?.body).toBe(body);
expect(requestInit?.credentials).toBe('include');
});

it('sets Auth0-Client telemetry header with proxy mode', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher({
telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react' },
getComponent: () => 'user-mfa-management',
});

await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined);

const [, requestInit] = mockFetch.mock.calls[0]!;
const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER);
expect(header).toBeTruthy();

const decoded = JSON.parse(atob(header!));
expect(decoded.is_proxy_mode).toBe(true);
expect(decoded.component).toBe('user-mfa-management');
expect(decoded.name).toBe('universal-components');
expect(decoded.css).toBe('tailwind');
expect(decoded.distribution).toBe('npm');
expect(decoded.framework).toBe('react');
});

it('uses component from getComponent callback', async () => {
const mockFetch = stubFetch();
const fetcher = createProxyFetcher({
telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react' },
getComponent: () => 'organization-sso-configuration',
});

await fetcher('https://example.com/my-org/identity-providers', { method: 'GET' }, undefined);

const [, requestInit] = mockFetch.mock.calls[0]!;
const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER);
const decoded = JSON.parse(atob(header!));
expect(decoded.component).toBe('organization-sso-configuration');
expect(decoded.css).toBe('scoped');
expect(decoded.distribution).toBe('shadcn');
});

it('uses custom fetcher when provided', async () => {
const customFetcher = vi.fn().mockResolvedValue(new Response());
const fetcher = createProxyFetcher({
customFetcher,
telemetry: defaultTelemetry,
getComponent: mockGetComponent,
});

await fetcher('https://example.com/api', { method: 'GET' }, undefined);

expect(customFetcher).toHaveBeenCalledWith(
'https://example.com/api',
expect.objectContaining({ method: 'GET' }),
undefined,
);
});
});

describe('createSpaFetcher', () => {
Expand Down Expand Up @@ -124,14 +212,19 @@ describe('api-utils', () => {
const config = createSpaConfig();
const dpopNonceId = '__test_dpop_nonce__';

createSpaFetcher(config, dpopNonceId);
createSpaFetcher(config, dpopNonceId, defaultTelemetry, mockGetComponent);

expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId });
});

it('sets Content-Type header to application/json', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);

await fetcher('https://example.com/api', { method: 'POST' }, undefined);

Expand All @@ -141,7 +234,12 @@ describe('api-utils', () => {

it('preserves existing headers from init when adding Content-Type', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);
const customHeaders = new Headers({ 'X-Custom': 'value' });

await fetcher(
Expand All @@ -158,7 +256,12 @@ describe('api-utils', () => {

it('preserves other init options when adding Content-Type header', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);
const body = JSON.stringify({ data: 'test' });

await fetcher(
Expand All @@ -176,7 +279,12 @@ describe('api-utils', () => {

it('delegates to SDK fetchWithAuth with scope and audience', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);

await fetcher(
'https://example.com/api',
Expand All @@ -193,7 +301,12 @@ describe('api-utils', () => {

it('handles undefined authParams', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);

await fetcher('https://example.com/api', { method: 'GET' }, undefined);

Expand All @@ -206,7 +319,12 @@ describe('api-utils', () => {

it('handles empty scope array', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);

await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] });

Expand All @@ -219,7 +337,12 @@ describe('api-utils', () => {

it('handles undefined init parameter', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(config, '__test_nonce__');
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
defaultTelemetry,
mockGetComponent,
);

await fetcher('https://example.com/api', undefined, { scope: ['read:users'] });

Expand All @@ -232,5 +355,48 @@ describe('api-utils', () => {
},
);
});

it('sets Auth0-Client telemetry header with SPA mode', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
{ css: 'tailwind', distribution: 'npm', framework: 'react' },
() => 'user-mfa-management',
);

await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined);

const [, requestInit] = mockFetchWithAuth.mock.calls[0]!;
const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER);
expect(header).toBeTruthy();

const decoded = JSON.parse(atob(header!));
expect(decoded.is_proxy_mode).toBe(false);
expect(decoded.component).toBe('user-mfa-management');
expect(decoded.name).toBe('universal-components');
expect(decoded.css).toBe('tailwind');
expect(decoded.distribution).toBe('npm');
expect(decoded.framework).toBe('react');
});

it('uses component from getComponent callback', async () => {
const config = createSpaConfig();
const fetcher = createSpaFetcher(
config,
'__test_nonce__',
{ css: 'scoped', distribution: 'shadcn', framework: 'react' },
() => 'organization-domain-management',
);

await fetcher('https://example.com/my-org/domains', { method: 'GET' }, undefined);

const [, requestInit] = mockFetchWithAuth.mock.calls[0]!;
const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER);
const decoded = JSON.parse(atob(header!));
expect(decoded.component).toBe('organization-domain-management');
expect(decoded.css).toBe('scoped');
expect(decoded.distribution).toBe('shadcn');
});
});
});
Loading
Loading