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/gorgeous-cats-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

adds the ability to call start with query parameters which are appended to the /authorize call
21 changes: 18 additions & 3 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +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 usernameComponent from './components/text.js';
import passwordComponent from './components/password.js';
Expand All @@ -10,9 +11,9 @@ import protect from './components/protect.js';
import flowLinkComponent from './components/flow-link.js';
import socialLoginButtonComponent from './components/social-login-button.js';

const config = {
const config: DaVinciConfig = {
clientId: '724ec718-c41c-4d51-98b0-84a583f450f9',
redirectUri: window.location.href,
redirectUri: window.location.origin + '/',
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because i added query params to the testing, i just appended the '/' since it's stripped when using origin.

scope: 'openid profile email name revoke',
serverConfig: {
wellknown:
Expand Down Expand Up @@ -178,7 +179,21 @@ const config = {
console.log('Event emitted from store:', node);
});

const node = await davinciClient.start();
const qs = window.location.search;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic so that we can pull query params dynamically in tests

const searchParams = new URLSearchParams(qs);

const query: Record<string, string | string[]> = {};

// Get all unique keys from the searchParams
const uniqueKeys = new Set(searchParams.keys());

// Iterate over the unique keys
for (const key of uniqueKeys) {
const values = searchParams.getAll(key);
query[key] = values.length > 1 ? values : values[0];
}
console.log('query', query);
const node = await davinciClient.start({ query });

formEl.addEventListener('submit', async (event) => {
event.preventDefault();
Expand Down
1 change: 1 addition & 0 deletions e2e/davinci-app/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"moduleResolution": "NodeNext",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this was me debugging but should be a noop since this affects test files.

"composite": true,
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
Expand Down
42 changes: 42 additions & 0 deletions e2e/davinci-suites/src/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,45 @@ test('Test happy paths on test page', async ({ page }) => {
const accessToken = await page.locator('#accessTokenValue').innerText();
await expect(accessToken).toBeTruthy();
});
test('ensure query params passed to start are sent off in authorize call', async ({ page }) => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy pasted above test, passed in a query param and then also added a few other query params from authorize specifically to make sure thats what we are testing

const { navigate } = asyncEvents(page);
// Wait for the request to a URL containing '/authorize'
const requestPromise = page.waitForRequest((request) => {
return request
.url()
.includes('https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize');
});
await navigate('/?testParam=123');

// Wait for the request to be made to authorize
const request = await requestPromise;

// Extract and verify the query parameters from authorize
const url = new URL(request.url());
const queryParams = Object.fromEntries(url.searchParams.entries());

expect(queryParams['testParam']).toBe('123');
expect(queryParams['client_id']).toBe('724ec718-c41c-4d51-98b0-84a583f450f9');
expect(queryParams['response_mode']).toBe('pi.flow');

expect(page.url()).toBe('http://localhost:5829/?testParam=123');

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

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

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

await expect(page.getByText('Complete')).toBeVisible();

const sessionToken = await page.locator('#sessionToken').innerText();
const authCode = await page.locator('#authCode').innerText();
expect(sessionToken).toBeTruthy();
expect(authCode).toBeTruthy();

await page.getByText('Get Tokens').click();

const accessToken = await page.locator('#accessTokenValue').innerText();
expect(accessToken).toBeTruthy();
});
13 changes: 10 additions & 3 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { wellknownApi } from './wellknown.api.js';
* Import the DaVinciRequest types
*/
import type { DaVinciConfig } from './config.types.js';
import type { DaVinciAction, DaVinciRequest } from './davinci.types.js';
import type {
DaVinciAction,
DaVinciRequest,
OutgoingQueryParams,
StartOptions,
} from './davinci.types.js';
import type { SingleValueCollectors } from './collector.types.js';
import type { InitFlow, Updater } from './client.types.js';

Expand Down Expand Up @@ -94,8 +99,10 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
* @method start - Method for initiating a DaVinci flow
* @returns {Promise} - a promise that initiates a DaVinci flow and returns a node
*/
start: async () => {
await store.dispatch(davinciApi.endpoints.start.initiate());
start: async <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(
options?: StartOptions<QueryParams> | undefined,
) => {
await store.dispatch(davinciApi.endpoints.start.initiate(options));
return store.getState().node;
},

Expand Down
28 changes: 23 additions & 5 deletions packages/davinci-client/src/lib/davinci.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { handleResponse, transformActionRequest, transformSubmitRequest } from '
* Import the DaVinci types
*/
import type { RootStateWithNode } from './client.store.utils.js';
import type { DaVinciCacheEntry, ThrownQueryError } from './davinci.types.js';
import type {
DaVinciCacheEntry,
OutgoingQueryParams,
StartOptions,
ThrownQueryError,
} from './davinci.types';
import type { ContinueNode } from './node.types.js';
import type { StartNode } from '../types.js';

Expand Down Expand Up @@ -186,11 +191,11 @@ export const davinciApi = createApi({
* @method start - method for initiating a DaVinci flow
* @param - needs no arguments, but need to declare types to make it explicit
*/
start: builder.mutation<unknown, void>({
start: builder.mutation<unknown, StartOptions<OutgoingQueryParams> | undefined>({
/**
* @method queryFn - This is just a wrapper around the fetch call
*/
async queryFn(_, api, __, baseQuery) {
async queryFn(options, api, __, baseQuery) {
const state = api.getState() as RootStateWithNode<StartNode>;

if (!state) {
Expand All @@ -216,9 +221,23 @@ export const davinciApi = createApi({
responseType: state?.config?.responseType,
scope: state?.config?.scope,
});
const url = new URL(authorizeUrl);
const existingParams = url.searchParams;

if (options?.query) {
Object.entries(options.query).forEach(([key, value]) => {
/**
* We use set here because if we have existing params, we want
* to make sure we override them and not add duplicates
*/
existingParams.set(key, String(value));
});

url.search = existingParams.toString();
}

const response = await baseQuery({
url: authorizeUrl,
url: url.toString(),
credentials: 'include',
method: 'GET',
headers: {
Expand Down Expand Up @@ -268,7 +287,6 @@ export const davinciApi = createApi({
}

const cacheEntry: DaVinciCacheEntry = api.getCacheEntry();

handleResponse(cacheEntry, api.dispatch, response?.status || 0);
},
}),
Expand Down
8 changes: 8 additions & 0 deletions packages/davinci-client/src/lib/davinci.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,11 @@ export interface ThrownQueryError {
isHandledError: boolean;
meta: FetchBaseQueryMeta;
}

export interface StartOptions<Query extends OutgoingQueryParams = OutgoingQueryParams> {
query: Query;
}
// Outgoing query parameters (sent in the request)
export interface OutgoingQueryParams {
[key: string]: string | string[];
}
1 change: 1 addition & 0 deletions packages/davinci-client/src/lib/davinci.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
DaVinciNextResponse,
DaVinciRequest,
DaVinciSuccessResponse,
StartOptions,
} from './davinci.types';
import type { ContinueNode } from './node.types';

Expand Down
6 changes: 5 additions & 1 deletion packages/davinci-client/src/lib/node.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ export const nodeSlice = createSlice({
*/
next(
state,
action: PayloadAction<{ data: DaVinciNextResponse; requestId: string; httpStatus: number }>,
action: PayloadAction<{
data: DaVinciNextResponse;
requestId: string;
httpStatus: number;
}>,
) {
const newState = state as Draft<ContinueNode>;

Expand Down