Skip to content

Commit

Permalink
e2e testing - playwright (#22)
Browse files Browse the repository at this point in the history
* **Playwright testing**

- Changed env.example
- update eslint rules
- gitignore updated
- auth.config.ts with updated env var
- removed kv rate limit

- Changed prisma relations - User with passwordResetTokens, verificationTokens, twoFactorTokens
- playwright tests
- github actions playwright.yml
- better approach for handling the registration restriction
- with allure reports
  • Loading branch information
zenWai authored Nov 4, 2024
1 parent 88dcb4f commit ffe02ff
Show file tree
Hide file tree
Showing 24 changed files with 1,229 additions and 36 deletions.
13 changes: 5 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
DATABASE_URL=
AUTH_SECRET=

GITHUB_CLIENT_SECRET=
GITHUB_CLIENT_ID=
AUTH_GITHUB_CLIENT_SECRET=
AUTH_GITHUB_CLIENT_ID=

GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
AUTH_GOOGLE_CLIENT_SECRET=
AUTH_GOOGLE_CLIENT_ID=

RESEND_API_KEY=

NEXT_PUBLIC_APP_URL=

KV_REST_API_READ_ONLY_TOKEN=""
KV_REST_API_TOKEN=""
KV_REST_API_URL=""
KV_URL=""
MAILSAC_API_KEY=
9 changes: 9 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
}
]
},
"overrides": [
{
"files": ["e2e-tests/**/*", "allure-report/**/*"],
"rules": {
"no-console": "off",
"consistent-return": "off"
}
}
],
"env": {
"browser": true,
"es6": true,
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
pages: write
id-token: write

jobs:
test:
name: Run E2E Tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Install Allure Commandline
run: npm install -g allure-commandline

- name: Run Playwright tests
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET }}
AUTH_GITHUB_CLIENT_ID: ${{ secrets.AUTH_GITHUB_CLIENT_ID }}
AUTH_GOOGLE_CLIENT_SECRET: ${{ secrets.AUTH_GOOGLE_CLIENT_SECRET }}
AUTH_GOOGLE_CLIENT_ID: ${{ secrets.AUTH_GOOGLE_CLIENT_ID }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }}
NEXT_PUBLIC_APP_URL: http://localhost:3000
KV_REST_API_READ_ONLY_TOKEN: ${{ secrets.KV_REST_API_READ_ONLY_TOKEN }}
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
KV_URL: ${{ secrets.KV_URL }}
run: npx playwright test
continue-on-error: true

- name: Generate Allure Report
if: always()
run: |
allure generate allure-results -o allure-report --clean
# Optional: Upload allure-results as artifact for debugging
- name: Upload Allure Results
if: always()
uses: actions/upload-artifact@v4
with:
name: allure-results
path: allure-results/
retention-days: 30

# Setup Pages
- name: Setup Pages
if: always()
uses: actions/configure-pages@v4

# Upload to GitHub Pages
- name: Upload Pages artifact
if: always()
uses: actions/upload-pages-artifact@v3
with:
path: allure-report

# Deploy job
deploy:
needs: test # Wait for test job to complete
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only deploy on main branch

environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
/.idea/
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/scripts/
/tests-examples/
/allure-results/
/allure-report/
4 changes: 2 additions & 2 deletions actions/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const login = async (values: zod.infer<typeof LoginSchema>, callbackUrl?:
}
}

const verificationToken = await generateVerificationToken(email);
const verificationToken = await generateVerificationToken(email, existingUser.id);

await sendVerificationEmail(verificationToken.email, verificationToken.token);

Expand Down Expand Up @@ -98,7 +98,7 @@ export const login = async (values: zod.infer<typeof LoginSchema>, callbackUrl?:
return { twoFactor: true };
}
}
const twoFactorToken = await generateTwoFactorToken(existingUser.email);
const twoFactorToken = await generateTwoFactorToken(existingUser.email, existingUser.id);

await sendTwoFactorTokenEmail(existingUser.email, twoFactorToken.token);

Expand Down
15 changes: 11 additions & 4 deletions actions/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const register = async (values: zod.infer<typeof RegisterSchema>) => {
const hashedIp = await hashIp(userIp);

/* If we can not determine the IP of the user, fails to register */
if ((process.env.NODE_ENV === 'production' && userIp === '127.0.0.1') || !userIp || hashedIp === 'unknown') {
return { error: 'Sorry! Something went wrong. Could not identify you as user' };
if (!userIp || hashedIp === 'unknown') {
return { error: 'Sorry! Something went wrong. Could not identify you as a human' };
}

const existingAccounts = await db.user.count({
Expand All @@ -34,23 +34,30 @@ export const register = async (values: zod.infer<typeof RegisterSchema>) => {
return { error: 'You are not allowed to register more accounts on this app preview' };
}

//TODO: Single Query Approach using Prisma Error code or upsert approach
const existingUser = await getUserByEmail(email);
if (existingUser) {
return { error: 'Email already registered!' };
}

const hashedPassword = await bcrypt.hash(password, 10);

await db.user.create({
const createdUser = await db.user.create({
data: {
name,
email,
password: hashedPassword,
ip: hashedIp,
},
select: {
id: true,
email: true,
},
});

const verificationToken = await generateVerificationToken(email);
if (!createdUser?.id || !createdUser?.email) return { error: 'Something went wrong!' };

const verificationToken = await generateVerificationToken(createdUser.email, createdUser.id);
await sendVerificationEmail(verificationToken.email, verificationToken.token);

return { success: 'Confirmation email sent!' };
Expand Down
2 changes: 1 addition & 1 deletion actions/reset-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const resetPassword = async (values: zod.infer<typeof ResetPasswordSchema
}
}

const passwordResetToken = await generatePasswordResetToken(email);
const passwordResetToken = await generatePasswordResetToken(email, existingUser.id);

await sendPasswordResetEmail(passwordResetToken.email, passwordResetToken.token);

Expand Down
2 changes: 1 addition & 1 deletion actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const settings = async (values: zod.infer<typeof SettingsSchema>) => {
}
}

const verificationToken = await generateVerificationToken(values.email, dbUser.email);
const verificationToken = await generateVerificationToken(values.email, dbUser.id, dbUser.email);
await sendVerificationEmail(verificationToken.email, verificationToken.token);

return { success: 'Verification email sent!' };
Expand Down
8 changes: 4 additions & 4 deletions auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { getUserByEmail } from '@/data/user';
export default {
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
clientId: process.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,
}),
Github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
clientId: process.env.AUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET,
}),
Credentials({
async authorize(credentials) {
Expand Down
15 changes: 15 additions & 0 deletions e2e-tests/config/test-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const TEST_CONFIG = {
MAILSAC_API_KEY: process.env.MAILSAC_API_KEY!,
DATABASE_URL: process.env.DATABASE_URL!,
TEST_EMAIL: '[email protected]',
TEST_PASSWORD: '1234567',
TEST_NAME: 'faketesting',
};

if (!TEST_CONFIG.MAILSAC_API_KEY) {
throw new Error('MAILSAC_API_KEY is required');
}

if (!TEST_CONFIG.DATABASE_URL) {
throw new Error('DATABASE_URL is required');
}
84 changes: 84 additions & 0 deletions e2e-tests/credentials-2FA.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect, Page, test } from '@playwright/test';

import { TEST_CONFIG } from '@/e2e-tests/config/test-config';
import { cleanupTestUserFromDB, createCredentialsTestUser } from '@/e2e-tests/helpers/helper-functions';
import { cleanupMailsacInbox, getEmailContent } from '@/e2e-tests/helpers/mailsac/mailsac';
import { fillLoginForm } from '@/e2e-tests/helpers/tests';

test.describe('2FA Authentication Flow', () => {
const { MAILSAC_API_KEY, TEST_EMAIL, TEST_PASSWORD, TEST_NAME } = TEST_CONFIG;

async function cleanupState() {
await cleanupTestUserFromDB(TEST_EMAIL);
const mailsacResponseStatus = await cleanupMailsacInbox(TEST_EMAIL, MAILSAC_API_KEY);
expect(mailsacResponseStatus).toBe(204);
}

async function createTwoFactorUser() {
await createCredentialsTestUser(TEST_NAME, TEST_EMAIL, TEST_PASSWORD, {
isTwoFactorEnabled: true,
emailVerified: true,
});
}

async function initiateLogin(page: Page) {
await page.goto('/login');
await fillLoginForm(page, {
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
await page.locator('button[type="submit"]').click();
}

async function getTwoFactorCode(): Promise<string> {
const emailContent = await getEmailContent(TEST_EMAIL, MAILSAC_API_KEY, '2FA Code', {
retries: 5,
delay: 2000,
exactMatch: false,
});

const twoFactorCode = emailContent.match(/(\d{6})/)?.[1];

if (!twoFactorCode) {
throw new Error('Could not extract 2FA code from email');
}

return twoFactorCode;
}

async function submitTwoFactorCode(page: Page, code: string) {
await page.locator('input[name="code"]').fill(code);
await page.locator('button[type="submit"]').click();
}

test('should successfully authenticate user with valid 2FA code', async ({ page }) => {
await test.step('Setup test environment', async () => {
await cleanupState();
await createTwoFactorUser();
});

await test.step('Initiate login process', async () => {
await initiateLogin(page);
});

await test.step('Process 2FA verification', async () => {
const twoFactorCode = await getTwoFactorCode();
await submitTwoFactorCode(page, twoFactorCode);
await page.waitForURL('**/settings');
await expect(page).toHaveURL('/settings');
});
});

test('should reject login attempt with invalid 2FA code', async ({ page }) => {
await test.step('Setup test environment', async () => {
await cleanupState();
await createTwoFactorUser();
});

await test.step('Attempt login with invalid 2FA code', async () => {
await initiateLogin(page);
await submitTwoFactorCode(page, '000000');
await expect(page.getByText('Invalid code')).toBeVisible();
});
});
});
Loading

0 comments on commit ffe02ff

Please sign in to comment.