Skip to content

Commit

Permalink
Merge pull request #450 from ForgeRock/mock-api-v2
Browse files Browse the repository at this point in the history
Mock api v2
  • Loading branch information
ryanbas21 authored Jul 22, 2024
2 parents 7c04777 + fcee976 commit 8555085
Show file tree
Hide file tree
Showing 62 changed files with 3,780 additions and 159 deletions.
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OS files
.DS_Store
*/DS_Store

**/*.DS_Store
# Generated code
tmp/
*/dist/*
Expand Down Expand Up @@ -29,7 +29,7 @@ packages/javascript-sdk/lib/
.env.serve.development

# Certificates
*.pem
# *.pem

# IDEs
.vscode
Expand Down Expand Up @@ -62,4 +62,6 @@ docs/packages/javascript-sdk
**/playwright-report
**/playwright/.cache

.nx/cache
.nx/*
!.nx/workflows
**/vite.config.ts.timestamp-*
7 changes: 7 additions & 0 deletions e2e/autoscript-suites/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const config: PlaywrightTestConfig = {
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
{
command: 'npx nx serve e2e-mock-api-v2',
url: 'http://localhost:9444/healthcheck',
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
{
command: 'npx nx serve autoscript-apps',
url: 'http://localhost:8443',
Expand Down
25 changes: 25 additions & 0 deletions e2e/mock-api-v2/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "README.md", ".DS_Store"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@typescript-eslint/no-empty-interface": [
"error",
{
"allowSingleExtends": true
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
109 changes: 109 additions & 0 deletions e2e/mock-api-v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
### Mock Api

## Docs

you can run the server and visit `http://localhost:9443/docs#` to visit the swagger docs
The swagger docs are automatically created (with effect-http) by way of the schemas that are defined in the [spec file]('./src/spec.ts')

## Creating an endpoint in the Api specification

To create an endpoint, visit the [spec]('./src/spec.ts') file and add your endpoint. For organization and cleanliness, endpoints can be abstracted into the [endpoints]('./src/endpoints/') folder.

When an endpoint is made, you can add it to the [`spec`]('./src/spec.ts') `pipe`.

## Handling your new endpoint

When you have created an endpoint in the specification, you need to now handle the endpoint. This is the actual code implementation of your endpoint.

handlers are saved in the [handlers]('./src/handlers/') folder.

You use RouterBuilder.handler, passing in the [api spec]('./src/spec.ts') and the name of the route, and then an Effect returning function (`Effect.gen` is the simplest form of this).

The request arguments, you define in your endpoint specification, (query params, path params, request bodies, etc) are the arguments passed to the callback function of `RouterBuilder.handler`.

Ensure that you also add your `handler` to the RouterBuilder.handle [here]('./src/main.ts');

## Adding a journey / flow to the response map

If you are adding a flow to the response map the first thing to do is open up [responseMap]('./src/responses/index.ts')

This file is a `map` of Names -> Array<Step> where a Step is the response you want to return (in order). Order is key here.

If the Response you want to return is not already defined as a schema, you will have to define a new Schema and add the response.

A schema is defined in the schemas folder [here]('./src/schemas/');
A response is defined in the responses folder [here]('./src/responses/')

This is still a work in progress in terms of making it more scalable.

## Validating your code in Test

After adding a journey/flow to the response map and defining a schema, you next want to have some validation on the submitted request. You can do this by adding it to the `validator` function [here]('./src/helpers/match.ts');

This functions job is to `match` the type passed in, and validate based on the condition provided. If it passes, a boolean is returned, if it fails, a new Error should be returned.

## Services

To make it so types line up easier, each route, has a service dedicated to itself. The service under the hood, uses the `Requester` service. The `Requester` service is to mimic a call to the authorization server.

Let's look at the `Authorize` service. This service is the workhorse of the `authorize` handler.

`Authorize`, the service, uses `Requester` which will fetch a response from the authorization server.

After retrieving the response, the service will catch any errors that may be thrown, and mold them into HttpErrors to respond back to the client.

In a mock environment, rather than fetching from the client, authorization service will grab the next response from the `responseMap`.

In a live environment, it will forward a request to the Fetch service, and return that response.

## Creating Errors

If you want to create an error, it is simple. This is the skeleton of how to create an Error in `Effect`

```
class MyErrorName {
readonly _tag = 'MyErrorName'
}
```

The `_tag` is important as this is the name of the error, and how we can `catchTags` in our error handling. For simplicity, you can name is the same as your error class.

## Handling Errors

We want to return our errors back to the client, but typically we need an error response body that informs the client of the issue.

You should add your error responses to the response folder [here]('./src/responses');

In the service where you want to handle your error, you will see a `catchTags` function.

Let's pause here to understand the `Effect` type.

```ts
Effect<Success, Error, Requirements>;
```

When reading an Effect type, the first generic, is what is returned if the effect is successful.

The second argument is what is returned if the effect is unsuccessful.

The third argument is any services (or layers) that are required to run this effect.

So if we have an `effect` like this `Effect<Users, HttpError.HttpError | NoSuchElementException, never>`

This tells us the `effect` returns `users`, and can error two ways, `NoSuchElementException`, and with an `HttpError`.

We would rather handle this `NoSuchElementException` and send back to the client an HttpError informing them of the error that occurred.

We can do something like this now

```ts
Effect.catchTag('NoSuchElementException', () =>
HttpError.unauthorizedError('no such element found'),
);
```

This will return a 401 with that message.

When handling errors, we try to keep the handler always returning an `HttpError`, so we should handle any other errors we have deeper in the call stack, to return HttpError unless there is a valid reason to allow the error to bubble up.

If you have a shape of an error that you want to return from a handler, that does not match the current schema, you can add it to the `api spec`.
73 changes: 73 additions & 0 deletions e2e/mock-api-v2/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "e2e-mock-api-v2",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/mock-api-v2/src",
"projectType": "application",
"tags": ["e2e"],
"targets": {
"build": {
"executor": "@nx/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"platform": "node",
"outputPath": "dist/e2e/mock-api-v2",
"format": ["cjs"],
"bundle": false,
"main": "e2e/mock-api-v2/src/main.ts",
"tsConfig": "e2e/mock-api-v2/tsconfig.app.json",
"assets": ["e2e/mock-api-v2/src/assets"],
"generatePackageJson": false,
"esbuildOptions": {
"sourcemap": true,
"outExtension": {
".js": ".js"
}
}
},
"configurations": {
"development": {
"watch": true
},
"production": {
"esbuildOptions": {
"sourcemap": false,
"outExtension": {
".js": ".js"
}
}
}
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"options": {
"buildTarget": "e2e-mock-api-v2:build"
},
"configurations": {
"development": {
"buildTarget": "e2e-mock-api-v2:build:development"
},
"production": {
"buildTarget": "e2e-mock-api-v2:build:production"
}
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../coverage/e2e/mock-api-v2"
},
"configurations": {
"watch": {
"watch": true
}
}
}
}
}
Empty file.
43 changes: 43 additions & 0 deletions e2e/mock-api-v2/src/endpoints/custom-html.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Schema } from '@effect/schema';
import { pipe } from 'effect';
import { Api, ApiResponse } from 'effect-http';

import {
DavinciAuthorizeHeaders,
DavinciAuthorizeResponseHeaders,
} from '../schemas/authorize.schema';
import {
PingOneCustomHtmlRequestBody,
PingOneRequestQuery,
} from '../schemas/custom-html-template/custom-html-template-request.schema';
import {
PingOneCustomHtmlResponseBody,
PingOneCustomHtmlResponseErrorBody,
PingOnePathParams,
} from '../schemas/custom-html-template/custom-html-template-response.schema';
import { SuccessResponseRedirect } from '../schemas/return-success-response-redirect.schema';

const customHtmlEndPoint = Api.addEndpoint(
pipe(
Api.post(
'PingOneCustomHtml',
'/:envid/davinci/connections/:connectionid/capabilities/customHTMLTemplate',
).pipe(
Api.setRequestPath(PingOnePathParams),
Api.setRequestQuery(PingOneRequestQuery),
Api.setRequestBody(PingOneCustomHtmlRequestBody),

Api.setRequestHeaders(DavinciAuthorizeHeaders),

Api.setResponseBody(Schema.Union(PingOneCustomHtmlResponseBody, SuccessResponseRedirect)),
Api.setResponseHeaders(DavinciAuthorizeResponseHeaders),

Api.addResponse(
ApiResponse.make(401, Schema.Union(PingOneCustomHtmlResponseErrorBody, Schema.String)),
),
Api.addResponse(ApiResponse.make(403, Schema.String)),
),
),
);

export { customHtmlEndPoint };
28 changes: 28 additions & 0 deletions e2e/mock-api-v2/src/endpoints/davinci-authorize.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Schema } from '@effect/schema';
import { pipe } from 'effect';
import { Api } from 'effect-http';

import {
AuthorizePath,
DavinciAuthorizeHeaders,
DavinciAuthorizeQuery,
DavinciAuthorizeResponseHeaders,
} from '../schemas/authorize.schema';
import { PingOneCustomHtmlResponseBody } from '../schemas/custom-html-template/custom-html-template-response.schema';
import { SuccessResponseRedirect } from '../schemas/return-success-response-redirect.schema';

const davinciAuthorize = Api.addEndpoint(
pipe(
Api.get('DavinciAuthorize', '/:envid/as/authorize').pipe(
Api.setRequestPath(AuthorizePath),
Api.setRequestQuery(DavinciAuthorizeQuery),
Api.setRequestHeaders(DavinciAuthorizeHeaders),
),

Api.setResponseBody(Schema.Union(PingOneCustomHtmlResponseBody, SuccessResponseRedirect)),
Api.setResponseHeaders(DavinciAuthorizeResponseHeaders),
Api.setResponseStatus(200),
),
);

export { davinciAuthorize };
16 changes: 16 additions & 0 deletions e2e/mock-api-v2/src/endpoints/open-id-configuration.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Schema } from '@effect/schema';
import { pipe } from 'effect';
import { Api } from 'effect-http';
import { openIdConfigurationResponseSchema } from '../schemas/open-id-configuration/open-id-configuration-response.schema';

const openidConfiguration = Api.addEndpoint(
pipe(
Api.get('openidConfiguration', '/:envid/as/.well-known/openid-configuration').pipe(
Api.setRequestPath(Schema.Struct({ envid: Schema.String })),
Api.setResponseBody(openIdConfigurationResponseSchema),
Api.setResponseStatus(200),
),
),
);

export { openidConfiguration };
20 changes: 20 additions & 0 deletions e2e/mock-api-v2/src/endpoints/token.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Schema } from '@effect/schema';
import { pipe } from 'effect';
import { Api } from 'effect-http';

import { TokenResponseBody } from '../schemas/token/token.schema';

const pingOneToken = Api.addEndpoint(
pipe(
Api.post('PingOneToken', '/:envid/as/token').pipe(
Api.setRequestPath(Schema.Struct({ envid: Schema.String })),
Api.setRequestBody(Api.FormData),

// Responses
Api.setResponseBody(TokenResponseBody),
Api.setResponseStatus(200),
),
),
);

export { pingOneToken };
19 changes: 19 additions & 0 deletions e2e/mock-api-v2/src/endpoints/userinfo.endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Schema } from '@effect/schema';
import { pipe } from 'effect';
import { Api, ApiResponse, Security } from 'effect-http';

import { UserInfoSchema } from '../schemas/userinfo/userinfo.schema';

const userInfo = Api.addEndpoint(
pipe(
Api.get('UserInfo', '/:envid/as/userinfo').pipe(
Api.setRequestPath(Schema.Struct({ envid: Schema.String })),
Api.setSecurity(Security.bearer({})),
Api.setResponseStatus(200),
Api.setResponseBody(UserInfoSchema),
Api.addResponse(ApiResponse.make(401, Schema.String)),
),
),
);

export { userInfo };
16 changes: 16 additions & 0 deletions e2e/mock-api-v2/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class InvalidUsernamePassword {
readonly _tag = 'InvalidUsernamePassword';
}

class FetchError {
readonly _tag = 'FetchError';
}

class InvalidProtectNode {
readonly _tag = 'InvalidProtectNode';
}
class UnableToFindNextStep {
readonly _tag = 'UnableToFindNextStep';
}

export { FetchError, InvalidUsernamePassword, InvalidProtectNode, UnableToFindNextStep };
Loading

0 comments on commit 8555085

Please sign in to comment.