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
16 changes: 16 additions & 0 deletions .changeset/app_mock-entry-point.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@equinor/fusion-framework-app": minor
---

Add a `./mock` entry point: `mockAppModules` runs an application's real module pipeline — the real `event`/`http`/`msal` modules, the real `AppConfigurator` configuration pipeline and real lifecycle — against a mocked parent Fusion instance, so a test exercises the wiring an application actually depends on instead of a reimplementation of it.

```ts
import { mockAppModules } from '@equinor/fusion-framework-app/mock';

const manifest = { appKey: 'my-app', displayName: 'My App', description: 'My app', type: 'standalone' } as const;
const modules = await mockAppModules(undefined, { manifest });
```

`enableAppManifestMock` registers the `app` module on a parent `mockFramework` configurator, serving an app's own manifest and config while delegating every other request to whatever service discovery (or a pre-configured http client) would really resolve. `mockAppModules` uses it to build its zero-configuration default parent; call it directly when a test needs to customize `serviceDiscovery` first.

Restructured `README.md` into an entry point pointing at `docs/http-clients.md`, `docs/bookmarks.md` and `docs/testing.md`, matching the convention already used by `@equinor/fusion-framework-module-http` and `@equinor/fusion-framework-module-msal`.
5 changes: 5 additions & 0 deletions .changeset/docs_http-module-testing-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@equinor/fusion-framework-docs": patch
---

Add a "Testing" page to the HTTP module's vue-press docs, `@include`ing the package's own `docs/testing.md`, and add the corresponding sidebar entry.
14 changes: 14 additions & 0 deletions .changeset/framework_http-mock-real-configurator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@equinor/fusion-framework": major
---

`FrameworkMockConfigurator.http` now returns the real `IHttpClientConfigurator` instead of a mock-specific one — fake a response by registering a short-circuiting middleware through `.http.addMiddleware(...)` instead of swapping the module out:

```ts
const configurator = new FrameworkMockConfigurator();
configurator.http.addMiddleware(async (uri, init, next) =>
uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
);
```

See `@equinor/fusion-framework-module-http`'s `addMiddleware` changeset for the full API.
20 changes: 20 additions & 0 deletions .changeset/module-app_mock-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@equinor/fusion-framework-module-app": minor
---

Add `MockAppClient`, exported from a new `./mock` subpath, so a test can serve one app's own manifest and config locally instead of contacting the app service.

```ts
import { MockAppClient } from '@equinor/fusion-framework-module-app/mock';

enableAppModule(configurator, (builder) => {
builder.setClient(async ({ requireInstance }) => {
const http = await requireInstance('http');
return new MockAppClient(http.createClient('apps'), manifest, config);
});
});
```

`getAppManifest` only resolves locally for `manifest.appKey` with no `tag` at all. `getAppConfig` resolves for `manifest.appKey` when `tag` is either absent or equal to the manifest's own `build.version` — the same tag `App` requests when loading config for the manifest it already resolved. Every other request — other app keys, tagged requests, builds, settings — still goes through the real `AppClient` it wraps, so pointing service discovery at a different registry or a real local mock server keeps working unchanged.

Also export `AppConfig` as a value from the package root (previously type-only), so a test can construct one directly with `new AppConfig({ environment, endpoints })`.
31 changes: 31 additions & 0 deletions .changeset/module-http_addmiddleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@equinor/fusion-framework-module-http": major
---

Replace the `@equinor/fusion-framework-module-http/mock` entry point's router-based mock (`enableHttpMock`, `HttpMockConfigurator`, `HttpMockRouter`) with `addMiddleware` on the real `HttpClientConfigurator`, so answering a request in a test no longer means swapping out a separate configurator.

```ts
configurator.configureHttpClient('catalog', { baseUri: 'https://api.example.com' });
configurator.http.addMiddleware(async (uri, init, next) =>
uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
);
```

`addMiddleware` wraps `_performFetch` rather than replacing it, so the exact same client and configuration a real app registers is what a test exercises — only the boundary that would reach the network is short-circuited, and a middleware that calls `next(uri, init)` falls through to the real call (or the next middleware) unchanged.

Two adapters cover what the old router and its Express-style adapters did:

- `createRouterMiddleware(baseUri, build)` — a minimal Express-like router (`.get`/`.post`/`.put`/`.patch`/`.delete`/`.on`, `:id`-style path params) for one base URI, with no dependency on a real routing library.
- `createOpenApiMockMiddleware(openApiMock)` — adapts an `@equinor/fusion-openapi-mock` instance, so a real `openapi.json`/`openapi.yaml` fakes every response with no handlers written at all.

```ts
import { createRouterMiddleware } from '@equinor/fusion-framework-module-http/mock';

configurator.http.addMiddleware(
createRouterMiddleware('https://context.example.com', (router) => {
router.get('/contexts/:id', ({ params }) => Response.json({ id: params.id }));
}),
);
```

`@equinor/fusion-framework`'s `FrameworkMockConfigurator.http` now returns the real `IHttpClientConfigurator` instead of a mock-specific one, for the same reason.
5 changes: 5 additions & 0 deletions .changeset/module-http_fix-abort-through-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@equinor/fusion-framework-module-http": patch
---

Fix `HttpClient.abort()` not cancelling the underlying network call when middleware is registered. `next(...)` resolves through a `Promise`, so a middleware calling it created a subscription to `_performFetch` outside the tree the `takeUntil(this._abort$)` teardown reaches -- the outer request settled, but the real `fetch` kept running. `abort()` now also aborts a per-request `AbortSignal` combined into the request `init`, so `_performFetch` (`fromFetch` by default) is cancelled directly regardless of whether middleware severed the RxJS subscription chain.
23 changes: 0 additions & 23 deletions .changeset/module-http_mock-entry-point.md

This file was deleted.

100 changes: 35 additions & 65 deletions packages/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ single `configureModules` call.
> **Most Fusion apps should use `@equinor/fusion-framework-react-app` instead.**
> This lower-level package is for framework-agnostic or advanced scenarios.

## Documentation

| Topic | Description |
|---|---|
| [Configure HTTP Clients](docs/http-clients.md) | Named clients from app config, service discovery, and explicit registration, plus resolution priority |
| [Enable Bookmarks](docs/bookmarks.md) | Registering the bookmark module via `enableBookmark` |
| [Testing](docs/testing.md) | The `/mock` entry point: `mockAppModules`, `AppMockConfigurator`, and `enableAppManifestMock` |

## Installation

```sh
Expand Down Expand Up @@ -43,6 +51,7 @@ const modules = await initialize({ fusion, env });
| `AppModuleInitiator` | Callback signature accepted by `configureModules` for user-supplied setup. |
| `AppEnv` | Environment descriptor containing the app manifest, config, and optional basename. |
| `enableBookmark` | Helper to enable the bookmark module (import from `@equinor/fusion-framework-app/enable-bookmark`). |
| `mockAppModules` | Runs the real module pipeline against deterministic fakes for tests (import from `@equinor/fusion-framework-app/mock`). |

## API Surface

Expand All @@ -63,78 +72,23 @@ giving you access to:
|---|---|
| `@equinor/fusion-framework-app` | `configureModules`, `AppConfigurator`, `IAppConfigurator`, all type aliases |
| `@equinor/fusion-framework-app/enable-bookmark` | `enableBookmark` function |
| `@equinor/fusion-framework-app/mock` | `mockAppModules`, `AppMockConfigurator`, `enableAppManifestMock` |

## Configure HTTP Clients

The `AppConfigurator` can register named HTTP clients from several sources.
You retrieve a client at runtime with `framework.modules.http.createClient(name)`.

### From Application Config (auto-registration)

Endpoints defined in `app.config.<env>.ts` are **automatically registered as
named HTTP clients** when the `AppConfigurator` is created — no extra code
needed in `config.ts`.

```ts
// app.config.ts
import { defineAppConfig } from '@equinor/fusion-framework-cli/app';

export default defineAppConfig(() => ({
endpoints: {
schedule: {
url: 'https://schedule-api.example.com',
scopes: ['api://schedule-id/.default'],
},
},
}));
```

After initialization, use the client directly:

```ts
const client = framework.modules.http.createClient('schedule');
const data = await client.json('/items');
```

### Via Service Discovery
The `AppConfigurator` can register named HTTP clients from several sources —
application config endpoints, service discovery, or explicit registration —
and you retrieve one at runtime with `framework.modules.http.createClient(name)`.

```ts
const initialize = configureModules((configurator) => {
configurator.useFrameworkServiceClient('people');
});
```

### Explicit Registration

Use `configureHttpClient` in `config.ts` when the endpoint is **not** in
`app.config.ts`, or when you need custom transport behavior such as headers,
response guards, or a custom client class.

```ts
configurator.configureHttpClient('custom-api', {
baseUri: 'https://custom.api.example.com',
defaultScopes: ['api://custom-id/.default'],
onCreate: (client) => {
client.requestHandler.setHeader('X-Source', 'portal');
},
});
```

### Resolution Priority

When the same client name is configured in more than one place, the
highest-priority source wins:

| Priority | Source | Example |
|----------|--------|---------|
| 1 (highest) | **Session overrides** | User-specific URL / scopes set at runtime via `sessionStorage` |
| 2 | **Application config endpoints** | `endpoints` in `app.config.ts` |
| 3 | **Service-discovery registry** | Resolved via `useFrameworkServiceClient` |
| 4 (lowest) | **Explicit registration** | `configureHttpClient(name, options)` in `config.ts` |

This means an endpoint defined in `app.config.ts` will override a
`configureHttpClient` call for the same name, and a session override will
override both.
See [Configure HTTP Clients](docs/http-clients.md) for auto-registration from
`app.config.ts`, explicit registration, and resolution priority when a client
is configured in more than one place.

## Enable Bookmarks

Expand All @@ -144,16 +98,32 @@ The bookmark module allows applications to save and restore application state.
> `@equinor/fusion-framework-module-bookmark` directly.

```ts
import { configureModules } from '@equinor/fusion-framework-app';
import { enableBookmark } from '@equinor/fusion-framework-app/enable-bookmark';

const initialize = configureModules((configurator) => {
enableBookmark(configurator);
});
```

Payload generators registered through the bookmark module are automatically
cleaned up when the module is disposed.
See [Enable Bookmarks](docs/bookmarks.md) for payload generator cleanup behavior.

## Testing

Import from `@equinor/fusion-framework-app/mock` to run an application's real
module pipeline in tests — the real `event`/`http`/`msal` modules, the real
`AppConfigurator` configuration pipeline, and real lifecycle — while only the
boundaries that reach outside the process are substituted with deterministic
fakes. This entry point has no dependency on Vitest or any other test runner.

```ts
import { mockAppModules } from '@equinor/fusion-framework-app/mock';

const manifest = { appKey: 'my-app', displayName: 'My App', description: 'My app', type: 'standalone' } as const;
const modules = await mockAppModules(undefined, { manifest });
```

See [Testing](docs/testing.md) for `AppMockConfigurator`, `enableAppManifestMock`,
and customizing the mocked parent's service discovery.

## Types

Expand Down
18 changes: 18 additions & 0 deletions packages/app/docs/bookmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Enable Bookmarks

The bookmark module allows applications to save and restore application state.

> **Important:** Import `enableBookmark` from the app-level package, not from
> `@equinor/fusion-framework-module-bookmark` directly.

```ts
import { configureModules } from '@equinor/fusion-framework-app';
import { enableBookmark } from '@equinor/fusion-framework-app/enable-bookmark';

const initialize = configureModules((configurator) => {
enableBookmark(configurator);
});
```

Payload generators registered through the bookmark module are automatically
cleaned up when the module is disposed.
71 changes: 71 additions & 0 deletions packages/app/docs/http-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Configure HTTP Clients

The `AppConfigurator` can register named HTTP clients from several sources.
You retrieve a client at runtime with `framework.modules.http.createClient(name)`.

## From Application Config (auto-registration)

Endpoints defined in `app.config.<env>.ts` are **automatically registered as
named HTTP clients** when the `AppConfigurator` is created — no extra code
needed in `config.ts`.

```ts
// app.config.ts
import { defineAppConfig } from '@equinor/fusion-framework-cli/app';

export default defineAppConfig(() => ({
endpoints: {
schedule: {
url: 'https://schedule-api.example.com',
scopes: ['api://schedule-id/.default'],
},
},
}));
```

After initialization, use the client directly:

```ts
const client = framework.modules.http.createClient('schedule');
const data = await client.json('/items');
```

## Via Service Discovery

```ts
const initialize = configureModules((configurator) => {
configurator.useFrameworkServiceClient('people');
});
```

## Explicit Registration

Use `configureHttpClient` in `config.ts` when the endpoint is **not** in
`app.config.ts`, or when you need custom transport behavior such as headers,
response guards, or a custom client class.

```ts
configurator.configureHttpClient('custom-api', {
baseUri: 'https://custom.api.example.com',
defaultScopes: ['api://custom-id/.default'],
onCreate: (client) => {
client.requestHandler.setHeader('X-Source', 'portal');
},
});
```

## Resolution Priority

When the same client name is configured in more than one place, the
highest-priority source wins:

| Priority | Source | Example |
|----------|--------|---------|
| 1 (highest) | **Session overrides** | User-specific URL / scopes set at runtime via `sessionStorage` |
| 2 | **Application config endpoints** | `endpoints` in `app.config.ts` |
| 3 | **Service-discovery registry** | Resolved via `useFrameworkServiceClient` |
| 4 (lowest) | **Explicit registration** | `configureHttpClient(name, options)` in `config.ts` |

This means an endpoint defined in `app.config.ts` will override a
`configureHttpClient` call for the same name, and a session override will
override both.
Loading
Loading