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

feat: templategen command e2e integration tests #13993

Merged
merged 6 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslint-dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
"syncable",
"tablename",
"tailwindcss",
"templategen",
"testother",
"testschemadeployer",
"textract",
Expand Down
1 change: 1 addition & 0 deletions packages/amplify-migration-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@aws-sdk/client-appsync": "^3.666.0",
"@aws-sdk/client-cloudcontrol": "^3.658.1",
"@aws-sdk/client-cognito-identity": "^3.670.0",
"@aws-sdk/client-s3": "^3.674.0",
"execa": "^5.1.1",
"fs-extra": "^8.1.0",
"lodash": "^4.17.21"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';
import assert from 'node:assert';
import { createNewProjectDir, npmInstall } from '@aws-amplify/amplify-e2e-core';
import { createNewProjectDir, generateRandomShortId, npmInstall } from '@aws-amplify/amplify-e2e-core';
import { createGen2Renderer } from '@aws-amplify/amplify-gen2-codegen';
import { copyFunctionFile } from '../function_utils';
import { copyGen1Schema } from '../api_utils';
Expand Down Expand Up @@ -46,7 +46,7 @@ void describe('Codegen E2E tests', () => {
beforeEach(async () => {
const baseDir = process.env.INIT_CWD ?? process.cwd();
projRoot = await createNewProjectDir('codegen_e2e_flow_test', path.join(baseDir, '..', '..'));
projName = `test${Math.floor(Math.random() * 1000000)}`;
projName = `test${generateRandomShortId()}`;
});

afterEach(async () => {
Expand All @@ -60,8 +60,7 @@ void describe('Codegen E2E tests', () => {
await runCodegenCommand(projRoot);
await copyFunctionFile(projRoot, 'function', gen1FunctionName);
await copyGen1Schema(projRoot, projName);
// TODO: replace below line with correct package version
await updatePackageDependency(projRoot, '@aws-amplify/backend', '0.0.0-test-20241003180022');
await updatePackageDependency(projRoot, '@aws-amplify/backend');
await npmInstall(projRoot);
const gen2StackName = await runGen2SandboxCommand(projRoot);
await assertAuthResource(projRoot, gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1Region);
Expand All @@ -78,8 +77,7 @@ void describe('Codegen E2E tests', () => {
await runCodegenCommand(projRoot);
await copyFunctionFile(projRoot, 'auth', gen1FunctionName);
await removeErrorThrowsFromAuthResourceFile(projRoot);
// TODO: replace below line with correct package version
await updatePackageDependency(projRoot, '@aws-amplify/backend', '0.0.0-test-20241003180022');
await updatePackageDependency(projRoot, '@aws-amplify/backend');
Copy link
Member

Choose a reason for hiding this comment

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

are todo comments relevant still ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Irrelevant and removed - b28e101

await npmInstall(projRoot);
await toggleSandboxSecrets(projRoot, 'set');
const gen2StackName = await runGen2SandboxCommand(projRoot);
Expand All @@ -94,8 +92,7 @@ void describe('Codegen E2E tests', () => {
projRoot,
);
await runCodegenCommand(projRoot);
// TODO: replace below line with correct package version
await updatePackageDependency(projRoot, '@aws-amplify/backend', '0.0.0-test-20241003180022');
await updatePackageDependency(projRoot, '@aws-amplify/backend');
await npmInstall(projRoot);
await runGen2SandboxCommand(projRoot);
await assertAuthResource(projRoot, gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1Region);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import path from 'node:path';
import assert from 'node:assert';
import { createNewProjectDir, npmInstall, deleteS3Bucket, generateRandomShortId } from '@aws-amplify/amplify-e2e-core';
import { assertDefaultGen1Setup } from '../assertions';
import { setupAndPushDefaultGen1Project, runCodegenCommand, runGen2SandboxCommand, cleanupProjects } from '..';
import { copyFunctionFile } from '../function_utils';
import { copyGen1Schema } from '../api_utils';
import { updatePackageDependency } from '../updatePackageJson';
import { createS3Bucket } from '../sdk_calls';
import { runTemplategenCommand, stackRefactor } from '../templategen';

void describe('Templategen E2E tests', () => {
void describe('Full Migration Templategen Flow', () => {
let projRoot: string;
let projName: string;
let bucketName: string;

beforeEach(async () => {
const baseDir = process.env.INIT_CWD ?? process.cwd();
projRoot = await createNewProjectDir('templategen_e2e_flow_test', path.join(baseDir, '..', '..'));
projName = `test${generateRandomShortId()}`;
bucketName = `testbucket${generateRandomShortId()}`;
});

afterEach(async () => {
await cleanupProjects(projRoot);
await deleteS3Bucket(bucketName);
});

void it('should init a project & add auth, function, storage, api with defaults & perform full migration templategen flow', async () => {
await setupAndPushDefaultGen1Project(projRoot, projName);
const { gen1StackName, gen1FunctionName, gen1Region } = await assertDefaultGen1Setup(projRoot);
await createS3Bucket(bucketName, gen1Region);
assert(gen1StackName);
await runCodegenCommand(projRoot);
await copyFunctionFile(projRoot, 'function', gen1FunctionName);
await copyGen1Schema(projRoot, projName);
await updatePackageDependency(projRoot, '@aws-amplify/backend');
await npmInstall(projRoot);
const gen2StackName = await runGen2SandboxCommand(projRoot);
assert(gen2StackName);
await runTemplategenCommand(projRoot, gen1StackName, gen2StackName);
await stackRefactor(projRoot, 'auth', bucketName);
await stackRefactor(projRoot, 'storage', bucketName);
});
});
});
20 changes: 15 additions & 5 deletions packages/amplify-migration-e2e/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { removeProperties } from '.';
import { $TSAny } from '@aws-amplify/amplify-cli-core';
import assert from 'node:assert';

async function assertUserPool(gen1Meta: $TSAny, gen1Region: string) {
export async function assertUserPool(gen1Meta: $TSAny, gen1Region: string) {
const { UserPoolId: gen1UserPoolId } = Object.keys(gen1Meta.auth).map((key) => gen1Meta.auth[key])[0].output;
const cloudUserPool = await getUserPool(gen1UserPoolId, gen1Region);
expect(cloudUserPool.UserPool).toBeDefined();
return { gen1UserPoolId };
}

async function assertUserPoolClients(gen1Meta: $TSAny, gen1Region: string) {
export async function assertUserPoolClients(gen1Meta: $TSAny, gen1Region: string) {
const {
UserPoolId: userPoolId,
AppClientIDWeb: appClientIdWeb,
Expand All @@ -32,7 +32,7 @@ async function assertUserPoolClients(gen1Meta: $TSAny, gen1Region: string) {
return { gen1ClientIds };
}

async function assertIdentityPool(gen1Meta: $TSAny, gen1Region: string) {
export async function assertIdentityPool(gen1Meta: $TSAny, gen1Region: string) {
const { IdentityPoolId: gen1IdentityPoolId } = Object.keys(gen1Meta.auth).map((key) => gen1Meta.auth[key])[0].output;
const cloudIdentityPool = await getIdentityPool(gen1IdentityPoolId, gen1Region);
expect(cloudIdentityPool).toBeDefined();
Expand All @@ -48,7 +48,7 @@ async function assertFunction(gen1Meta: $TSAny, gen1Region: string) {
return { gen1FunctionName };
}

async function assertStorage(gen1Meta: $TSAny, gen1Region: string) {
export async function assertStorage(gen1Meta: $TSAny, gen1Region: string) {
const { BucketName: gen1BucketName } = Object.keys(gen1Meta.storage).map((key) => gen1Meta.storage[key])[0].output;
expect(gen1BucketName).toBeDefined();
const bucketExists = await checkIfBucketExists(gen1BucketName, gen1Region);
Expand Down Expand Up @@ -88,6 +88,7 @@ async function assertUserPoolGroups(gen1Meta: $TSAny) {

export async function assertDefaultGen1Setup(projRoot: string) {
const gen1Meta = getProjectMeta(projRoot);
const gen1StackName = gen1Meta.providers.awscloudformation.StackName;
const gen1Region = gen1Meta.providers.awscloudformation.Region;
const { gen1UserPoolId } = await assertUserPool(gen1Meta, gen1Region);
const { gen1FunctionName } = await assertFunction(gen1Meta, gen1Region);
Expand All @@ -96,7 +97,16 @@ export async function assertDefaultGen1Setup(projRoot: string) {
const { gen1GraphqlApiId } = await assertAPI(gen1Meta, gen1Region);
const { gen1IdentityPoolId } = await assertIdentityPool(gen1Meta, gen1Region);
const { gen1ClientIds } = await assertUserPoolClients(gen1Meta, gen1Region);
return { gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1FunctionName, gen1BucketName, gen1GraphqlApiId, gen1Region };
return {
gen1StackName,
gen1UserPoolId,
gen1ClientIds,
gen1IdentityPoolId,
gen1FunctionName,
gen1BucketName,
gen1GraphqlApiId,
gen1Region,
};
}

export async function assertAuthWithMaxOptionsGen1Setup(projRoot: string) {
Expand Down
8 changes: 8 additions & 0 deletions packages/amplify-migration-e2e/src/envVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const envVariable = {
set: (name: string, value: string): void => {
process.env[name] = value;
},
delete: (name: string): void => {
delete process.env[name];
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from 'path';
import { RefactorCategory } from './templategen';
import { getProjectMeta } from '@aws-amplify/amplify-e2e-core';
import { assertIdentityPool, assertStorage, assertUserPool, assertUserPoolClients } from './assertions';
import { getResourceDetails } from './sdk_calls';

async function getGen1AuthResourceDetails(projRoot: string) {
const gen1ProjRoot = path.join(projRoot, '.amplify', 'migration');
const gen1Meta = getProjectMeta(gen1ProjRoot);
const gen1Region = gen1Meta.providers.awscloudformation.Region;
const { gen1UserPoolId } = await assertUserPool(gen1Meta, gen1Region);
const { gen1IdentityPoolId } = await assertIdentityPool(gen1Meta, gen1Region);
const { gen1ClientIds } = await assertUserPoolClients(gen1Meta, gen1Region);
const gen1ClientIdWeb = gen1ClientIds[0];
const gen1ResourceIds = [gen1UserPoolId, gen1IdentityPoolId, gen1ClientIdWeb];

const gen1ResourceDetails = await Promise.all([
getResourceDetails('AWS::Cognito::UserPool', gen1UserPoolId, gen1Region),
getResourceDetails('AWS::Cognito::IdentityPool', gen1IdentityPoolId, gen1Region),
getResourceDetails('AWS::Cognito::UserPoolClient', `${gen1UserPoolId}|${gen1ClientIdWeb}`, gen1Region),
]);

return { gen1ResourceIds, gen1ResourceDetails };
}

async function getGen1StorageResourceDetails(projRoot: string) {
const gen1ProjRoot = path.join(projRoot, '.amplify', 'migration');
const gen1Meta = getProjectMeta(gen1ProjRoot);
const gen1Region = gen1Meta.providers.awscloudformation.Region;
const { gen1BucketName } = await assertStorage(gen1Meta, gen1Region);
const gen1ResourceIds = [gen1BucketName];
const gen1ResourceDetails = await getResourceDetails('AWS::S3::Bucket', gen1BucketName, gen1Region);
return { gen1ResourceIds, gen1ResourceDetails };
}

export async function getGen1ResourceDetails(projRoot: string, category: RefactorCategory) {
if (category === 'auth') {
return await getGen1AuthResourceDetails(projRoot);
} else {
return await getGen1StorageResourceDetails(projRoot);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getProjectOutputs } from './projectOutputs';
import { getResourceDetails } from './sdk_calls';
import { RefactorCategory } from './templategen';

async function getGen2AuthResourceDetails(projRoot: string) {
const gen2Meta = getProjectOutputs(projRoot);
const gen2Region = gen2Meta.auth.aws_region;
const gen2UserPoolId = gen2Meta.auth.user_pool_id;
const gen2ClientIdWeb = gen2Meta.auth.user_pool_client_id;
const gen2IdentityPoolId = gen2Meta.auth.identity_pool_id;
const gen2ResourceIds = [gen2UserPoolId, gen2IdentityPoolId, gen2ClientIdWeb];

const gen2ResourceDetails = await Promise.all([
getResourceDetails('AWS::Cognito::UserPool', gen2UserPoolId, gen2Region),
getResourceDetails('AWS::Cognito::IdentityPool', gen2IdentityPoolId, gen2Region),
getResourceDetails('AWS::Cognito::UserPoolClient', `${gen2UserPoolId}|${gen2ClientIdWeb}`, gen2Region),
]);

return { gen2ResourceIds, gen2ResourceDetails };
}

async function getGen2StorageResourceDetails(projRoot: string) {
const gen2Meta = getProjectOutputs(projRoot);
const gen2Region = gen2Meta.auth.aws_region;
const gen2BucketName = gen2Meta.storage.bucket_name;
const gen2ResourceIds = [gen2BucketName];
const gen2ResourceDetails = await getResourceDetails('AWS::S3::Bucket', gen2BucketName, gen2Region);
return { gen2ResourceIds, gen2ResourceDetails };
}

export async function getGen2ResourceDetails(projRoot: string, category: RefactorCategory) {
if (category === 'auth') {
return await getGen2AuthResourceDetails(projRoot);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: explicitly check for storage if we ever expand to other categories.

return await getGen2StorageResourceDetails(projRoot);
}
}
36 changes: 3 additions & 33 deletions packages/amplify-migration-e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
addAuthWithDefault,
amplifyPush,
getNpxPath,
nspawn as spawn,
addS3WithGuestAccess,
addFunction,
functionBuild,
Expand All @@ -22,13 +21,15 @@ import {
import path from 'node:path';
import { unset } from 'lodash';
import execa from 'execa';
import { deleteGen2Sandbox } from './sandbox';

export * from './sdk_calls';
export * from './assertions';
export * from './projectOutputs';
export * from './updatePackageJson';
export * from './sandbox';

const pushTimeoutMS = 1000 * 60 * 20; // 20 minutes;
export const pushTimeoutMS = 1000 * 60 * 20; // 20 minutes;

export async function setupAndPushDefaultGen1Project(projRoot: string, projName: string) {
await initJSProjectWithProfile(projRoot, { name: projName, disableAmplifyAppCreation: false, includeGen2RecommendationPrompt: false });
Expand Down Expand Up @@ -75,37 +76,6 @@ export function runCodegenCommand(cwd: string) {
}
}

export async function runGen2SandboxCommand(cwd: string) {
const processResult = execa.sync(getNpxPath(), ['ampx', 'sandbox', '--once'], {
cwd,
env: { ...process.env, npm_config_user_agent: 'npm' },
encoding: 'utf-8',
});
if (processResult.exitCode === 0) {
const match = processResult.stdout.match(/arn:aws:cloudformation:.*:stack\/([^/]+)\//);
if (match) {
return match[1];
} else {
throw new Error('Stack name not found in the command output');
}
} else {
throw new Error(`Sandbox command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`);
}
}

function deleteGen2Sandbox(cwd: string) {
return spawn(getNpxPath(), ['ampx', 'sandbox', 'delete'], {
cwd,
stripColors: true,
noOutputTimeout: pushTimeoutMS,
env: { ...process.env, npm_config_user_agent: 'npm' },
})
.wait("Are you sure you want to delete all the resources in your sandbox environment (This can't be undone)?")
.sendConfirmYes()
.wait('Finished deleting.')
.runAsync();
}

export async function cleanupProjects(cwd: string) {
await deleteGen1Project(path.join(cwd, '.amplify', 'migration'));
await deleteGen2Sandbox(cwd);
Expand Down
55 changes: 55 additions & 0 deletions packages/amplify-migration-e2e/src/migrationReadmeParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function extractContent(readmeContent: string, startRegex: string, endRegex: string) {
const pattern = new RegExp(`${startRegex}([\\s\\S]*?)${endRegex}`, 'i');
const match = readmeContent.match(pattern);

if (match && match[1]) {
return match[1].trim();
}
throw new Error('README file parsing failed to get the stack refactor commands');
}

function extractCommands(readmeContent: string) {
const pattern = /```([\s\S]*?)```/g;
const matches = readmeContent.matchAll(pattern);
const commands = [];

for (const match of matches) {
if (match[1]) {
commands.push(match[1].trim());
}
}
if (commands.length === 0) {
throw new Error('README file parsing failed to get the stack refactor commands');
}
return commands;
}

/**
* Sample from the README file for STEP 1:
*
* ### STEP 1: UPDATE GEN-1 AUTH STACK
*
* It is a non-disruptive update since the template only replaces resource references with their resolved values. This is a required step to execute cloudformation stack refactor later.
*
* ```
* aws cloudformation update-stack \
* --stack-name my-auth-stack-name \
* --template-body file://path/to/template.json \
* --parameters '[{"ParameterKey":"authRoleArn","ParameterValue":"arn:aws:iam::123456789012:role/my-auth-role"},{"ParameterKey":"autoVerifiedAttributes","ParameterValue":"email"},{"ParameterKey":"allowUnauthenticatedIdentities","ParameterValue":"false"},{"ParameterKey":"smsVerificationMessage","ParameterValue":"Your verification code is {####}"}]' \
* --capabilities CAPABILITY_NAMED_IAM --tags '[]'
* ```
*
* ```
* aws cloudformation describe-stacks \
* --stack-name my-auth-stack-name
* ```
*/
export function getCommandsFromReadme(readmeContent: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Referencing the comment from the previous PR: #13984 (comment)

Choose a reason for hiding this comment

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

nit: give some sample of the readme fie, like the step 1 portion, would be helpful to visualize the file format.

const step1Content = extractContent(readmeContent, '### STEP 1', '#### Rollback step');
const step2Content = extractContent(readmeContent, '### STEP 2', '#### Rollback step');
const step3Content = extractContent(readmeContent, '### STEP 3', '#### Rollback step');
const step1Commands = extractCommands(step1Content);
const step2commands = extractCommands(step2Content);
const step3Commands = extractCommands(step3Content);
return { step1Commands, step2commands, step3Commands };
}
Loading
Loading