Skip to content
Open
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
22 changes: 22 additions & 0 deletions amplify-migration-apps/_test-common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# _test-common

Shared test utilities for the Amplify migration test apps. These helpers handle Cognito user provisioning, test execution, and common type definitions used across all apps in `amplify-migration-apps/`.

## Files

| File | Description |
|------|-------------|
| `test-apps-test-utils.ts` | Shared type definitions (`AmplifyConfig`, `TestUser`, `TestCredentials`, etc.) and barrel re-exports for backwards compatibility. |
| `signup.ts` | `provisionTestUser` — creates a test user via `AdminCreateUser` and sets a permanent password. Supports email, phone, and username signin patterns. Works even when self-signup is disabled on the user pool. |
| `runner.ts` | `TestRunner` class — runs async test functions, collects failures, and prints a summary. |
| `test-credentials.json` | Static test credentials (email, phone, username, password) consumed by test scripts. |

## Usage

```typescript
import { TestRunner } from '../_test-common/runner';
import { provisionTestUser } from '../_test-common/signup';
import testCredentials from '../_test-common/test-credentials.json';
```

`provisionTestUser` reads the Amplify config to determine the correct Cognito auth flow (which attribute is the signin identifier, which attributes are required at signup) and provisions a confirmed user via admin APIs. It does **not** call `signIn` — the caller handles that in its own module scope so the Amplify auth singleton retains the tokens.
61 changes: 61 additions & 0 deletions amplify-migration-apps/_test-common/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface TestFailure {
name: string;
message: string;
stack?: string;
}

function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'object' && error !== null) {
// Handle GraphQL-style errors: { errors: [{ message: "..." }] }
if ('errors' in error) {
const gqlErrors = (error as { errors: unknown }).errors;
if (Array.isArray(gqlErrors) && gqlErrors.length > 0) {
const first = gqlErrors[0] as { message?: string };
if (typeof first.message === 'string') {
return first.message;
}
}
}
return JSON.stringify(error, null, 2);
}
return String(error);
}

export class TestRunner {
readonly failures: TestFailure[] = [];

async runTest<T>(name: string, testFn: () => Promise<T>): Promise<T | null> {
try {
const result = await testFn();
return result;
} catch (error: unknown) {
const stack = error instanceof Error ? error.stack : undefined;
this.failures.push({ name, message: getErrorMessage(error), stack });
return null;
}
}

printSummary(): void {
console.log('\n' + '='.repeat(50));
console.log('📊 TEST SUMMARY');
console.log('='.repeat(50));

if (this.failures.length === 0) {
console.log('\n✅ All tests passed!');
} else {
console.log(`\n❌ ${this.failures.length} test(s) failed:\n`);
this.failures.forEach((f) => {
console.log(` • ${f.name}`);
console.log(` Error: ${f.message}`);
if (f.stack) {
console.log(` Stack: ${f.stack}`);
}
console.log('');
});
process.exit(1);
}
}
}
164 changes: 164 additions & 0 deletions amplify-migration-apps/_test-common/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {
CognitoIdentityProviderClient,
AdminCreateUserCommand,
AdminSetUserPasswordCommand,
type AttributeType,
} from '@aws-sdk/client-cognito-identity-provider';
import type { AmplifyConfig, SigninIdentifier, SignupAttribute, TestCredentials, TestUser } from './test-apps-test-utils';

interface ResolvedAuthConfig {
signinIdentifier: SigninIdentifier;
signupAttributes: SignupAttribute[];
}

function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'object' && error !== null) {
if ('errors' in error) {
const gqlErrors = (error as { errors: unknown }).errors;
if (Array.isArray(gqlErrors) && gqlErrors.length > 0) {
const first = gqlErrors[0] as { message?: string };
if (typeof first.message === 'string') {
return first.message;
}
}
}
return JSON.stringify(error, null, 2);
}
return String(error);
}

function resolveSigninIdentifier(config: AmplifyConfig): SigninIdentifier {
const usernameAttrs = config.aws_cognito_username_attributes ?? [];
if (usernameAttrs.includes('PHONE_NUMBER')) return 'phone';
if (usernameAttrs.includes('EMAIL')) return 'email';
return 'username';
}

function resolveSignupAttributes(config: AmplifyConfig): SignupAttribute[] {
const cognitoAttrs = config.aws_cognito_signup_attributes ?? [];
const mapping: Record<string, SignupAttribute> = {
EMAIL: 'email',
PHONE_NUMBER: 'phone',
USERNAME: 'username',
};
const mapped = cognitoAttrs.map((attr) => mapping[attr]).filter((a): a is SignupAttribute => a !== undefined);
return mapped.length > 0 ? mapped : ['email'];
}

function buildAdminCreateUserInput(
userPoolId: string,
resolved: ResolvedAuthConfig,
credentials: { email: string; phoneNumber: string; username: string; password: string },
): { UserPoolId: string; Username: string; TemporaryPassword: string; UserAttributes: AttributeType[]; MessageAction: 'SUPPRESS' } {
const { signinIdentifier, signupAttributes } = resolved;

const identifierValueMap: Record<SigninIdentifier, string> = {
email: credentials.email,
phone: credentials.phoneNumber,
username: credentials.username,
};
const username = identifierValueMap[signinIdentifier];

const attributeMap: Record<SignupAttribute, AttributeType> = {
email: { Name: 'email', Value: credentials.email },
phone: { Name: 'phone_number', Value: credentials.phoneNumber },
username: { Name: 'username', Value: credentials.username },
};

// When phone or username is used as the Username, Cognito already
// associates it with the user, so including it again in UserAttributes
// would be redundant. Email is the exception — Cognito requires it in
// UserAttributes for verification even when it's also the Username.
const userAttributes: AttributeType[] = signupAttributes
.filter((attr) => {
if (signinIdentifier === 'phone' && attr === 'phone') return false;
if (signinIdentifier === 'username' && attr === 'username') return false;
return true;
})
.map((attr) => attributeMap[attr]);

// Mark email/phone as verified so the user can sign in immediately
if (signupAttributes.includes('email')) {
userAttributes.push({ Name: 'email_verified', Value: 'true' });
}
if (signupAttributes.includes('phone')) {
userAttributes.push({ Name: 'phone_number_verified', Value: 'true' });
}

return {
UserPoolId: userPoolId,
Username: username,
TemporaryPassword: credentials.password,
UserAttributes: userAttributes,
MessageAction: 'SUPPRESS',
};
}

/**
* Provisions a test user via AdminCreateUser and sets a permanent password.
* Uses admin APIs so it works even when self-signup is disabled on the user pool.
* Does NOT sign in — the caller should handle signIn in its own module scope
* so the Amplify auth singleton has the tokens available for API/Storage calls.
* Returns the username to use for signIn.
*/
export async function provisionTestUser(
config: AmplifyConfig,
credentials: TestCredentials,
): Promise<{ signinValue: string; testUser: TestUser }> {
const { aws_user_pools_id: userPoolId, aws_cognito_region: region } = config;

const resolved: ResolvedAuthConfig = {
signinIdentifier: resolveSigninIdentifier(config),
signupAttributes: resolveSignupAttributes(config),
};
const { signupAttributes } = resolved;

const createUserInput = buildAdminCreateUserInput(userPoolId ?? '', resolved, credentials);
const signinValue = createUserInput.Username;

console.log(`\n🔑 Creating test user: ${createUserInput.Username}`);

const cognitoClient = new CognitoIdentityProviderClient({ region });

// Step 1: AdminCreateUser
try {
await cognitoClient.send(new AdminCreateUserCommand(createUserInput));
console.log('✅ AdminCreateUser succeeded');
} catch (error) {
console.error('❌ AdminCreateUser failed:', getErrorMessage(error));
return process.exit(1);
}

// Step 2: AdminSetUserPassword (set permanent password, moves user out of FORCE_CHANGE_PASSWORD)
try {
await cognitoClient.send(
new AdminSetUserPasswordCommand({
UserPoolId: userPoolId,
Username: createUserInput.Username,
Password: credentials.password,
Permanent: true,
}),
);
console.log('✅ AdminSetUserPassword succeeded');
} catch (error) {
console.error('❌ AdminSetUserPassword failed:', getErrorMessage(error));
return process.exit(1);
}

const testUser: TestUser = {
username: signinValue,
password: credentials.password,
};

if (signupAttributes.includes('email')) {
testUser.email = credentials.email;
}
if (signupAttributes.includes('phone')) {
testUser.phoneNumber = credentials.phoneNumber;
}

return { signinValue, testUser };
}
29 changes: 29 additions & 0 deletions amplify-migration-apps/_test-common/test-apps-test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type SigninIdentifier = 'email' | 'phone' | 'username';

export type SignupAttribute = 'email' | 'phone' | 'username';

export interface AmplifyConfig {
aws_user_pools_id?: string;
aws_user_pools_web_client_id?: string;
aws_cognito_region?: string;
aws_cognito_username_attributes?: string[];
aws_cognito_signup_attributes?: string[];
}

export interface TestUser {
username: string;
password: string;
email?: string;
phoneNumber?: string;
}

export interface TestCredentials {
email: string;
phoneNumber: string;
username: string;
password: string;
}

// Re-export runner and signup for backwards compatibility
export { TestRunner, type TestFailure } from './runner';
export { provisionTestUser } from './signup';
79 changes: 79 additions & 0 deletions amplify-migration-apps/project-boards/gen1-test-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Gen1 Test Script for Project Boards App
*
* This script tests all functionality for Amplify Gen1:
* 1. Public GraphQL Queries (no auth required)
* 2. Authenticated GraphQL Mutations (requires auth)
* 3. S3 Storage Operations (requires auth)
*
* Credentials are provisioned automatically via Cognito SignUp + AdminConfirmSignUp.
*/

// Polyfill crypto for Node.js environment (required for Amplify Auth)
import { webcrypto } from 'crypto';
if (typeof globalThis.crypto === 'undefined') {
(globalThis as any).crypto = webcrypto;
}

import { Amplify } from 'aws-amplify';
import { signIn, signOut, getCurrentUser } from 'aws-amplify/auth';
import amplifyconfig from './src/amplifyconfiguration.json';
import { TestRunner } from '../_test-common/test-apps-test-utils';
import { provisionTestUser } from '../_test-common/signup';
import testCredentials from '../_test-common/test-credentials.json';
import { createTestFunctions, createTestOrchestrator } from './test-utils';

// Configure Amplify
Amplify.configure(amplifyconfig);

// ============================================================
// Main Test Execution
// ============================================================

async function runAllTests(): Promise<void> {
console.log('🚀 Starting Gen1 Test Script\n');
console.log('This script tests:');
console.log(' 1. Public GraphQL Queries');
console.log(' 2. Authenticated GraphQL Mutations');
console.log(' 3. S3 Storage Operations');

// Provision user via SDK, then sign in here so tokens stay in this module's Amplify scope
const { signinValue, testUser } = await provisionTestUser(amplifyconfig, testCredentials);

// Sign in from this module so the auth tokens are available to api/storage
try {
await signIn({ username: signinValue, password: testUser.password });
const currentUser = await getCurrentUser();
console.log(`✅ Signed in as: ${currentUser.username}`);
} catch (error: any) {
console.error('❌ SignIn failed:', error.message || error);
process.exit(1);
}

const runner = new TestRunner();
const testFunctions = createTestFunctions();
const { runPublicQueryTests, runMutationTests, runStorageTests } = createTestOrchestrator(testFunctions, runner);

// Part 1: Public queries (no auth needed)
await runPublicQueryTests();

// Part 2: Mutations (already authenticated)
await runMutationTests();

// Part 3: Storage
await runStorageTests();

// Sign out
try {
await signOut();
console.log('✅ Signed out successfully');
} catch (error: any) {
console.error('❌ Sign out error:', error.message || error);
}

// Print summary and exit with appropriate code
runner.printSummary();
}

// Run all tests
void runAllTests();
Loading
Loading