Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add e2e test for lambda data client #2228

Draft
wants to merge 11 commits into
base: stocaaro/client-schema/main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineDeploymentTest } from './deployment.test.template.js';
import { DataAndFunctionTestProjectCreator } from '../../test-project-setup/data_and_function_project.js';

defineDeploymentTest(new DataAndFunctionTestProjectCreator());
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineSandboxTest } from './sandbox.test.template.js';
import { DataAndFunctionTestProjectCreator } from '../../test-project-setup/data_and_function_project.js';

defineSandboxTest(new DataAndFunctionTestProjectCreator());
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { TestProjectBase } from './test_project_base.js';
import fs from 'fs/promises';
import { createEmptyAmplifyProject } from './create_empty_amplify_project.js';
import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
import { TestProjectCreator } from './test_project_creator.js';
import { AmplifyClient } from '@aws-sdk/client-amplify';
import { BackendIdentifier } from '@aws-amplify/plugin-types';
import { LambdaClient } from '@aws-sdk/client-lambda';
import { DeployedResourcesFinder } from '../find_deployed_resource.js';
import { generateClientConfig } from '@aws-amplify/client-config';
import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
import { SemVer } from 'semver';
import crypto from 'node:crypto';
import {
ApolloClient,
ApolloLink,
HttpLink,
InMemoryCache,
} from '@apollo/client/core';
import { AUTH_TYPE, createAuthLink } from 'aws-appsync-auth-link';
import { gql } from 'graphql-tag';
import assert from 'assert';
import { NormalizedCacheObject } from '@apollo/client';
import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js';

// TODO: this is a work around
// it seems like as of amplify v6 , some of the code only runs in the browser ...
// see https://github.com/aws-amplify/amplify-js/issues/12751
if (process.versions.node) {
// node >= 20 now exposes crypto by default. This workaround is not needed: https://github.com/nodejs/node/pull/42083
if (new SemVer(process.versions.node).major < 20) {
// @ts-expect-error altering typing for global to make compiler happy is not worth the effort assuming this is temporary workaround
globalThis.crypto = crypto;
}
}

/**
* Creates the data and function test project.
*/
export class DataAndFunctionTestProjectCreator implements TestProjectCreator {
readonly name = 'data-and-function';

/**
* Creates project creator.
*/
constructor(
private readonly cfnClient: CloudFormationClient = new CloudFormationClient(
e2eToolingClientConfig
),
private readonly amplifyClient: AmplifyClient = new AmplifyClient(
e2eToolingClientConfig
),
private readonly lambdaClient: LambdaClient = new LambdaClient(
e2eToolingClientConfig
),
private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient(
e2eToolingClientConfig
),
private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder()
) {}

createProject = async (e2eProjectDir: string): Promise<TestProjectBase> => {
const { projectName, projectRoot, projectAmplifyDir } =
await createEmptyAmplifyProject(this.name, e2eProjectDir);

const project = new DataAndFunctionTestProject(
projectName,
projectRoot,
projectAmplifyDir,
this.cfnClient,
this.amplifyClient,
this.lambdaClient,
this.cognitoIdentityProviderClient,
this.resourceFinder
);
await fs.cp(
project.sourceProjectAmplifyDirURL,
project.projectAmplifyDirPath,
{
recursive: true,
}
);
return project;
};
}

/**
* The data and function test project.
*/
class DataAndFunctionTestProject extends TestProjectBase {
readonly sourceProjectDirPath = '../../src/test-projects/data-and-function';

readonly sourceProjectAmplifyDirSuffix = `${this.sourceProjectDirPath}/amplify`;

readonly sourceProjectAmplifyDirURL: URL = new URL(
this.sourceProjectAmplifyDirSuffix,
import.meta.url
);

/**
* Create a test project instance.
*/
constructor(
name: string,
projectDirPath: string,
projectAmplifyDirPath: string,
cfnClient: CloudFormationClient,
amplifyClient: AmplifyClient,
private readonly lambdaClient: LambdaClient = new LambdaClient(
e2eToolingClientConfig
),
private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient(
e2eToolingClientConfig
),
private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder()
) {
super(
name,
projectDirPath,
projectAmplifyDirPath,
cfnClient,
amplifyClient
);
}

override async assertPostDeployment(
backendId: BackendIdentifier
): Promise<void> {
await super.assertPostDeployment(backendId);

const clientConfig = await generateClientConfig(backendId, '1.1');
if (!clientConfig.data?.url) {
throw new Error('Data and function project must include data');
}
if (!clientConfig.data.api_key) {
throw new Error('Data and function project must include api_key');
}

// const dataUrl = clientConfig.data?.url;

const httpLink = new HttpLink({ uri: clientConfig.data.url });
const link = ApolloLink.from([
createAuthLink({
url: clientConfig.data.url,
region: clientConfig.data.aws_region,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: clientConfig.data.api_key,
},
}),
// see https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/473#issuecomment-543029072
httpLink,
]);
const apolloClient = new ApolloClient({
link,
cache: new InMemoryCache(),
});

await this.assertDataFunctionCallSucceeds(apolloClient);
await this.assertNoopWithImportCallSucceeds(apolloClient);
}

private assertDataFunctionCallSucceeds = async (
apolloClient: ApolloClient<NormalizedCacheObject>
): Promise<void> => {
const response = await apolloClient.query<number>({
query: gql`
query todoCount {
todoCount
}
`,
variables: {},
});

assert.deepEqual(response.data, { todoCount: 0 });
};

private assertNoopWithImportCallSucceeds = async (
apolloClient: ApolloClient<NormalizedCacheObject>
): Promise<void> => {
const response = await apolloClient.query<number>({
query: gql`
query noopImport {
noopImport
}
`,
variables: {},
});

assert.deepEqual(response.data, { noopImport: 'STATIC TEST RESPONSE' });
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineBackend } from '@aws-amplify/backend';
import { data } from './data/resource.js';
import { todoCount } from './functions/todo-count/resource.js';
import { noopImport } from './functions/noop-import/resource.js';

const backend = defineBackend({ data, todoCount, noopImport });
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { a, ClientSchema, defineData } from '@aws-amplify/backend';
import { todoCount } from '../functions/todo-count/resource.js';
import { noopImport } from '../functions/noop-import/resource.js';

const schema = a
.schema({
Todo: a
.model({
title: a.string().required(),
done: a.boolean().default(false), // default value is false
})
.authorization((allow) => [allow.publicApiKey()]),
todoCount: a
.query()
.arguments({})
.returns(a.integer())
.handler(a.handler.function(todoCount))
.authorization((allow) => [allow.publicApiKey()]),
noopImport: a
.query()
.arguments({})
.returns(a.string())
.handler(a.handler.function(noopImport))
.authorization((allow) => [allow.publicApiKey()]),
})
.authorization((allow) => [
allow.resource(todoCount),
allow.resource(noopImport),
]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
// API Key is used for a.allow.public() rules
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Handler } from 'aws-lambda';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '../../data/resource.js';
import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime';
// @ts-ignore
import { env } from '$amplify/env/noop-import.js';
import { S3Client } from '@aws-sdk/client-s3';

const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(
env
);

Amplify.configure(resourceConfig, libraryOptions);

const client = generateClient<Schema>();

export const handler: Handler = async () => {
const _s3Client = new S3Client();
const _todos = await client.models.Todo.list();
return 'STATIC TEST RESPONSE';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineFunction } from '@aws-amplify/backend';

export const noopImport = defineFunction({
name: 'noop-import',
entry: './handler.ts',
timeoutSeconds: 30,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Handler } from 'aws-lambda';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '../../data/resource.js';
import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime';
// @ts-ignore
Copy link
Member Author

Choose a reason for hiding this comment

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

Esbuild during synth is fine with all of this, but the ts check before synth is unhappy. It looks like a workaround was used for data-storage-auth-with-triggers-ts of setting up fake files to make the pre-synth check happy. Should I replicate that here? It would avoid the exclude change in the tsconfig below too.

Seems fine, but also very fake since this file isn't being built in the project.

import { env } from '$amplify/env/todo-count.js';

const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(
env
);

Amplify.configure(resourceConfig, libraryOptions);

const client = generateClient<Schema>();

export const handler: Handler = async () => {
const todos = await client.models.Todo.list();
return todos.data.length;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineFunction } from '@aws-amplify/backend';

export const todoCount = defineFunction({
name: 'todo-count',
entry: './handler.ts',
timeoutSeconds: 30,
});