Skip to content

Commit 0ab715e

Browse files
authored
Merge pull request #365 from ForgeRock/mock-api-routes
chore: update-routes
2 parents 777e771 + 73f522d commit 0ab715e

31 files changed

+685
-919
lines changed

e2e/mock-api-v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@effect/language-service": "catalog:effect",
17-
"@effect/opentelemetry": "0.53.1",
17+
"@effect/opentelemetry": "catalog:effect",
1818
"@effect/platform": "catalog:effect",
1919
"@effect/platform-node": "catalog:effect",
2020
"@opentelemetry/sdk-logs": "0.202.0",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
export const addStepCookie = (spec: any) => {
9+
const FLOW_TAGS = new Set(['Authorization', 'Capabilities']);
10+
const FLOW_PATH_MATCHERS = [
11+
/\/davinci\/authorize\b/,
12+
/\/davinci\/connections\/[^/]+\/capabilities\//,
13+
];
14+
15+
const shouldAnnotate = (path: string, op: any) =>
16+
(Array.isArray(op?.tags) && op.tags.some((t: string) => FLOW_TAGS.has(t))) ||
17+
FLOW_PATH_MATCHERS.some((rx) => rx.test(path));
18+
19+
const addCookieParam = (op: any) => {
20+
op.parameters ||= [];
21+
const already = op.parameters.some(
22+
(p: any) => p && p.in === 'cookie' && p.name === 'stepIndex',
23+
);
24+
if (!already) {
25+
op.parameters.push({
26+
name: 'stepIndex',
27+
in: 'cookie',
28+
required: false,
29+
description:
30+
'Current flow step. Server initializes on first request and increments thereafter.',
31+
schema: { type: 'integer', minimum: 0 },
32+
example: 2,
33+
});
34+
}
35+
};
36+
37+
const ensureSetCookie = (op: any, status: string) => {
38+
op.responses ||= {};
39+
const resp = (op.responses[status] ||= { description: 'Success' });
40+
resp.headers ||= {};
41+
if (!resp.headers['Set-Cookie']) {
42+
resp.headers['Set-Cookie'] = {
43+
description:
44+
'Updated step cookie (e.g., `stepIndex=3; Path=/; HttpOnly; Secure; SameSite=Lax`). ' +
45+
'May be removed on completion.',
46+
schema: { type: 'string' },
47+
example: 'stepIndex=3; Path=/; HttpOnly; Secure; SameSite=Lax',
48+
};
49+
}
50+
};
51+
52+
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
53+
for (const [, op] of Object.entries<any>(methods as any)) {
54+
if (!op || typeof op !== 'object') continue;
55+
if (!shouldAnnotate(path, op)) continue;
56+
addCookieParam(op);
57+
// Add for common success statuses you use
58+
ensureSetCookie(op, '200');
59+
ensureSetCookie(op, '302');
60+
}
61+
}
62+
63+
return spec;
64+
};
65+
66+
export default addStepCookie;

e2e/mock-api-v2/src/errors/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
class InvalidUsernamePassword {
8-
readonly _tag = 'InvalidUsernamePassword';
9-
}
107

118
class FetchError {
129
readonly _tag = 'FetchError';
@@ -20,4 +17,4 @@ class UnableToFindNextStep {
2017
readonly _tag = 'UnableToFindNextStep';
2118
}
2219

23-
export { FetchError, InvalidUsernamePassword, InvalidProtectNode, UnableToFindNextStep };
20+
export { FetchError, InvalidProtectNode, UnableToFindNextStep };

e2e/mock-api-v2/src/handlers/authorize.handler.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,26 @@
66
*/
77
import { Effect } from 'effect';
88

9-
import { Authorize } from '../services/authorize.service.js';
109
import { MockApi } from '../spec.js';
11-
import { HttpApiBuilder } from '@effect/platform';
10+
import { HttpApiBuilder, HttpApiError } from '@effect/platform';
11+
import { getFirstElementAndRespond } from '../services/mock-env-helpers/index.js';
1212

1313
const AuthorizeHandlerMock = HttpApiBuilder.group(MockApi, 'Authorization', (handlers) =>
14-
handlers.handle('DavinciAuthorize', ({ urlParams }) =>
14+
handlers.handle('authorize', ({ urlParams }) =>
1515
Effect.gen(function* () {
16-
const { handleAuthorize } = yield* Authorize;
16+
/**
17+
* We expect an acr_value query parameter to be present in the request.
18+
* If it is not present, we return a 404 Not Found error.
19+
*/
20+
const acr_value = urlParams?.acr_values ?? '';
1721

18-
const response = yield* handleAuthorize(urlParams);
22+
if (!acr_value) {
23+
return yield* Effect.fail(new HttpApiError.NotFound());
24+
}
1925

20-
return response.body;
26+
const response = yield* getFirstElementAndRespond(urlParams);
27+
28+
return response;
2129
}).pipe(Effect.withSpan('DavinciAuthorize')),
2230
),
2331
);
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { Console, Effect, pipe } from 'effect';
8+
import { MockApi } from '../spec.js';
9+
import {
10+
HttpApiBuilder,
11+
HttpApiError,
12+
HttpBody,
13+
HttpServerRequest,
14+
HttpServerResponse,
15+
} from '@effect/platform';
16+
import { responseMap } from '../responses/index.js';
17+
import { validator } from '../helpers/match.js';
18+
import { returnSuccessResponseRedirect } from '../responses/return-success-redirect.js';
19+
20+
const CapabilitiesHandlerMock = HttpApiBuilder.group(MockApi, 'Capabilities', (handlers) =>
21+
handlers.handle('capabilities', ({ urlParams, payload }) =>
22+
Effect.gen(function* () {
23+
/**
24+
* We expect an acr_value query parameter to be present in the request.
25+
* If it is not present, we return a 404 Not Found error.
26+
*/
27+
const acr_value = urlParams?.acr_values ?? '';
28+
console.log('acr_value', acr_value);
29+
30+
if (!acr_value) {
31+
return yield* Effect.fail(new HttpApiError.NotFound());
32+
}
33+
34+
/**
35+
* We need a step index cookie to determine which step of the authentication process we are on.
36+
* If the cookie is not present, we return a 404 Not Found error.
37+
*/
38+
39+
const req = yield* HttpServerRequest.HttpServerRequest;
40+
41+
const stepIndexCookie = req.cookies['stepIndex'];
42+
console.log(req.cookies);
43+
44+
/**
45+
* If we are here with no step index that means we can't continue through a flow.
46+
* We should error
47+
*/
48+
if (!stepIndexCookie) {
49+
console.log('no step index');
50+
return yield* Effect.fail(new HttpApiError.NotFound());
51+
}
52+
53+
const stepIndex = parseInt(stepIndexCookie);
54+
55+
/**
56+
* If we have no step index, we should error or if its an invalid number
57+
*/
58+
59+
if (isNaN(stepIndex) || stepIndex < 0) {
60+
return yield* Effect.fail(new HttpApiError.NotFound());
61+
}
62+
63+
/**
64+
* Match the body against validators now
65+
* if the body has no match, we are defaulting to a successful response.
66+
*/
67+
const result = yield* validator(payload);
68+
69+
if (result === false) {
70+
return yield* Effect.fail(new HttpApiError.Unauthorized());
71+
}
72+
73+
/**
74+
* We use the step index to find the next step in the response map.
75+
* If the step index is out of bounds, we return a 404 Not Found error.
76+
*/
77+
const steps = responseMap[acr_value];
78+
79+
/**
80+
* This may not be the best way to write this.
81+
* An alternative option would be for us to include the success response we want to return,
82+
* in the response map.
83+
*
84+
* then we can check if we are at the last step. if we are we write the cookie
85+
* and then we return the success response (last item in array)
86+
*
87+
* for now, this returns a default success response and writes cookies.
88+
*/
89+
if (stepIndex + 1 >= steps.length) {
90+
/**
91+
* we need to return a success because we have not failed yet,
92+
* and we have no more steps to process.
93+
*/
94+
const body = yield* HttpBody.json(returnSuccessResponseRedirect).pipe(
95+
Effect.tap(Console.log(`here stepIndex: ${stepIndex}`)),
96+
/**
97+
* Decide on a better way to handle this error possibiltiy
98+
*/
99+
Effect.catchTag('HttpBodyError', () =>
100+
Effect.fail(
101+
new HttpApiError.HttpApiDecodeError({
102+
message: 'Failed to encode body',
103+
issues: [],
104+
}),
105+
),
106+
),
107+
);
108+
return pipe(
109+
HttpServerResponse.json(body),
110+
HttpServerResponse.setCookie('ST', 'MockApiCookie123'),
111+
HttpServerResponse.setCookie(
112+
'interactionId',
113+
returnSuccessResponseRedirect.interactionId,
114+
{
115+
httpOnly: true,
116+
secure: true,
117+
sameSite: 'strict',
118+
},
119+
),
120+
HttpServerResponse.setCookie(
121+
'interactionToken',
122+
returnSuccessResponseRedirect.interactionToken,
123+
{
124+
httpOnly: true,
125+
secure: true,
126+
sameSite: 'strict',
127+
},
128+
),
129+
HttpServerResponse.removeCookie('stepIndex'),
130+
HttpServerResponse.setStatus(200),
131+
HttpServerResponse.setHeader('Content-Type', 'application/json'),
132+
);
133+
}
134+
135+
/**
136+
* The stepIndex middleware is used to auto-increment the step index
137+
* based on the request type. If the step index is out of bounds,
138+
* we return a 404 Not Found error. so we won't increment it, but we check for the next step
139+
* in the flow.
140+
*/
141+
const nextStep = steps[stepIndex + 1];
142+
143+
return nextStep;
144+
}).pipe(Effect.withSpan('Capabilities Handler Mock')),
145+
),
146+
);
147+
148+
export { CapabilitiesHandlerMock };

e2e/mock-api-v2/src/handlers/custom-html-template.handler.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

e2e/mock-api-v2/src/handlers/revoke.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Tokens } from '../services/tokens.service.js';
99
import { HttpApiBuilder } from '@effect/platform';
1010
import { Effect } from 'effect';
1111

12-
const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'TokenRevocation', (handlers) =>
12+
const RevokeTokenHandler = HttpApiBuilder.group(MockApi, 'Revoke', (handlers) =>
1313
handlers.handle('RevokeToken', () =>
1414
Effect.gen(function* () {
1515
const { revokeToken } = yield* Tokens;

e2e/mock-api-v2/src/handlers/userinfo.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { UserInfo } from '../services/userinfo.service.js';
1010
import { HttpApiBuilder } from '@effect/platform';
1111
import { BearerToken } from '../middleware/Authorization.js';
1212

13-
const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'Protected Requests', (handlers) =>
13+
const UserInfoMockHandler = HttpApiBuilder.group(MockApi, 'ProtectedRequests', (handlers) =>
1414
handlers.handle('UserInfo', () =>
1515
Effect.gen(function* () {
1616
const authToken = yield* BearerToken;

e2e/mock-api-v2/src/helpers/match.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
*/
77
import { Effect, Match, Schema } from 'effect';
88

9-
import { InvalidUsernamePassword, InvalidProtectNode } from '../errors/index.js';
10-
import { PingOneCustomHtmlRequestBody } from '../schemas/custom-html-template/custom-html-template-request.schema.js';
9+
import { HttpApiError } from '@effect/platform';
10+
import { CapabilitiesRequestBody } from '../schemas/capabilities/capabilities.request.schema.js';
1111

12-
type PingRequestData = Schema.Schema.Type<
13-
typeof PingOneCustomHtmlRequestBody
14-
>['parameters']['data']['formData']['value'];
12+
type PingRequestData = Schema.Schema.Type<typeof CapabilitiesRequestBody>;
1513
/**
1614
* Using this to match on the data types, realistically, this will be a schema of possible
1715
* response bodies we want to validate against they validate to our conditions.
@@ -20,18 +18,19 @@ type PingRequestData = Schema.Schema.Type<
2018
* or we can continue to the next step in the flow
2119
*/
2220
const validator = Match.type<PingRequestData>().pipe(
23-
Match.when({ username: Match.string, password: Match.string }, ({ username, password }) => {
24-
return Effect.if(username == 'testuser' && password === 'Password', {
25-
onFalse: () => Effect.fail(new InvalidUsernamePassword()),
26-
onTrue: () => Effect.succeed(true),
27-
});
28-
}),
29-
Match.when({ pingprotectsdk: Match.string }, ({ pingprotectsdk }) => {
30-
return Effect.if(pingprotectsdk.length > 1, {
31-
onTrue: () => Effect.succeed(true),
32-
onFalse: () => Effect.fail(new InvalidProtectNode()),
33-
});
34-
}),
35-
Match.exhaustive,
21+
Match.when(
22+
{ parameters: { data: { formData: { username: Match.string, password: Match.string } } } },
23+
({ parameters }) =>
24+
Effect.if(
25+
parameters.data.formData.username == 'testuser' &&
26+
parameters.data.formData.password === 'Password',
27+
{
28+
onFalse: () => Effect.fail(new HttpApiError.Unauthorized()),
29+
onTrue: () => Effect.succeed(true),
30+
},
31+
),
32+
),
33+
Match.orElse(() => Effect.succeed(true)),
3634
);
35+
3736
export { validator, PingRequestData };

e2e/mock-api-v2/src/index.css

Whitespace-only changes.

0 commit comments

Comments
 (0)