Skip to content

Commit ad8e5b0

Browse files
authored
impl createPartialContext for tests (#358)
## Why + What changed we use createPartialContext to mock complex contexts, good to abstract this into something shared we can use ## Versioning - [ ] Breaking protocol change - [ ] Breaking ts/js API change <!-- Kind reminder to add tests and updated documentation if needed -->
1 parent 1110a09 commit ad8e5b0

File tree

4 files changed

+112
-3
lines changed

4 files changed

+112
-3
lines changed

__tests__/e2e.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert, beforeEach, describe, expect, test, vi } from 'vitest';
22
import {
33
closeAllConnections,
4+
createPartialContext,
45
isReadableDone,
56
numberOfConnections,
67
readNextResult,
@@ -972,6 +973,72 @@ describe.each(testMatrix())(
972973
});
973974
});
974975

976+
test('createPartialContext throws on unmocked property access', async () => {
977+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
978+
type TestContext = {
979+
db: { query: (sql: string) => string };
980+
cache: { get: (key: string) => string };
981+
};
982+
983+
const ctx = createPartialContext<TestContext>({
984+
db: { query: (sql) => `result: ${sql}` },
985+
});
986+
987+
// provided properties work
988+
expect(ctx.db.query('SELECT 1')).toBe('result: SELECT 1');
989+
990+
// unmocked properties throw
991+
expect(() => ctx.cache).toThrow(
992+
'cache is not mocked in the test context',
993+
);
994+
});
995+
996+
test('createPartialContext works as extendedContext with server dispose', async () => {
997+
const clientTransport = getClientTransport('client');
998+
const serverTransport = getServerTransport();
999+
const dbDispose = vi.fn();
1000+
1001+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
1002+
type TestContext = {
1003+
db: { [Symbol.asyncDispose]: () => Promise<void> };
1004+
cache: { get: (key: string) => string };
1005+
};
1006+
1007+
const ctx = createPartialContext<TestContext>({
1008+
db: { [Symbol.asyncDispose]: dbDispose },
1009+
});
1010+
1011+
const ServiceSchema = createServiceSchema<TestContext>();
1012+
const services = {
1013+
test: ServiceSchema.define({
1014+
ping: Procedure.rpc({
1015+
requestInit: Type.Object({}),
1016+
responseData: Type.Object({}),
1017+
async handler() {
1018+
return Ok({});
1019+
},
1020+
}),
1021+
}),
1022+
};
1023+
1024+
const server = createServer(serverTransport, services, {
1025+
extendedContext: ctx,
1026+
});
1027+
addPostTestCleanup(async () => {
1028+
await cleanupTransports([clientTransport, serverTransport]);
1029+
});
1030+
1031+
// server.close() should dispose context values without
1032+
// throwing on unmocked properties (cache)
1033+
await server.close();
1034+
expect(dbDispose).toBeCalledTimes(1);
1035+
await testFinishesCleanly({
1036+
clientTransports: [clientTransport],
1037+
serverTransport,
1038+
server,
1039+
});
1040+
});
1041+
9751042
test('works with non-object schemas', async () => {
9761043
// setup
9771044
const clientTransport = getClientTransport('client');

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@replit/river",
33
"description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!",
4-
"version": "0.213.0",
4+
"version": "0.213.1",
55
"type": "module",
66
"exports": {
77
".": {

testUtil/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,45 @@ export function closeAllConnections<ConnType extends Connection>(
246246
conn.close();
247247
}
248248
}
249+
250+
/**
251+
* Wraps a partial context object in a proxy that throws when accessing
252+
* properties that weren't provided. This is useful for test contexts where
253+
* you only want to mock the dependencies a test actually uses.
254+
*
255+
* Symbols and `then` are allowed through without throwing — river checks
256+
* for `Symbol.asyncDispose` / `Symbol.dispose` on context values during
257+
* `server.close()`, and `then` is checked by the JS runtime when the
258+
* proxy is returned from an async function.
259+
*
260+
* @example
261+
* ```ts
262+
* const ctx = createPartialContext<MyContext>({
263+
* database: mockDb,
264+
* // accessing ctx.redis will throw
265+
* });
266+
*
267+
* const server = createServer(transport, services, {
268+
* extendedContext: ctx,
269+
* });
270+
* ```
271+
*/
272+
export function createPartialContext<T extends Record<string, unknown>>(
273+
partial: Partial<T>,
274+
): T {
275+
return new Proxy(partial as T, {
276+
get(target, prop, receiver) {
277+
if (prop in target) {
278+
return Reflect.get(target, prop, receiver);
279+
}
280+
281+
if (typeof prop === 'string' && prop !== 'then') {
282+
throw new Error(
283+
`${prop} is not mocked in the test context. Provide it via createPartialContext if your test needs it.`,
284+
);
285+
}
286+
287+
return undefined;
288+
},
289+
});
290+
}

0 commit comments

Comments
 (0)