@equinor/fusion-framework 8.0.15 → 8.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +30 -0
- package/dist/esm/__tests__/mock/application-module.test.js +53 -0
- package/dist/esm/__tests__/mock/application-module.test.js.map +1 -0
- package/dist/esm/__tests__/mock/documented-usage.test.js +73 -0
- package/dist/esm/__tests__/mock/documented-usage.test.js.map +1 -0
- package/dist/esm/__tests__/mock/mock-framework.test.js +212 -0
- package/dist/esm/__tests__/mock/mock-framework.test.js.map +1 -0
- package/dist/esm/init.js +7 -2
- package/dist/esm/init.js.map +1 -1
- package/dist/esm/mock/FrameworkMockConfigurator.js +233 -0
- package/dist/esm/mock/FrameworkMockConfigurator.js.map +1 -0
- package/dist/esm/mock/index.js +32 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/mock/mock-framework.js +55 -0
- package/dist/esm/mock/mock-framework.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/__tests__/mock/application-module.test.d.ts +1 -0
- package/dist/types/__tests__/mock/documented-usage.test.d.ts +1 -0
- package/dist/types/__tests__/mock/mock-framework.test.d.ts +1 -0
- package/dist/types/mock/FrameworkMockConfigurator.d.ts +185 -0
- package/dist/types/mock/index.d.ts +29 -0
- package/dist/types/mock/mock-framework.d.ts +58 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/testing-api.md +25 -0
- package/docs/testing-choosing-a-layer.md +90 -0
- package/docs/testing-design.md +63 -0
- package/docs/testing-extending.md +102 -0
- package/docs/testing.md +178 -0
- package/package.json +17 -11
- package/src/__tests__/mock/application-module.test.ts +87 -0
- package/src/__tests__/mock/documented-usage.test.ts +98 -0
- package/src/__tests__/mock/mock-framework.test.ts +278 -0
- package/src/init.ts +7 -2
- package/src/mock/FrameworkMockConfigurator.ts +270 -0
- package/src/mock/index.ts +62 -0
- package/src/mock/mock-framework.ts +72 -0
- package/src/version.ts +1 -1
- package/vitest.config.ts +1 -1
package/docs/testing.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
Recipes for the situations a test normally runs into. Every one of them builds a real framework instance — only the boundaries that leave the process are substituted.
|
|
4
|
+
|
|
5
|
+
`mockFramework` takes a single callback, which receives a [`FrameworkMockConfigurator`](#the-configurator). That configurator **is** a `FrameworkConfigurator`, so everything an application does at configure time works here unchanged.
|
|
6
|
+
|
|
7
|
+
## Zero configuration
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
11
|
+
|
|
12
|
+
const fusion = await mockFramework();
|
|
13
|
+
|
|
14
|
+
fusion.modules.auth.account?.name; // 'Test User'
|
|
15
|
+
await fusion.modules.serviceDiscovery.resolveService('apps'); // resolves offline
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Every module declared by `FrameworkConfigurator` is initialized: `event`, `auth`, `http`, `serviceDiscovery`, `context` and `telemetry`.
|
|
19
|
+
|
|
20
|
+
## The configurator
|
|
21
|
+
|
|
22
|
+
Modules whose boundary is mocked expose their mock configurator directly, so a test configures them without registering a callback:
|
|
23
|
+
|
|
24
|
+
| Property | Type |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `msal` | `MsalMockConfigurator` |
|
|
27
|
+
| `serviceDiscovery` | `ServiceDiscoveryMockConfigurator` |
|
|
28
|
+
| `http` | `IHttpClientConfigurator` (the real configurator — see [`.http`](testing-extending.md#http)) |
|
|
29
|
+
| `context` | `ContextMockConfigurator` |
|
|
30
|
+
| `telemetry` | `TelemetryMockConfigurator` |
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const fusion = await mockFramework((configurator) => {
|
|
34
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace' });
|
|
35
|
+
configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
These are the real module configurators, so the real builder API, the real validation and the real provider are used.
|
|
40
|
+
|
|
41
|
+
## Choosing the signed-in user
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const fusion = await mockFramework((configurator) => {
|
|
45
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace', username: 'ada@equinor.com' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const token = await fusion.modules.auth.acquireAccessToken({
|
|
49
|
+
request: { scopes: ['Files.Read'] },
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The token is a structurally valid JWT carrying the claims an application reads. It is unsigned by default and must never be accepted by anything but a test.
|
|
54
|
+
|
|
55
|
+
`setAccount` records configuration only — the user is signed in on the client before the provider initializes. It takes an object, `null` when nobody is signed in, or an ordinary config-builder callback resolving either:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
configurator.msal.setAccount(null);
|
|
59
|
+
configurator.msal.setAccount(async ({ hasModule }) => ({
|
|
60
|
+
name: hasModule('app') ? 'App User' : 'Portal User',
|
|
61
|
+
}));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Because the user is in place before `MsalProvider.initialize()` runs, the provider's real start-up path acts on it — pair `signedOut` with `setRequiresAuth(true)` to watch the automatic login happen.
|
|
65
|
+
|
|
66
|
+
The account replaces any previously declared account, and is signed in on whichever client the module authenticates through — the one it builds, one supplied through `setClient`, or the host's when the module is hoisted onto a host application's provider. When that client cannot represent a declared user, it throws rather than failing quietly.
|
|
67
|
+
|
|
68
|
+
## Testing signed-out behaviour
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const fusion = await mockFramework((configurator) => {
|
|
72
|
+
configurator.msal.setAccount({ signedOut: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
fusion.modules.auth.account; // null
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Silent flows then resolve empty so the provider follows its unauthenticated path, while an explicit `login()` still succeeds — so a test can drive the sign-in journey, not only its end state.
|
|
79
|
+
|
|
80
|
+
## Composing the service registry
|
|
81
|
+
|
|
82
|
+
Nothing has to be constructed to add a service, or to move every service to a locally running mock server such as Mockoon or Prism — in which case the application makes real HTTP calls with nothing intercepting them.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const fusion = await mockFramework((configurator) => {
|
|
86
|
+
configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
|
|
87
|
+
configurator.serviceDiscovery.addService({ key: 'my-api' });
|
|
88
|
+
configurator.serviceDiscovery.removeService('bookmarks');
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
To replace the baseline registry outright rather than compose onto it, use `setServices`:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
configurator.serviceDiscovery.setServices([{ key: 'apps', uri: 'http://localhost:3000' }]);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
By default an undeclared service resolves to a synthesised entry rather than throwing, so a test does not fail merely because the application resolved something the test did not think to declare. Call `setResolveUnknownServices(false)` to assert the opposite.
|
|
99
|
+
|
|
100
|
+
> [!WARNING]
|
|
101
|
+
> Built-in modules resolve services **while the framework starts** — the context module resolves `context`, for example. Combining `setResolveUnknownServices(false)` with a `setServices` registry that omits them fails initialization rather than the assertion you were writing. Either keep synthesis on, or declare every service the framework itself needs.
|
|
102
|
+
|
|
103
|
+
## Seeding context
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const fusion = await mockFramework((configurator) => {
|
|
107
|
+
configurator.context.setCurrentContext({ id: 'project-42', type: { id: 'ProjectMaster' }, value: {} });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
fusion.modules.context.currentContext; // the seeded item
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`configurator.context` is a `ContextMockConfigurator` — a small, context-domain vocabulary (`setCurrentContext`, `setContexts`, `addContext`, `setRelatedContexts`) covers seeding a known item with no HTTP mock and no service-discovery mock required. `setResolver` is the escape hatch for a custom resolution need the friendly methods do not cover. Real `ContextProvider` behaviour — `validateContext`, `resolveContext`, parent-context propagation — still runs against the seeded data in both.
|
|
114
|
+
|
|
115
|
+
This is one of two ways to fake context data: seeding an item directly (above) substitutes only the data source, with no transport involved. Mocking the context API's HTTP responses instead, through `.http`, exercises the real `ContextModuleConfigurator`/services/HTTP pipeline — reach for that when the test needs to cover that pipeline itself, optionally paired with `createOpenApiMockMiddleware` for faker-generated data straight from context's OpenAPI spec.
|
|
116
|
+
|
|
117
|
+
## Mocking an individual call
|
|
118
|
+
|
|
119
|
+
That is your test runner's job, not this entry point's. Mock clients are plain classes with ordinary methods, so any runner can spy on them with its own tooling — including call assertions and its own reset semantics.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(service);
|
|
123
|
+
|
|
124
|
+
afterEach(() => vi.restoreAllMocks());
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The same holds for `bun:test`'s `spyOn` and Node's `t.mock.method`, which is why this entry point introduces no mocking API of its own.
|
|
128
|
+
|
|
129
|
+
## Configuring the framework as an application does
|
|
130
|
+
|
|
131
|
+
The configurator is a `FrameworkConfigurator`, so every `enableX` and `configureX` helper is available and behaves normally.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const fusion = await mockFramework((configurator) => {
|
|
135
|
+
configurator.onConfigured(() => {
|
|
136
|
+
/* ... */
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Mocks are registered **before** the callback runs, so anything configured there wins — including replacing a mock with a different one.
|
|
142
|
+
|
|
143
|
+
## Registering your own modules
|
|
144
|
+
|
|
145
|
+
Pass your module descriptors as a type argument. They are then typed on both the configurator and the returned instance, so no cast is needed to reach them.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
|
|
149
|
+
enableInvoicesMock(configurator, { total: 42 });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await fusion.modules.invoices.getInvoice('inv-1'); // fully typed
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`addModule` is available if it reads better at the call site; it is sugar for the same call.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
configurator.addModule((c) => enableInvoicesMock(c, { total: 42 }));
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> [!NOTE]
|
|
162
|
+
> Only modules that ship a mock configurator get a property such as `configurator.msal`. Everything else is registered exactly as it is in production — through its own `enableX` helper or `configurator.addConfig`. Module *instances* never exist at configure time; they are created by `initialize`.
|
|
163
|
+
|
|
164
|
+
See [Adding a mock for another module](./testing-extending.md) for how to give your module a test double, and a property on the configurator.
|
|
165
|
+
|
|
166
|
+
## Bringing your own configurator
|
|
167
|
+
|
|
168
|
+
`FrameworkMockConfigurator` can be constructed directly and initialized with `init`, which is useful when a test needs to hold on to the configurator.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { init } from '@equinor/fusion-framework';
|
|
172
|
+
import { FrameworkMockConfigurator } from '@equinor/fusion-framework/mock';
|
|
173
|
+
|
|
174
|
+
const configurator = new FrameworkMockConfigurator();
|
|
175
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace' });
|
|
176
|
+
|
|
177
|
+
const fusion = await init(configurator);
|
|
178
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"import": "./dist/esm/index.js",
|
|
10
10
|
"types": "./dist/types/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./mock": {
|
|
13
|
+
"import": "./dist/esm/mock/index.js",
|
|
14
|
+
"types": "./dist/types/mock/index.d.ts"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"keywords": [
|
|
14
18
|
"fusion",
|
|
15
19
|
"fusion-framework",
|
|
16
|
-
"utility"
|
|
20
|
+
"utility",
|
|
21
|
+
"testing",
|
|
22
|
+
"mock"
|
|
17
23
|
],
|
|
18
24
|
"homepage": "https://equinor.github.io/fusion-framework/",
|
|
19
25
|
"author": {
|
|
@@ -32,18 +38,18 @@
|
|
|
32
38
|
},
|
|
33
39
|
"dependencies": {
|
|
34
40
|
"rxjs": "^7.8.1",
|
|
35
|
-
"@equinor/fusion-framework-module
|
|
36
|
-
"@equinor/fusion-framework-module-
|
|
37
|
-
"@equinor/fusion-framework-module-
|
|
38
|
-
"@equinor/fusion-framework-module": "
|
|
39
|
-
"@equinor/fusion-framework-module-service-discovery": "10.0.
|
|
40
|
-
"@equinor/fusion-framework-module-
|
|
41
|
-
"@equinor/fusion-framework-module-telemetry": "
|
|
42
|
-
"@equinor/fusion-framework-module-services": "8.1.0"
|
|
41
|
+
"@equinor/fusion-framework-module": "6.1.3-next.0",
|
|
42
|
+
"@equinor/fusion-framework-module-msal": "11.0.0-next.0",
|
|
43
|
+
"@equinor/fusion-framework-module-event": "6.1.0-next.0",
|
|
44
|
+
"@equinor/fusion-framework-module-http": "8.1.0-next.0",
|
|
45
|
+
"@equinor/fusion-framework-module-service-discovery": "10.1.0-next.0",
|
|
46
|
+
"@equinor/fusion-framework-module-context": "9.0.0-next.0",
|
|
47
|
+
"@equinor/fusion-framework-module-telemetry": "8.0.0-next.0",
|
|
48
|
+
"@equinor/fusion-framework-module-services": "8.1.1-next.0"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
51
|
"typescript": "^7.0.2",
|
|
46
|
-
"vitest": "^4.1.
|
|
52
|
+
"vitest": "^4.1.10"
|
|
47
53
|
},
|
|
48
54
|
"scripts": {
|
|
49
55
|
"build": "tsc -b",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IModulesConfigurator, Module } from '@equinor/fusion-framework-module';
|
|
4
|
+
|
|
5
|
+
import { mockFramework } from '../../mock/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A module an application team owns, which the framework knows nothing about.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* The point of these tests is that nothing below required support from
|
|
12
|
+
* `@equinor/fusion-framework/mock`. If an application team can do this, the
|
|
13
|
+
* extension pattern holds.
|
|
14
|
+
*/
|
|
15
|
+
interface InvoiceClient {
|
|
16
|
+
getInvoice(id: string): Promise<{ id: string; total: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class InvoiceConfigurator {
|
|
20
|
+
#client?: InvoiceClient;
|
|
21
|
+
|
|
22
|
+
public setClient(client: InvoiceClient): void {
|
|
23
|
+
this.#client = client;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public createClient(): InvoiceClient {
|
|
27
|
+
if (!this.#client) {
|
|
28
|
+
throw new Error('An invoice client is required');
|
|
29
|
+
}
|
|
30
|
+
return this.#client;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type InvoiceModule = Module<'invoices', InvoiceClient, InvoiceConfigurator>;
|
|
35
|
+
|
|
36
|
+
const invoiceModule: InvoiceModule = {
|
|
37
|
+
name: 'invoices',
|
|
38
|
+
configure: () => new InvoiceConfigurator(),
|
|
39
|
+
initialize: ({ config }) => config.createClient(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The team's own mock, following the pattern the built-in modules use. */
|
|
43
|
+
const enableInvoicesMock = (
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: mirrors every enableX helper
|
|
45
|
+
configurator: IModulesConfigurator<any, any>,
|
|
46
|
+
options: { total?: number } = {},
|
|
47
|
+
): void => {
|
|
48
|
+
configurator.addConfig({
|
|
49
|
+
module: invoiceModule,
|
|
50
|
+
configure: (builder: InvoiceConfigurator) =>
|
|
51
|
+
builder.setClient({
|
|
52
|
+
getInvoice: async (id) => ({ id, total: options.total ?? 0 }),
|
|
53
|
+
}),
|
|
54
|
+
} as { module: InvoiceModule });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('application modules', () => {
|
|
58
|
+
it('fails to start without its mock, so the seam is real and not a no-op', async () => {
|
|
59
|
+
await expect(
|
|
60
|
+
mockFramework<[InvoiceModule]>((configurator) => {
|
|
61
|
+
configurator.addConfig({ module: invoiceModule } as { module: InvoiceModule });
|
|
62
|
+
}),
|
|
63
|
+
).rejects.toThrow(/invoice client is required/i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('composes with the built-in mocks and is typed without a cast', async () => {
|
|
67
|
+
const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
|
|
68
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace' });
|
|
69
|
+
enableInvoicesMock(configurator, { total: 42 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// No cast: `TModules` must flow through to `fusion.modules`.
|
|
73
|
+
const invoice = await fusion.modules.invoices.getInvoice('inv-1');
|
|
74
|
+
|
|
75
|
+
expect(invoice).toEqual({ id: 'inv-1', total: 42 });
|
|
76
|
+
expect(fusion.modules.auth.account?.name).toBe('Ada Lovelace');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('is registered exactly as it is in production', async () => {
|
|
80
|
+
// The same helper, against a real FrameworkConfigurator, would behave identically.
|
|
81
|
+
const fusion = await mockFramework<[InvoiceModule]>((configurator) =>
|
|
82
|
+
enableInvoicesMock(configurator, { total: 7 }),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await expect(fusion.modules.invoices.getInvoice('inv-2')).resolves.toMatchObject({ total: 7 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { decodeJwtSegment } from '@equinor/fusion-framework-module-msal/mock';
|
|
4
|
+
|
|
5
|
+
import { init } from '../../init.js';
|
|
6
|
+
import { createMockService, FrameworkMockConfigurator, mockFramework } from '../../mock/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pins the snippets in `docs/testing.md` and the package README to real
|
|
10
|
+
* behaviour, so documentation cannot drift away from the API it describes.
|
|
11
|
+
*/
|
|
12
|
+
describe('documented usage', () => {
|
|
13
|
+
afterEach(() => vi.restoreAllMocks());
|
|
14
|
+
|
|
15
|
+
it('composes the service registry on the builder', async () => {
|
|
16
|
+
const fusion = await mockFramework((configurator) => {
|
|
17
|
+
configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
|
|
18
|
+
configurator.serviceDiscovery.addService({ key: 'my-api' });
|
|
19
|
+
configurator.serviceDiscovery.removeService('bookmarks');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await expect(fusion.modules.serviceDiscovery.resolveService('my-api')).resolves.toMatchObject({
|
|
23
|
+
key: 'my-api',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('replaces the baseline registry with setServices', async () => {
|
|
28
|
+
const fusion = await mockFramework((configurator) => {
|
|
29
|
+
configurator.serviceDiscovery.setServices([{ key: 'apps', uri: 'http://localhost:3000' }]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(fusion.modules.serviceDiscovery.resolveService('apps')).resolves.toMatchObject({
|
|
33
|
+
uri: 'http://localhost:3000',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps startup working when the registry is replaced, because unknown services synthesise', async () => {
|
|
38
|
+
// The context module resolves `context` while initializing, so a replaced
|
|
39
|
+
// registry that omits it would break start-up if synthesis were disabled.
|
|
40
|
+
const fusion = await mockFramework((configurator) => {
|
|
41
|
+
configurator.serviceDiscovery.setServices([{ key: 'apps', uri: 'http://localhost:3000' }]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await expect(fusion.modules.serviceDiscovery.resolveService('people')).resolves.toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('lets the test runner spy on the discovery client', async () => {
|
|
48
|
+
const fusion = await mockFramework();
|
|
49
|
+
|
|
50
|
+
vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(
|
|
51
|
+
createMockService({ key: 'apps', uri: 'http://spied' }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await expect(fusion.modules.serviceDiscovery.resolveService('apps')).resolves.toMatchObject({
|
|
55
|
+
uri: 'http://spied',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('lets the test runner spy on the auth client', async () => {
|
|
60
|
+
const fusion = await mockFramework();
|
|
61
|
+
|
|
62
|
+
const spy = vi.spyOn(fusion.modules.auth.client, 'acquireToken');
|
|
63
|
+
|
|
64
|
+
await fusion.modules.auth.acquireAccessToken({ request: { scopes: ['Files.Read'] } });
|
|
65
|
+
|
|
66
|
+
expect(spy).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('resolves the real scope through the real provider, not the test double', async () => {
|
|
70
|
+
const fusion = await mockFramework((configurator) => {
|
|
71
|
+
// The client is configured with what it talks to, exactly as in production
|
|
72
|
+
configurator.msal.setClientConfig({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const token = await fusion.modules.auth.acquireAccessToken();
|
|
76
|
+
const claims = JSON.parse(decodeJwtSegment(token?.split('.')[1] ?? ''));
|
|
77
|
+
|
|
78
|
+
// MsalProvider — not the mock — turns "no scopes requested" into `${clientId}/.default`
|
|
79
|
+
expect(claims.scp).toBe('my-app/.default');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('signs nobody in when the account is signed out', async () => {
|
|
83
|
+
const fusion = await mockFramework((configurator) => {
|
|
84
|
+
configurator.msal.setAccount({ signedOut: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(fusion.modules.auth.account).toBeFalsy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('can be constructed directly and initialized with init', async () => {
|
|
91
|
+
const configurator = new FrameworkMockConfigurator();
|
|
92
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace' });
|
|
93
|
+
|
|
94
|
+
const fusion = await init(configurator);
|
|
95
|
+
|
|
96
|
+
expect(fusion.modules.auth.account?.name).toBe('Ada Lovelace');
|
|
97
|
+
});
|
|
98
|
+
});
|