Skip to content

Commit

Permalink
Add support for fetching static content (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospassos authored Sep 25, 2024
1 parent a12d588 commit a96f23d
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 33 deletions.
39 changes: 38 additions & 1 deletion src/config/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {cookies} from 'next/headers';
import {Token} from '@croct/sdk/token';
import {getRequestContext, RequestContext, resolveRequestContext} from '@/config/context';
import {getRequestContext, RequestContext, resolvePreferredLocale, resolveRequestContext} from '@/config/context';
import {Header} from '@/config/http';
import {getUserTokenCookieOptions} from '@/config/cookie';
import {getCookies, getHeaders, PartialRequest, PartialResponse, RouteContext} from '@/headers';
Expand Down Expand Up @@ -203,3 +203,40 @@ describe('resolveRequestContext', () => {
expect(getCookies).toHaveBeenCalledWith(route);
});
});

describe('resolvePreferredLocale', () => {
beforeEach(() => {
delete process.env.NEXT_PUBLIC_CROCT_DEFAULT_PREFERRED_LOCALE;
jest.clearAllMocks();
});

it('should return the preferred locale from the headers', () => {
const headers = new Headers();

headers.set(Header.PREFERRED_LOCALE, 'en');

const route: RouteContext = {
req: {} as PartialRequest,
res: {} as PartialResponse,
};

jest.mocked(getHeaders).mockReturnValue(headers);

expect(resolvePreferredLocale(route)).toEqual('en');
expect(getHeaders).toHaveBeenCalledWith(route);
});

it('should return the preferred locale from the environment', () => {
process.env.NEXT_PUBLIC_CROCT_DEFAULT_PREFERRED_LOCALE = 'en';

jest.mocked(getHeaders).mockReturnValue(new Headers());

expect(resolvePreferredLocale()).toEqual('en');
});

it('should return null when the preferred locale is missing', () => {
jest.mocked(getHeaders).mockReturnValue(new Headers());

expect(resolvePreferredLocale()).toBeNull();
});
});
14 changes: 11 additions & 3 deletions src/config/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ export function getRequestContext(headers: HeaderReader, cookies: CookieReader):
clientId: clientId,
};

const locale = headers.get(Header.PREFERRED_LOCALE)
?? getEnvValue(process.env.NEXT_PUBLIC_CROCT_DEFAULT_PREFERRED_LOCALE)
?? null;
const locale = getPreferredLocale(headers);

if (locale !== null) {
context.preferredLocale = locale;
Expand Down Expand Up @@ -80,6 +78,16 @@ export function getRequestContext(headers: HeaderReader, cookies: CookieReader):
return context;
}

export function resolvePreferredLocale(route?: RouteContext): string|null {
return getPreferredLocale(getHeaders(route));
}

function getPreferredLocale(headers: HeaderReader): string|null {
return headers.get(Header.PREFERRED_LOCALE)
?? getEnvValue(process.env.NEXT_PUBLIC_CROCT_DEFAULT_PREFERRED_LOCALE)
?? null;
}

/**
* Get the user token from the headers or cookies.
*
Expand Down
10 changes: 6 additions & 4 deletions src/server/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type EvaluationOptions<T extends JsonValue = JsonValue> = Omit<BaseOption
};

export function evaluate<T extends JsonValue>(query: string, options: EvaluationOptions<T> = {}): Promise<T> {
const {route, ...rest} = options;
const {route, logger, ...rest} = options;

let context: RequestContext;

Expand All @@ -32,6 +32,8 @@ export function evaluate<T extends JsonValue>(query: string, options: Evaluation
return Promise.reject(error);
}

const timeout = getDefaultFetchTimeout();

return executeQuery<T>(query, {
apiKey: getApiKey(),
clientIp: context.clientIp ?? '127.0.0.1',
Expand All @@ -40,16 +42,16 @@ export function evaluate<T extends JsonValue>(query: string, options: Evaluation
...(context.clientId !== undefined && {clientId: context.clientId}),
...(context.clientAgent !== undefined && {clientAgent: context.clientAgent}),
...getEnvEntry('baseEndpointUrl', process.env.NEXT_PUBLIC_CROCT_BASE_ENDPOINT_URL),
timeout: getDefaultFetchTimeout(),
...(timeout !== undefined && {timeout: timeout}),
extra: {
cache: 'no-store',
},
...rest,
logger: rest.logger ?? (
logger: logger ?? (
getEnvFlag(process.env.NEXT_PUBLIC_CROCT_DEBUG)
? new ConsoleLogger()
: FilteredLogger.include(new ConsoleLogger(), ['warn', 'error'])
),
...rest,
...(context.uri !== undefined
? {
context: {
Expand Down
94 changes: 81 additions & 13 deletions src/server/fetchContent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {ApiKey, ApiKey as MockApiKey} from '@croct/sdk/apiKey';
import {FilteredLogger} from '@croct/sdk/logging/filteredLogger';
import type {NextRequest, NextResponse} from 'next/server';
import {fetchContent, FetchOptions} from './fetchContent';
import {RequestContext, resolveRequestContext} from '@/config/context';
import {RequestContext, resolvePreferredLocale, resolveRequestContext} from '@/config/context';
import {getDefaultFetchTimeout} from '@/config/timeout';
import {getApiKey} from '@/config/security';
import {RouteContext} from '@/headers';
Expand Down Expand Up @@ -43,6 +43,7 @@ jest.mock(
__esModule: true,
...jest.requireActual('@/config/context'),
resolveRequestContext: jest.fn(),
resolvePreferredLocale: jest.fn(),
}),
);

Expand Down Expand Up @@ -84,7 +85,8 @@ describe('fetchContent', () => {
});

type FetchScenario = {
request: RequestContext,
request?: RequestContext,
preferredLocale?: string,
options: FetchOptions<any>,
resolvedOptions: ResolvedFetchOptions,
};
Expand Down Expand Up @@ -170,6 +172,28 @@ describe('fetchContent', () => {
logger: expect.any(FilteredLogger),
},
},
'of static content': {
options: {
static: true,
},
resolvedOptions: {
static: true,
apiKey: ApiKey.from(apiKey),
logger: expect.any(FilteredLogger),
},
},
'of static content with preferred locale': {
preferredLocale: request.preferredLocale,
options: {
static: true,
},
resolvedOptions: {
static: true,
apiKey: ApiKey.from(apiKey),
logger: expect.any(FilteredLogger),
preferredLocale: request.preferredLocale,
},
},
}))('should forward the call %s to the fetchContent function', async (_, scenario) => {
const slotId = 'slot-id';
const content: FetchResponse<any> = {
Expand All @@ -178,15 +202,26 @@ describe('fetchContent', () => {
},
};

jest.mocked(resolveRequestContext).mockReturnValue(scenario.request);
if (scenario.request === undefined) {
jest.mocked(resolveRequestContext).mockImplementation(() => {
throw new Error('next/headers requires app router');
});
} else {
jest.mocked(resolveRequestContext).mockReturnValue(scenario.request);
}

if (scenario.preferredLocale !== undefined) {
jest.mocked(resolvePreferredLocale).mockReturnValue(scenario.preferredLocale);
}

jest.mocked(loadContent).mockResolvedValue(content);

await expect(fetchContent<any, any>(slotId, scenario.options)).resolves.toEqual(content);

expect(loadContent).toHaveBeenCalledWith(slotId, scenario.resolvedOptions);
});

it('should use the default fetch timeout', async () => {
it.each([true, false])('should use the default fetch timeout (static: %s)', async staticContent => {
const defaultTimeout = 1000;
const slotId = 'slot-id';
const content: FetchResponse<any> = {
Expand All @@ -203,14 +238,17 @@ describe('fetchContent', () => {

jest.mocked(loadContent).mockResolvedValue(content);

await fetchContent<any, any>(slotId);
await fetchContent<any, any>(slotId, {
static: staticContent,
});

expect(loadContent).toHaveBeenCalledWith(slotId, expect.objectContaining({
static: staticContent,
timeout: defaultTimeout,
}));
});

it('should forward the route context', async () => {
it('should forward the route context when requesting dynamic content', async () => {
const route: RouteContext = {
req: {} as NextRequest,
res: {} as NextResponse,
Expand All @@ -230,6 +268,27 @@ describe('fetchContent', () => {
expect(resolveRequestContext).toHaveBeenCalledWith(route);
});

it('should forward the route context when requesting static content', async () => {
const route: RouteContext = {
req: {} as NextRequest,
res: {} as NextResponse,
};

jest.mocked(resolveRequestContext).mockReturnValue(request);
jest.mocked(loadContent).mockResolvedValue({
content: {
_component: 'component',
},
});

await fetchContent('slot-id', {
static: true,
route: route,
});

expect(resolvePreferredLocale).toHaveBeenCalledWith(route);
});

it('should report an error if the route context is missing', async () => {
jest.mocked(resolveRequestContext).mockImplementation(() => {
throw new Error('next/headers requires app router');
Expand All @@ -256,7 +315,7 @@ describe('fetchContent', () => {
await expect(fetchContent('true', {route: route})).rejects.toBe(error);
});

it('should override the default fetch timeout', async () => {
it.each([true, false])('should override the default fetch timeout', async staticContent => {
const defaultTimeout = 1000;
const timeout = 2000;
const slotId = 'slot-id';
Expand All @@ -275,15 +334,17 @@ describe('fetchContent', () => {
jest.mocked(loadContent).mockResolvedValue(content);

await fetchContent<any, any>(slotId, {
static: staticContent,
timeout: timeout,
});

expect(loadContent).toHaveBeenCalledWith(slotId, expect.objectContaining({
static: staticContent,
timeout: timeout,
}));
});

it('should log warnings and errors', async () => {
it.each([true, false])('should log warnings and errors', async staticContent => {
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'debug').mockImplementation();
jest.spyOn(console, 'warn').mockImplementation();
Expand All @@ -298,7 +359,9 @@ describe('fetchContent', () => {

jest.mocked(resolveRequestContext).mockReturnValue(request);

await fetchContent<any, any>('slot-id');
await fetchContent<any, any>('slot-id', {
static: staticContent,
});

const {logger} = jest.mocked(loadContent).mock.calls[0][1] as ResolvedFetchOptions;

Expand All @@ -317,7 +380,7 @@ describe('fetchContent', () => {
expect(console.log).not.toHaveBeenCalled();
});

it('should log all messages if the debug mode is enabled', async () => {
it.each([true, false])('should log all messages if the debug mode is enabled', async staticContent => {
process.env.NEXT_PUBLIC_CROCT_DEBUG = 'true';

jest.spyOn(console, 'log').mockImplementation();
Expand All @@ -334,7 +397,9 @@ describe('fetchContent', () => {

jest.mocked(resolveRequestContext).mockReturnValue(request);

await fetchContent<any, any>('slot-id');
await fetchContent<any, any>('slot-id', {
static: staticContent,
});

const {logger} = jest.mocked(loadContent).mock.calls[0][1] as ResolvedFetchOptions;

Expand All @@ -353,7 +418,7 @@ describe('fetchContent', () => {
expect(console.log).not.toHaveBeenCalled();
});

it('should use the base endpoint URL from the environment', async () => {
it.each([true, false])('should use the base endpoint URL from the environment', async staticContent => {
process.env.NEXT_PUBLIC_CROCT_BASE_ENDPOINT_URL = 'https://example.com';

const slotId = 'slot-id';
Expand All @@ -370,9 +435,12 @@ describe('fetchContent', () => {

jest.mocked(loadContent).mockResolvedValue(content);

await fetchContent<any, any>(slotId);
await fetchContent<any, any>(slotId, {
static: staticContent,
});

expect(loadContent).toHaveBeenCalledWith(slotId, expect.objectContaining({
static: staticContent,
baseEndpointUrl: process.env.NEXT_PUBLIC_CROCT_BASE_ENDPOINT_URL,
}));
});
Expand Down
Loading

0 comments on commit a96f23d

Please sign in to comment.