Skip to content

Commit

Permalink
Test to validate out of band container create (#22594)
Browse files Browse the repository at this point in the history
Adds a test that builds a wrapper over a document service factory and
allows the actual creation of the container to done by a decoupled out
of band function. In the test it is just another function, but this
could be a call to a separate process or server.

---------

Co-authored-by: jzaffiro <110866475+jzaffiro@users.noreply.github.com>
  • Loading branch information
anthony-murphy and jzaffiro authored Sep 24, 2024
1 parent 2c9ff85 commit ff1b2c7
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 11 deletions.
10 changes: 6 additions & 4 deletions packages/test/local-server-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lint:fix": "fluid-build . --task eslint:fix --task format",
"test": "npm run test:mocha",
"test:coverage": "c8 npm test",
"test:mocha": "mocha \"dist/test/**/*.spec.*js\" --exit",
"test:mocha": "mocha \"lib/test/**/*.spec.*js\" --exit",
"test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha"
},
"c8": {
Expand All @@ -53,8 +53,7 @@
],
"temp-directory": "nyc/.nyc_output"
},
"devDependencies": {
"@biomejs/biome": "~1.8.3",
"dependencies": {
"@fluid-internal/client-utils": "workspace:~",
"@fluid-internal/mocha-test-setup": "workspace:~",
"@fluid-private/test-loader-utils": "workspace:~",
Expand Down Expand Up @@ -91,7 +90,10 @@
"@fluidframework/server-local-server": "^5.0.0",
"@fluidframework/shared-object-base": "workspace:~",
"@fluidframework/telemetry-utils": "workspace:~",
"@fluidframework/test-utils": "workspace:~",
"@fluidframework/test-utils": "workspace:~"
},
"devDependencies": {
"@biomejs/biome": "~1.8.3",
"@types/mocha": "^9.1.1",
"@types/nock": "^9.3.0",
"@types/node": "^18.19.0",
Expand Down
128 changes: 128 additions & 0 deletions packages/test/local-server-tests/src/test/decoupledCreate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "assert";

import type { IRequest } from "@fluidframework/core-interfaces";
import type { FluidObject } from "@fluidframework/core-interfaces/internal";
import {
IDocumentServiceFactory,
type IResolvedUrl,
type ISummaryTree,
} from "@fluidframework/driver-definitions/internal";
import {
LocalDocumentServiceFactory,
LocalResolver,
} from "@fluidframework/local-driver/internal";
import {
LocalDeltaConnectionServer,
type ILocalDeltaConnectionServer,
} from "@fluidframework/server-local-server";
import type { ITestFluidObject } from "@fluidframework/test-utils/internal";

import { createLoader } from "../utils.js";

function createDSFWithOutOfBandCreate({
deltaConnectionServer,
createContainerCallback,
}: {
deltaConnectionServer: ILocalDeltaConnectionServer;
createContainerCallback: (
summary: ISummaryTree | undefined,
resolvedUrl: IResolvedUrl,
) => Promise<IRequest>;
}) {
return new Proxy<IDocumentServiceFactory>(
new LocalDocumentServiceFactory(deltaConnectionServer),
{
get: (t, p: keyof IDocumentServiceFactory, r) => {
if (p === "createContainer") {
return async (summary, resolvedUrl, logger, clientIsSummarizer) => {
const url = await createContainerCallback(summary, resolvedUrl);
// this is more like the load flow, where we resolve the url
// and create the document service, and it works here, as
// the callback actually does the work of creating the container.
const resolver = new LocalResolver();
return t.createDocumentService(
await resolver.resolve(url),
logger,
clientIsSummarizer,
);
};
}

return Reflect.get(t, p, r);
},
},
);
}

async function createContainerOutOfBand(
deltaConnectionServer: ILocalDeltaConnectionServer,
createContainerParams: {
summary: ISummaryTree | undefined;
resolvedUrl: IResolvedUrl;
},
) {
// this actually creates the container
const { summary, resolvedUrl } = createContainerParams;
const documentServiceFactory = new LocalDocumentServiceFactory(deltaConnectionServer);
const documentService = await documentServiceFactory.createContainer(summary, resolvedUrl);
const resolver = new LocalResolver();
return resolver.getAbsoluteUrl(documentService.resolvedUrl, "");
}

describe("Scenario Test", () => {
it("Create container via a decoupled out of band function and validate both attaching container and freshly loaded container both work.", async () => {
const deltaConnectionServer = LocalDeltaConnectionServer.create();

/*
* Setup a document service factory that uses a user specifiable createContainerCallback.
* This callback could make a different server call, and just needs to return the url
* of the newly created container/file.
*/
let request: IRequest | undefined;
const documentServiceFactory = createDSFWithOutOfBandCreate({
deltaConnectionServer,
createContainerCallback: async (summary, resolvedUrl) =>
(request = {
url: await createContainerOutOfBand(deltaConnectionServer, {
summary,
resolvedUrl,
}),
}),
});

const { loader, codeDetails, urlResolver } = createLoader({
deltaConnectionServer,
documentServiceFactory,
});

const container = await loader.createDetachedContainer(codeDetails);

{
// put a bit of data in the detached container so we can validate later
const entryPoint: FluidObject<ITestFluidObject> = await container.getEntryPoint();
entryPoint.ITestFluidObject?.root.set("someKey", "someValue");
}

// kicking off attach will end up calling the create container callback
// which will actually create the container, and eventually finish the attach
await container.attach(urlResolver.createCreateNewRequest("test"));

{
// just reuse the same server, nothing else from the initial create
const { loader: loader2 } = createLoader({ deltaConnectionServer });

// ensure and use the url we got from out of band create to load the container
assert(request !== undefined);
const container2 = await loader2.resolve(request);

// ensure the newly loaded container has the data we expect.
const entryPoint: FluidObject<ITestFluidObject> = await container2.getEntryPoint();
assert.strictEqual(entryPoint.ITestFluidObject?.root.get("someKey"), "someValue");
}
});
});
5 changes: 2 additions & 3 deletions packages/test/local-server-tests/src/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"extends": "../../../../../common/build/build-common/tsconfig.test.node16.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "../../dist/test",
"rootDir": "../",
"outDir": "../../lib",
"types": ["mocha"],
"noUncheckedIndexedAccess": false,
"exactOptionalPropertyTypes": false,
},
"include": ["./**/*"],
}
108 changes: 108 additions & 0 deletions packages/test/local-server-tests/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct/internal";
import {
type IFluidCodeDetails,
type IHostLoader,
type ILoaderOptions,
type IRuntimeFactory,
ICodeDetailsLoader,
} from "@fluidframework/container-definitions/internal";
import { Loader } from "@fluidframework/container-loader/internal";
import type {
IDocumentServiceFactory,
IUrlResolver,
} from "@fluidframework/driver-definitions/internal";
import {
LocalDocumentServiceFactory,
LocalResolver,
} from "@fluidframework/local-driver/internal";
import { SharedMap } from "@fluidframework/map/internal";
import type { IFluidDataStoreFactory } from "@fluidframework/runtime-definitions/internal";
import { ILocalDeltaConnectionServer } from "@fluidframework/server-local-server";
import { TestFluidObjectFactory, LocalCodeLoader } from "@fluidframework/test-utils/internal";

/**
* This allows the input object to be general,
* and the default object to be specific,
* which maintains strong typing for both inputs, and the defaults in the result.
* So if a user specifies a value, that values type will be strongly specified on the Result.
* However if the user does not specify an option input, the result will also get a strong
* type based the default.
*/
export type OptionalToDefault<TInput, TDefault> = {
[P in keyof TDefault]: P extends keyof TInput
? Exclude<TInput[P], undefined> extends never
? TDefault[P]
: TInput[P]
: TDefault[P];
};

export interface CreateLoaderParams {
deltaConnectionServer: ILocalDeltaConnectionServer;
codeDetails?: IFluidCodeDetails;
defaultDataStoreFactory?: IFluidDataStoreFactory;
runtimeFactory?: IRuntimeFactory;
codeLoader?: ICodeDetailsLoader;
documentServiceFactory?: IDocumentServiceFactory;
urlResolver?: IUrlResolver;
options?: ILoaderOptions;
}

export interface CreateLoaderDefaultResults
extends Required<Omit<CreateLoaderParams, "options">> {
documentServiceFactory: LocalDocumentServiceFactory;
urlResolver: LocalResolver;
codeLoader: LocalCodeLoader;
defaultDataStoreFactory: TestFluidObjectFactory;
runtimeFactory: ContainerRuntimeFactoryWithDefaultDataStore;
loader: IHostLoader;
}

export function createLoader<T extends CreateLoaderParams>(
opts: T,
): OptionalToDefault<T, CreateLoaderDefaultResults> {
const deltaConnectionServer = opts.deltaConnectionServer;
const documentServiceFactory =
opts.documentServiceFactory ?? new LocalDocumentServiceFactory(deltaConnectionServer);

const urlResolver = opts.urlResolver ?? new LocalResolver();

const defaultDataStoreFactory =
opts.defaultDataStoreFactory ??
new TestFluidObjectFactory([["map", SharedMap.getFactory()]], "default");

const runtimeFactory =
opts.runtimeFactory ??
new ContainerRuntimeFactoryWithDefaultDataStore({
defaultFactory: defaultDataStoreFactory,
registryEntries: [
[defaultDataStoreFactory.type, Promise.resolve(defaultDataStoreFactory)],
],
});

const codeDetails = opts.codeDetails ?? { package: "test" };

const codeLoader = opts.codeLoader ?? new LocalCodeLoader([[codeDetails, runtimeFactory]]);

const loader = new Loader({
codeLoader,
documentServiceFactory,
urlResolver,
});

const rtn: OptionalToDefault<CreateLoaderParams, CreateLoaderDefaultResults> = {
deltaConnectionServer,
documentServiceFactory,
urlResolver,
codeDetails,
defaultDataStoreFactory,
runtimeFactory,
codeLoader,
loader,
};
return rtn as OptionalToDefault<T, CreateLoaderDefaultResults>;
}
9 changes: 5 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ff1b2c7

Please sign in to comment.