Skip to content

Commit

Permalink
Add ContextRunner types for tests (#7831)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
  • Loading branch information
dcousens and dcousens committed Sep 29, 2022
1 parent 51ab286 commit cacc4e1
Show file tree
Hide file tree
Showing 40 changed files with 216 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .changeset/bye-slow-fish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': major
---

Removes `createContext` from `TestArgs` returned by `setupTestEnv`
5 changes: 5 additions & 0 deletions .changeset/lazy-times-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/auth': patch
---

Adds type refinement for `withAuth` using a TypeInfo type parameter
5 changes: 5 additions & 0 deletions .changeset/oh-my-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': major
---

Changes `setupTestEnv` to contextualise by `TypeInfo` (import from `.keystone/types`) instead of `Context`
5 changes: 3 additions & 2 deletions examples/ecommerce/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'dotenv/config';
import { insertSeedData } from './seed-data';
import { sendPasswordResetEmail } from './lib/mail';
import { extendGraphqlSchema } from './mutations';
import { TypeInfo } from '.keystone/types';

const databaseURL = process.env.DATABASE_URL || 'file:./keystone.db';

Expand All @@ -27,7 +28,7 @@ const { withAuth } = createAuth({
secretField: 'password',
initFirstItem: {
fields: ['name', 'email', 'password'],
// TODO: Add in inital roles here
// TODO: Add in initial roles here
},
passwordResetLink: {
async sendToken(args) {
Expand All @@ -39,7 +40,7 @@ const { withAuth } = createAuth({
});

export default withAuth(
config({
config<TypeInfo>({
server: {
cors: {
origin: [process.env.FRONTEND_URL!],
Expand Down
2 changes: 1 addition & 1 deletion examples/ecommerce/tests/mutations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Context } from '.keystone/types';
const FAKE_ID = 'cinjfgbkjnfg';

const asUser = (context: Context, itemId?: string) => context.withSession({ itemId, data: {} });
const runner = setupTestRunner<Context>({ config });
const runner = setupTestRunner({ config });

describe(`Custom mutations`, () => {
describe('checkout(token)', () => {
Expand Down
15 changes: 9 additions & 6 deletions examples/testing/example.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { setupTestEnv, setupTestRunner, TestEnv } from '@keystone-6/core/testing';
import config from './keystone';
import { Context } from '.keystone/types';
import { Context, TypeInfo } from '.keystone/types';

// Setup a test runner which will provide a clean test environment
// with access to our GraphQL API for each test.
const runner = setupTestRunner<Context>({ config });
const runner = setupTestRunner({ config });

describe('Example tests using test runner', () => {
test(
Expand Down Expand Up @@ -143,19 +143,22 @@ describe('Example tests using test environment', () => {
//
// This gives us the opportunity to seed test data once up front and use it in
// multiple tests.
let testEnv: TestEnv<Context>, context: Context;
let testEnv: TestEnv<TypeInfo>;
let context: Context;
let person: { id: string };

beforeAll(async () => {
testEnv = await setupTestEnv<Context>({ config });
testEnv = await setupTestEnv({ config });
context = testEnv.testArgs.context;

await testEnv.connect();

// Create a person in the database to be used in multiple tests
person = (await context.query.Person.createOne({
person = await context.db.Person.createOne({
data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' },
})) as { id: string };
});
});

afterAll(async () => {
await testEnv.disconnect();
});
Expand Down
3 changes: 2 additions & 1 deletion examples/testing/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { config } from '@keystone-6/core';
import { statelessSessions } from '@keystone-6/core/session';
import { createAuth } from '@keystone-6/auth';
import { lists } from './schema';
import { TypeInfo } from '.keystone/types';

// createAuth configures signin functionality based on the config below. Note this only implements
// authentication, i.e signing in as an item using identity and secret fields in a list. Session
Expand Down Expand Up @@ -33,7 +34,7 @@ const session = statelessSessions({
// We wrap our config using the withAuth function. This will inject all
// the extra config required to add support for authentication in our system.
export default withAuth(
config({
config<TypeInfo>({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({
* It validates the auth config against the provided keystone config, and preserves existing
* config by composing existing extendGraphqlSchema functions and ui config.
*/
const withAuth = (keystoneConfig: KeystoneConfig): KeystoneConfig => {
const withAuth = <TypeInfo extends BaseKeystoneTypeInfo>(
keystoneConfig: KeystoneConfig<TypeInfo>
): KeystoneConfig<TypeInfo> => {
validateConfig(keystoneConfig);
let ui = keystoneConfig.ui;
if (!keystoneConfig.ui?.isDisabled) {
Expand Down
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
"image-type": "^4.1.0",
"inflection": "^1.13.1",
"intersection-observer": "^0.12.0",
"memoize-one": "^6.0.0",
"meow": "^9.0.0",
"micro": "^9.3.4",
"next": "^12.2.4",
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/lib/schema-type-printer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export function printGeneratedTypes(
printInterimType(list, listKey, gqlNames.updateInputName, 'Update')
);

listsTypeInfo.push(` readonly ${listKey}: ${listTypeInfoName};`);
listsTypeInfo.push(` readonly ${listKey}: ${listTypeInfoName};`);
listsNamespaces.push(printListTypeInfo(listKey, list));
}

Expand All @@ -223,7 +223,6 @@ export function printGeneratedTypes(
'',
interimCreateUpdateTypes.join('\n\n'),
'',
'',
'export declare namespace Lists {',
...listsNamespaces,
'}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ type ResolvedTodoUpdateInput = {
title?: import('.prisma/client').Prisma.TodoUpdateInput["title"];
};
export declare namespace Lists {
export type Todo = import('@keystone-6/core').ListConfig<Lists.Todo.TypeInfo, any>;
namespace Todo {
Expand Down Expand Up @@ -145,7 +144,7 @@ export type Context = import('@keystone-6/core/types').KeystoneContext<TypeInfo>
export type TypeInfo = {
lists: {
readonly Todo: Lists.Todo.TypeInfo;
readonly Todo: Lists.Todo.TypeInfo;
};
prisma: import('.prisma/client').PrismaClient;
};
Expand Down
65 changes: 35 additions & 30 deletions packages/core/src/testing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import path from 'path';
import crypto from 'crypto';
import fs from 'fs';
import memoizeOne from 'memoize-one';
import type { CreateContext, KeystoneConfig, KeystoneContext } from './types';
import { createHash } from 'crypto';
import { mkdirSync } from 'fs';
import type { BaseKeystoneTypeInfo, KeystoneConfig, KeystoneContext } from './types';
import {
getCommittedArtifacts,
writeCommittedArtifacts,
Expand All @@ -11,43 +10,41 @@ import {
} from './artifacts';
import { pushPrismaSchemaToDatabase } from './migrations';
import { initConfig, createSystem } from './system';
import { getContext } from './context';

export type TestArgs<Context extends KeystoneContext = KeystoneContext> = {
context: Context;
createContext: CreateContext;
config: KeystoneConfig;
export type TestArgs<TypeInfo extends BaseKeystoneTypeInfo> = {
context: KeystoneContext<TypeInfo>;
config: KeystoneConfig<TypeInfo>;
};

export type TestEnv<Context extends KeystoneContext = KeystoneContext> = {
export type TestEnv<TypeInfo extends BaseKeystoneTypeInfo> = {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
testArgs: TestArgs<Context>;
testArgs: TestArgs<TypeInfo>;
};

const _hashPrismaSchema = memoizeOne(prismaSchema =>
crypto.createHash('md5').update(prismaSchema).digest('hex')
);
function sha1(text: string) {
return createHash('sha1').update(text).digest('hex');
}

const _alreadyGeneratedProjects = new Set<string>();
export async function setupTestEnv<Context extends KeystoneContext>({
config: _config,
}: {
config: KeystoneConfig;
}): Promise<TestEnv<Context>> {
async function generateSchemas<TypeInfo extends BaseKeystoneTypeInfo>(
_config: KeystoneConfig<TypeInfo>
) {
// Force the UI to always be disabled.
const config = initConfig({ ..._config, ui: { ..._config.ui, isDisabled: true } });
const { graphQLSchema, getKeystone } = createSystem(config);

const { graphQLSchema } = createSystem(config);
const artifacts = await getCommittedArtifacts(graphQLSchema, config);
const hash = _hashPrismaSchema(artifacts.prisma);

const hash = sha1(artifacts.prisma);
const artifactPath = path.resolve('.keystone', 'tests', hash);

if (!_alreadyGeneratedProjects.has(hash)) {
_alreadyGeneratedProjects.add(hash);
fs.mkdirSync(artifactPath, { recursive: true });
mkdirSync(artifactPath, { recursive: true });
await writeCommittedArtifacts(artifacts, artifactPath);
await generateNodeModulesArtifacts(graphQLSchema, config, artifactPath);
}

await pushPrismaSchemaToDatabase(
config.db.url,
config.db.shadowDatabaseUrl,
Expand All @@ -56,27 +53,35 @@ export async function setupTestEnv<Context extends KeystoneContext>({
true // shouldDropDatabase
);

const { connect, disconnect, createContext } = getKeystone(requirePrismaClient(artifactPath));
return artifactPath;
}

export async function setupTestEnv<TypeInfo extends BaseKeystoneTypeInfo>({
config,
}: {
config: KeystoneConfig<TypeInfo>;
}): Promise<TestEnv<TypeInfo>> {
const artifactPath = await generateSchemas<TypeInfo>(config);
const { connect, context, disconnect } = getContext(config, requirePrismaClient(artifactPath));

return {
connect,
disconnect,
testArgs: {
context: createContext() as Context,
createContext,
context,
config,
},
};
}

export function setupTestRunner<Context extends KeystoneContext>({
export function setupTestRunner<TypeInfo extends BaseKeystoneTypeInfo>({
config,
}: {
config: KeystoneConfig;
config: KeystoneConfig<TypeInfo>;
}) {
return (testFn: (testArgs: TestArgs<Context>) => Promise<void>) => async () => {
return (testFn: (testArgs: TestArgs<TypeInfo>) => Promise<void>) => async () => {
// Reset the database to be empty for every test.
const { connect, disconnect, testArgs } = await setupTestEnv<Context>({ config });
const { connect, disconnect, testArgs } = await setupTestEnv({ config });
await connect();

try {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export type CreateRequestContext<TypeInfo extends BaseKeystoneTypeInfo> = (
res: ServerResponse
) => Promise<KeystoneContext<TypeInfo>>;

export type CreateContext = (args: {
export type CreateContext<Context extends KeystoneContext = KeystoneContext> = (args: {
sessionContext?: SessionContext<any>;
sudo?: boolean;
req?: IncomingMessage;
}) => KeystoneContext;
}) => Context;

export type SessionImplementation = {
createSessionContext(
Expand Down
8 changes: 5 additions & 3 deletions tests/api-tests/access-control/field-access.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { KeystoneContext } from '@keystone-6/core/types';
import { setupTestEnv, TestEnv } from '@keystone-6/api-tests/test-runner';
import { ExecutionResult } from 'graphql';
import { expectAccessDenied } from '../utils';
import { expectAccessDenied, ContextFromConfig, TypeInfoFromConfig } from '../utils';
import { nameFn, fieldMatrix, getFieldName, getItemListName, config } from './utils';

type IdType = any;
Expand All @@ -11,8 +10,10 @@ describe(`Field access`, () => {
const mode = 'item';
const listKey = nameFn[mode](listAccess);

let testEnv: TestEnv, context: KeystoneContext;
let testEnv: TestEnv<TypeInfoFromConfig<typeof config>>;
let context: ContextFromConfig<typeof config>;
let items: Record<string, { id: IdType; name: string }[]>;

beforeAll(async () => {
testEnv = await setupTestEnv({ config });
context = testEnv.testArgs.context;
Expand All @@ -29,6 +30,7 @@ describe(`Field access`, () => {
})) as { id: IdType; name: string }[];
}
});

afterAll(async () => {
await testEnv.disconnect();
});
Expand Down
4 changes: 2 additions & 2 deletions tests/api-tests/access-control/list-access.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLError, ExecutionResult } from 'graphql';
import { KeystoneContext } from '@keystone-6/core/types';
import { setupTestEnv, TestEnv } from '@keystone-6/api-tests/test-runner';
import { expectAccessDenied } from '../utils';
import { expectAccessDenied, TypeInfoFromConfig } from '../utils';
import {
nameFn,
listAccessVariations,
Expand Down Expand Up @@ -37,7 +37,7 @@ const expectNoAccessMany = (
type IdType = any;

describe(`List access`, () => {
let testEnv: TestEnv, context: KeystoneContext;
let testEnv: TestEnv<TypeInfoFromConfig<typeof config>>, context: KeystoneContext;
let items: Record<string, { id: IdType; name: string }[]>;
beforeAll(async () => {
testEnv = await setupTestEnv({ config });
Expand Down
5 changes: 2 additions & 3 deletions tests/api-tests/fields/types/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { text } from '@keystone-6/core/fields';
import { document } from '@keystone-6/fields-document';
import { list } from '@keystone-6/core';
import { setupTestEnv, setupTestRunner } from '@keystone-6/api-tests/test-runner';
import { KeystoneContext } from '@keystone-6/core/types';
import { component, fields } from '@keystone-6/fields-document/component-blocks';
import { allowAll } from '@keystone-6/core/access';
import { apiTestConfig, expectInternalServerError } from '../../utils';
import { apiTestConfig, ContextFromRunner, expectInternalServerError } from '../../utils';
import { withServer } from '../../with-server';

const runner = setupTestRunner({
Expand Down Expand Up @@ -56,7 +55,7 @@ const runner = setupTestRunner({
}),
});

const initData = async ({ context }: { context: KeystoneContext }) => {
const initData = async ({ context }: { context: ContextFromRunner<typeof runner> }) => {
const alice = await context.query.Author.createOne({ data: { name: 'Alice' } });
const bob = await context.query.Author.createOne({
data: {
Expand Down
2 changes: 1 addition & 1 deletion tests/api-tests/hooks/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const runner = setupTestRunner({
}
},
validateDelete: ({ item, addValidationError }) => {
if (item.name.startsWith('no delete')) {
if (typeof item.name === 'string' && item.name.startsWith('no delete')) {
addValidationError('Deleting this item would be bad');
}
},
Expand Down
1 change: 0 additions & 1 deletion tests/api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"globby": "^11.0.4",
"graphql": "^16.6.0",
"graphql-upload": "^15.0.2",
"memoize-one": "^6.0.0",
"mime": "^3.0.0",
"node-fetch": "^2.6.7",
"prisma": "4.3.1",
Expand Down
5 changes: 2 additions & 3 deletions tests/api-tests/queries/cache-hints.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { CacheScope } from 'apollo-server-types';
import { text, relationship, integer } from '@keystone-6/core/fields';
import { list, graphQLSchemaExtension } from '@keystone-6/core';
import { KeystoneContext } from '@keystone-6/core/types';
import { setupTestRunner } from '@keystone-6/api-tests/test-runner';
import { allowAll } from '@keystone-6/core/access';
import { apiTestConfig } from '../utils';
import { apiTestConfig, ContextFromRunner } from '../utils';
import { withServer } from '../with-server';

const runner = setupTestRunner({
Expand Down Expand Up @@ -75,7 +74,7 @@ const runner = setupTestRunner({
}),
});

const addFixtures = async (context: KeystoneContext) => {
const addFixtures = async (context: ContextFromRunner<typeof runner>) => {
const users = await context.query.User.createMany({
data: [
{ name: 'Jess', favNumber: 1 },
Expand Down
Loading

1 comment on commit cacc4e1

@vercel
Copy link

@vercel vercel bot commented on cacc4e1 Sep 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.