Skip to content

Commit ca2c5b4

Browse files
committed
test(client): support read-only clients
Configure presence multi-client testing to have a single writer and all others read only. Add test infrastructure support to have uniquely reader clients in a session. Token provider need only specify write scope during attach.
1 parent a0f2f5d commit ca2c5b4

File tree

5 files changed

+57
-39
lines changed

5 files changed

+57
-39
lines changed

packages/runtime/test-runtime-utils/src/insecureTokenProvider.ts

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,43 +38,39 @@ export class InsecureTokenProvider implements ITokenProvider {
3838
* @defaultValue [ ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite ]
3939
*/
4040
private readonly scopes?: ScopeType[],
41+
42+
/**
43+
* Optional. Override of attach container scopes. If a param is not provided,
44+
* InsecureTokenProvider will use the value of {@link scopes}.
45+
*
46+
* @remarks Common use of this parameter is to allow write for container
47+
* attach and just read for all other access. Effectively can create a
48+
* create and then read-only client.
49+
*
50+
* @param attachContainerScopes - See {@link @fluidframework/protocol-definitions#ITokenClaims.scopes}
51+
*
52+
* @defaultValue {@link scopes}
53+
*/
54+
private readonly attachContainerScopes?: ScopeType[],
4155
) {}
4256

43-
/**
44-
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchOrdererToken}
45-
*/
46-
public async fetchOrdererToken(
57+
private readonly fetchToken = async (
4758
tenantId: string,
4859
documentId?: string,
49-
): Promise<ITokenResponse> {
60+
): Promise<ITokenResponse> => {
61+
const generalScopes = this.scopes ?? [
62+
ScopeType.DocRead,
63+
ScopeType.DocWrite,
64+
ScopeType.SummaryWrite,
65+
];
66+
const scopes = (documentId ? undefined : this.attachContainerScopes) ?? generalScopes;
5067
return {
5168
fromCache: true,
52-
jwt: generateToken(
53-
tenantId,
54-
this.tenantKey,
55-
this.scopes ?? [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
56-
documentId,
57-
this.user,
58-
),
69+
jwt: generateToken(tenantId, this.tenantKey, scopes, documentId, this.user),
5970
};
60-
}
71+
};
6172

62-
/**
63-
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchStorageToken}
64-
*/
65-
public async fetchStorageToken(
66-
tenantId: string,
67-
documentId: string,
68-
): Promise<ITokenResponse> {
69-
return {
70-
fromCache: true,
71-
jwt: generateToken(
72-
tenantId,
73-
this.tenantKey,
74-
this.scopes ?? [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
75-
documentId,
76-
this.user,
77-
),
78-
};
79-
}
73+
public readonly fetchOrdererToken = this.fetchToken;
74+
75+
public readonly fetchStorageToken = this.fetchToken;
8076
}

packages/service-clients/end-to-end-tests/azure-client/src/test/AzureTokenFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ export function createAzureTokenProvider(
1111
id: string,
1212
name: string,
1313
scopes?: ScopeType[],
14+
attachScopes?: ScopeType[],
1415
): ITokenProvider {
1516
const key = process.env.azure__fluid__relay__service__key as string;
1617
if (key) {
1718
const userConfig = {
1819
id,
1920
name,
2021
};
21-
return new InsecureTokenProvider(key, userConfig, scopes);
22+
return new InsecureTokenProvider(key, userConfig, scopes, attachScopes);
2223
} else {
2324
throw new Error("Cannot create token provider.");
2425
}

packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "@fluidframework/azure-client";
1414
import { AttachState } from "@fluidframework/container-definitions";
1515
import { ConnectionState } from "@fluidframework/container-loader";
16+
import type { ScopeType } from "@fluidframework/driver-definitions/legacy";
1617
import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static";
1718
import {
1819
getPresence,
@@ -26,10 +27,8 @@ import {
2627
import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
2728
import { timeoutPromise } from "@fluidframework/test-utils/internal";
2829

29-
import type { ScopeType } from "../AzureClientFactory.js";
3030
import { createAzureTokenProvider } from "../AzureTokenFactory.js";
3131
import { TestDataObject } from "../TestDataObject.js";
32-
import type { configProvider } from "../utils.js";
3332

3433
import type { MessageFromChild, MessageToChild, UserIdAndName } from "./messageTypes.js";
3534

@@ -54,8 +53,8 @@ if (useAzure && endPoint === undefined) {
5453
const getOrCreatePresenceContainer = async (
5554
id: string | undefined,
5655
user: UserIdAndName,
57-
config?: ReturnType<typeof configProvider>,
5856
scopes?: ScopeType[],
57+
createScopes?: ScopeType[],
5958
): Promise<{
6059
container: IFluidContainer;
6160
presence: Presence;
@@ -68,12 +67,17 @@ const getOrCreatePresenceContainer = async (
6867
const connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure
6968
? {
7069
tenantId,
71-
tokenProvider: createAzureTokenProvider(user.id ?? "foo", user.name ?? "bar", scopes),
70+
tokenProvider: createAzureTokenProvider(
71+
user.id ?? "foo",
72+
user.name ?? "bar",
73+
scopes,
74+
createScopes,
75+
),
7276
endpoint: endPoint,
7377
type: "remote",
7478
}
7579
: {
76-
tokenProvider: new InsecureTokenProvider("fooBar", user, scopes),
80+
tokenProvider: new InsecureTokenProvider("fooBar", user, scopes, createScopes),
7781
endpoint: "http://localhost:7071",
7882
type: "local",
7983
};
@@ -319,6 +323,8 @@ class MessageHandler {
319323
const { container, presence, containerId } = await getOrCreatePresenceContainer(
320324
msg.containerId,
321325
msg.user,
326+
msg.scopes,
327+
msg.createScopes,
322328
);
323329
this.container = container;
324330
this.presence = presence;

packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
// eslint-disable-next-line import/no-internal-modules
77
import type { JsonSerializable } from "@fluidframework/core-interfaces/internal";
8+
import type { ScopeType } from "@fluidframework/driver-definitions/legacy";
89
import type { AttendeeId } from "@fluidframework/presence/beta";
910

1011
export interface UserIdAndName {
@@ -40,6 +41,8 @@ interface PingCommand {
4041
export interface ConnectCommand {
4142
command: "connect";
4243
user: UserIdAndName;
44+
scopes: ScopeType[];
45+
createScopes?: ScopeType[];
4346
/**
4447
* The ID of the Fluid container to connect to.
4548
* If not provided, a new Fluid container will be created.

packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/orchestratorUtils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { fork, type ChildProcess } from "node:child_process";
77

8+
import { ScopeType } from "@fluidframework/driver-definitions/legacy";
89
import type { AttendeeId } from "@fluidframework/presence/beta";
910
import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
1011

@@ -80,13 +81,18 @@ export async function forkChildProcesses(
8081
*
8182
* @param id - Suffix used to construct stable test user identity.
8283
*/
83-
export function composeConnectMessage(id: string | number): ConnectCommand {
84+
function composeConnectMessage(
85+
id: string | number,
86+
scopes: ScopeType[] = [ScopeType.DocRead],
87+
): ConnectCommand {
8488
return {
8589
command: "connect",
8690
user: {
8791
id: `test-user-id-${id}`,
8892
name: `test-user-name-${id}`,
8993
},
94+
scopes,
95+
createScopes: [ScopeType.DocWrite],
9096
};
9197
}
9298

@@ -122,7 +128,13 @@ export async function connectChildProcesses(
122128
}
123129
});
124130
});
125-
firstChild.send(composeConnectMessage(0));
131+
{
132+
// Note that DocWrite is used to have this attendee be the "leader".
133+
// DocRead would also be valid as DocWrite is specified for attach when there
134+
// is no document id (container id).
135+
const connectContainerCreator = composeConnectMessage(0, [ScopeType.DocWrite]);
136+
firstChild.send(connectContainerCreator);
137+
}
126138
const { containerCreatorAttendeeId, containerId } = await timeoutAwait(
127139
containerReadyPromise,
128140
{

0 commit comments

Comments
 (0)