Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fifty-berries-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Introduce request middleware feature to DaVinci Client
23 changes: 20 additions & 3 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import './style.css';
import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk';
import { davinci } from '@forgerock/davinci-client';

import type { DaVinciConfig } from '@forgerock/davinci-client/types';
import type { DaVinciConfig, RequestMiddleware } from '@forgerock/davinci-client/types';

import usernameComponent from './components/text.js';
import passwordComponent from './components/password.js';
Expand All @@ -18,10 +18,27 @@ const searchParams = new URLSearchParams(qs);
const config: DaVinciConfig =
serverConfigs[searchParams.get('clientId') || '724ec718-c41c-4d51-98b0-84a583f450f9'];

const requestMiddleware: RequestMiddleware[] = [
(fetchArgs, action, next) => {
if (action.type === 'DAVINCI_START') {
fetchArgs.url.searchParams.set('start', 'true');
fetchArgs.headers?.set('Accept-Language', 'xx-XX');
}
next();
},
(fetchArgs, action, next) => {
if (action.type === 'DAVINCI_NEXT') {
fetchArgs.url.searchParams.set('next', 'true');
fetchArgs.headers?.set('Accept-Language', 'zz-ZZ');
}
next();
},
];

const urlParams = new URLSearchParams(window.location.search);

(async () => {
const davinciClient = await davinci({ config });
const davinciClient = await davinci({ config, requestMiddleware });
const continueToken = urlParams.get('continueToken');
const formEl = document.getElementById('form') as HTMLFormElement;
let resumed: any;
Expand Down Expand Up @@ -181,7 +198,7 @@ const urlParams = new URLSearchParams(window.location.search);
* It returns an unsubscribe function that you can call to stop listening
*/
davinciClient.subscribe(() => {
const node = davinciClient.getClient();
const node = davinciClient.getNode();
console.log('Event emitted from store:', node);
});

Expand Down
9 changes: 5 additions & 4 deletions e2e/davinci-suites/src/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { password, username } from './utils/demo-user.js';

test('Test happy paths on test page', async ({ page }) => {
const { navigate } = asyncEvents(page);
Expand All @@ -9,8 +10,8 @@ test('Test happy paths on test page', async ({ page }) => {

await expect(page.getByText('Username/Password Form')).toBeVisible();

await page.getByLabel('Username').fill('demouser');
await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh');
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Sign On' }).click();

Expand Down Expand Up @@ -68,8 +69,8 @@ test('ensure query params passed to start are sent off in authorize call', async

await expect(page.getByText('Username/Password Form')).toBeVisible();

await page.getByLabel('Username').fill('demouser');
await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh');
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);

await page.getByText('Sign On').click();

Expand Down
2 changes: 1 addition & 1 deletion e2e/davinci-suites/src/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test('Test happy paths on test page', async ({ page }) => {

await expect(page.getByText('Username/Password Form')).toBeVisible();

await page.getByLabel('Username').fill('demouser');
await page.getByLabel('Username').fill('baduser');
await page.getByLabel('Password').fill('badpassword');

await page.getByText('Sign On').click();
Expand Down
35 changes: 35 additions & 0 deletions e2e/davinci-suites/src/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';

test('Test middleware on test page', async ({ page }) => {
const networkArray = [];
page.on('request', async (req) => {
const url = req.url().toString();
const langHeader = await req.headerValue('Accept-Language');
if (url.includes('https://auth.pingone.ca')) {
networkArray.push({ url, langHeader });
}
});

const { navigate } = asyncEvents(page);
await navigate('/');

expect(page.url()).toBe('http://localhost:5829/');

await expect(page.getByText('Username/Password Form')).toBeVisible();

const startRequest = networkArray.find((req) => req.url.includes('/authorize'));
const nextRequest = networkArray.find((req) => req.url.includes('/customHTMLTemplate'));

// Check for addition of query params
await expect(startRequest.url.includes('start=true')).toBeTruthy();
await expect(startRequest.url.includes('next=true')).toBeFalsy();
await expect(nextRequest.url.includes('next=true')).toBeTruthy();
await expect(nextRequest.url.includes('start=true')).toBeFalsy();

// Check that Accept-Language header was modified from default en-US locale
await expect(startRequest.langHeader).not.toContain('en-US');
await expect(startRequest.langHeader).toBe('xx-XX');
await expect(nextRequest.langHeader).not.toContain('en-US');
await expect(nextRequest.langHeader).toBe('zz-ZZ');
});
3 changes: 2 additions & 1 deletion e2e/davinci-suites/src/register.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { password } from './utils/demo-user.js';

test('Test happy paths on test page', async ({ page }) => {
const { navigate } = asyncEvents(page);
Expand All @@ -18,7 +19,7 @@ test('Test happy paths on test page', async ({ page }) => {
await page.getByLabel('First Name').fill('Bruce');
await page.getByLabel('Last Name').fill('Wayne');
await page.getByLabel('Email').fill(`${randomEmailPrefix}@autogenerated.com`);
await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh');
await page.getByLabel('Password').fill(password);

await page.getByText('Save').click();

Expand Down
2 changes: 2 additions & 0 deletions e2e/davinci-suites/src/utils/demo-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const username = 'demouser';
export const password = 'U.QPDWEN47ZMyJhCDmhGLK*nr';
11 changes: 9 additions & 2 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { SingleValueCollectors, IdpCollector } from './collector.types.js';
import type { InitFlow, Updater } from './client.types.js';
import { returnValidator } from './collector.utils.js';
import { authorize } from './davinci.utils.js';
import type { RequestMiddleware } from './effects/request.effect.types.js';

/**
* Create a client function that returns a set of methods
Expand All @@ -30,8 +31,14 @@ import { authorize } from './davinci.utils.js';
* @param {ConfigurationOptions} options - the configuration options for the client
* @returns {Observable} - an observable client for DaVinci flows
*/
export async function davinci({ config }: { config: DaVinciConfig }) {
const store = createClientStore();
export async function davinci({
config,
requestMiddleware,
}: {
config: DaVinciConfig;
requestMiddleware?: RequestMiddleware[];
}) {
const store = createClientStore({ requestMiddleware });

if (!config.serverConfig.wellknown) {
throw new Error('`wellknown` property is a required as part of the `config.serverOptions`');
Expand Down
22 changes: 20 additions & 2 deletions packages/davinci-client/src/lib/client.store.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { davinciApi } from './davinci.api.js';
import { ErrorNode, ContinueNode, StartNode, SuccessNode } from '../types.js';
import { wellknownApi } from './wellknown.api.js';

export function createClientStore() {
import type { RequestMiddleware } from './effects/request.effect.types.js';

export function createClientStore({
requestMiddleware,
}: {
requestMiddleware?: RequestMiddleware[];
}) {
return configureStore({
reducer: {
config: configSlice.reducer,
Expand All @@ -15,7 +21,19 @@ export function createClientStore() {
[wellknownApi.reducerPath]: wellknownApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(davinciApi.middleware).concat(wellknownApi.middleware),
getDefaultMiddleware({
thunk: {
extraArgument: {
/**
* This becomes the `api.extra` argument, and will be passed into the
* customer query wrapper for `baseQuery`
*/
requestMiddleware: requestMiddleware,
},
},
})
.concat(davinciApi.middleware)
.concat(wellknownApi.middleware),
});
}

Expand Down
56 changes: 44 additions & 12 deletions packages/davinci-client/src/lib/davinci.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
* Import the RTK Query library from Redux Toolkit
* @see https://redux-toolkit.js.org/rtk-query/overview
*/
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
import {
createApi,
FetchArgs,
fetchBaseQuery,
FetchBaseQueryError,
FetchBaseQueryMeta,
QueryReturnValue,
} from '@reduxjs/toolkit/query';

/**
* Import internal modules
Expand All @@ -22,13 +29,24 @@ import type {
} from './davinci.types.js';
import type { ContinueNode } from './node.types.js';
import type { StartNode } from '../types.js';
import { initQuery } from './effects/request.effect.utils.js';
import { RequestMiddleware } from './effects/request.effect.types.js';

type BaseQueryResponse = Promise<
QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>
>;

interface Extras {
requestMiddleware: RequestMiddleware[];
}

/**
* @const davinciApi - Define the DaVinci API for Redux state management
* @see https://redux-toolkit.js.org/rtk-query/overview
*/
export const davinciApi = createApi({
reducerPath: 'davinci',
// TODO: implement extraOptions for request interceptors: https://stackoverflow.com/a/77569083 & https://stackoverflow.com/a/65129117
baseQuery: fetchBaseQuery({
prepareHeaders: (headers) => {
headers.set('Accept', 'application/json');
Expand All @@ -49,14 +67,14 @@ export const davinciApi = createApi({
const state = api.getState() as RootStateWithNode<ContinueNode>;
const links = state.node.server._links;
const requestBody = transformActionRequest(state.node, params.action);
const requestMiddleware = (api.extra as Extras).requestMiddleware;

let href = '';

if (links && 'next' in links) {
href = links['next'].href || '';
}

const response = await baseQuery({
const request: FetchArgs = {
// TODO: If we don't have a `next.href`, we should handle this better
url: href,
credentials: 'include',
Expand All @@ -67,7 +85,10 @@ export const davinciApi = createApi({
interactionToken: state.node.server.interactionToken,
},
body: JSON.stringify(requestBody),
});
};
const response: BaseQueryResponse = initQuery(request, 'flow')
.applyMiddleware(requestMiddleware)
.applyQuery(async (req: FetchArgs) => await baseQuery(req));

/**
* Returns the original response from DaVinci,
Expand Down Expand Up @@ -120,6 +141,7 @@ export const davinciApi = createApi({
async queryFn(body, api, __, baseQuery) {
const state = api.getState() as RootStateWithNode<ContinueNode>;
const links = state.node.server._links;
const requestMiddleware = (api.extra as Extras).requestMiddleware;

let requestBody;
let href = '';
Expand All @@ -134,7 +156,7 @@ export const davinciApi = createApi({
requestBody = body;
}

const response = await baseQuery({
const request: FetchArgs = {
url: href,
credentials: 'include',
method: 'POST',
Expand All @@ -144,7 +166,10 @@ export const davinciApi = createApi({
interactionToken: state.node.server.interactionToken,
},
body: JSON.stringify(requestBody),
});
};
const response: BaseQueryResponse = initQuery(request, 'next')
.applyMiddleware(requestMiddleware)
.applyQuery(async (req: FetchArgs) => await baseQuery(req));

/**
* Returns the original response from DaVinci,
Expand Down Expand Up @@ -196,6 +221,7 @@ export const davinciApi = createApi({
* @method queryFn - This is just a wrapper around the fetch call
*/
async queryFn(options, api, __, baseQuery) {
const requestMiddleware = (api.extra as Extras).requestMiddleware;
const state = api.getState() as RootStateWithNode<StartNode>;

if (!state) {
Expand Down Expand Up @@ -236,14 +262,17 @@ export const davinciApi = createApi({
url.search = existingParams.toString();
}

const response = await baseQuery({
const request: FetchArgs = {
url: url.toString(),
credentials: 'include',
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
};
const response: BaseQueryResponse = initQuery(request, 'start')
.applyMiddleware(requestMiddleware)
.applyQuery(async (req: FetchArgs) => await baseQuery(req));

/**
* Returns the original response from DaVinci,
Expand Down Expand Up @@ -291,8 +320,9 @@ export const davinciApi = createApi({
},
}),
resume: builder.query<unknown, { continueToken: string }>({
async queryFn({ continueToken }, _api, _c, baseQuery) {
async queryFn({ continueToken }, api, _c, baseQuery) {
const continueUrl = window.localStorage.getItem('continueUrl') || null;
const requestMiddleware = (api.extra as Extras).requestMiddleware;

if (!continueToken) {
return {
Expand All @@ -319,7 +349,7 @@ export const davinciApi = createApi({
window.localStorage.removeItem('continueUrl');
}

const response = await baseQuery({
const request: FetchArgs = {
url: continueUrl,
credentials: 'include',
method: 'POST',
Expand All @@ -328,7 +358,10 @@ export const davinciApi = createApi({
Authorization: `Bearer ${continueToken}`,
},
body: JSON.stringify({}),
});
};
const response: BaseQueryResponse = initQuery(request, 'resume')
.applyMiddleware(requestMiddleware)
.applyQuery(async (req: FetchArgs) => await baseQuery(req));

return response;
},
Expand All @@ -349,7 +382,6 @@ export const davinciApi = createApi({
}

const cacheEntry: DaVinciCacheEntry = api.getCacheEntry();
console.log('resumed handling repsonse');
handleResponse(cacheEntry, api.dispatch, response?.status || 0);
},
}),
Expand Down
Loading