Skip to content

Commit ffe02ff

Browse files
authored
e2e testing - playwright (#22)
* **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
1 parent 88dcb4f commit ffe02ff

24 files changed

+1229
-36
lines changed

.env.example

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
DATABASE_URL=
22
AUTH_SECRET=
33

4-
GITHUB_CLIENT_SECRET=
5-
GITHUB_CLIENT_ID=
4+
AUTH_GITHUB_CLIENT_SECRET=
5+
AUTH_GITHUB_CLIENT_ID=
66

7-
GOOGLE_CLIENT_SECRET=
8-
GOOGLE_CLIENT_ID=
7+
AUTH_GOOGLE_CLIENT_SECRET=
8+
AUTH_GOOGLE_CLIENT_ID=
99

1010
RESEND_API_KEY=
1111

1212
NEXT_PUBLIC_APP_URL=
1313

14-
KV_REST_API_READ_ONLY_TOKEN=""
15-
KV_REST_API_TOKEN=""
16-
KV_REST_API_URL=""
17-
KV_URL=""
14+
MAILSAC_API_KEY=

.eslintrc.json

+9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
}
2828
]
2929
},
30+
"overrides": [
31+
{
32+
"files": ["e2e-tests/**/*", "allure-report/**/*"],
33+
"rules": {
34+
"no-console": "off",
35+
"consistent-return": "off"
36+
}
37+
}
38+
],
3039
"env": {
3140
"browser": true,
3241
"es6": true,

.github/workflows/playwright.yml

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: E2E Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
jobs:
15+
test:
16+
name: Run E2E Tests
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
cache: 'npm'
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Install Playwright browsers
32+
run: npx playwright install --with-deps
33+
34+
- name: Install Allure Commandline
35+
run: npm install -g allure-commandline
36+
37+
- name: Run Playwright tests
38+
env:
39+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
40+
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
41+
AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET }}
42+
AUTH_GITHUB_CLIENT_ID: ${{ secrets.AUTH_GITHUB_CLIENT_ID }}
43+
AUTH_GOOGLE_CLIENT_SECRET: ${{ secrets.AUTH_GOOGLE_CLIENT_SECRET }}
44+
AUTH_GOOGLE_CLIENT_ID: ${{ secrets.AUTH_GOOGLE_CLIENT_ID }}
45+
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
46+
MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }}
47+
NEXT_PUBLIC_APP_URL: http://localhost:3000
48+
KV_REST_API_READ_ONLY_TOKEN: ${{ secrets.KV_REST_API_READ_ONLY_TOKEN }}
49+
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
50+
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
51+
KV_URL: ${{ secrets.KV_URL }}
52+
run: npx playwright test
53+
continue-on-error: true
54+
55+
- name: Generate Allure Report
56+
if: always()
57+
run: |
58+
allure generate allure-results -o allure-report --clean
59+
60+
# Optional: Upload allure-results as artifact for debugging
61+
- name: Upload Allure Results
62+
if: always()
63+
uses: actions/upload-artifact@v4
64+
with:
65+
name: allure-results
66+
path: allure-results/
67+
retention-days: 30
68+
69+
# Setup Pages
70+
- name: Setup Pages
71+
if: always()
72+
uses: actions/configure-pages@v4
73+
74+
# Upload to GitHub Pages
75+
- name: Upload Pages artifact
76+
if: always()
77+
uses: actions/upload-pages-artifact@v3
78+
with:
79+
path: allure-report
80+
81+
# Deploy job
82+
deploy:
83+
needs: test # Wait for test job to complete
84+
runs-on: ubuntu-latest
85+
if: github.ref == 'refs/heads/main' # Only deploy on main branch
86+
87+
environment:
88+
name: github-pages
89+
url: ${{ steps.deployment.outputs.page_url }}
90+
91+
steps:
92+
- name: Deploy to GitHub Pages
93+
id: deployment
94+
uses: actions/deploy-pages@v4

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,12 @@ yarn-error.log*
3636
*.tsbuildinfo
3737
next-env.d.ts
3838
/.idea/
39+
node_modules/
40+
/test-results/
41+
/playwright-report/
42+
/blob-report/
43+
/playwright/.cache/
44+
/scripts/
45+
/tests-examples/
46+
/allure-results/
47+
/allure-report/

actions/login.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const login = async (values: zod.infer<typeof LoginSchema>, callbackUrl?:
4747
}
4848
}
4949

50-
const verificationToken = await generateVerificationToken(email);
50+
const verificationToken = await generateVerificationToken(email, existingUser.id);
5151

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

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

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

actions/register.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export const register = async (values: zod.infer<typeof RegisterSchema>) => {
2323
const hashedIp = await hashIp(userIp);
2424

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

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

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

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

44-
await db.user.create({
45+
const createdUser = await db.user.create({
4546
data: {
4647
name,
4748
email,
4849
password: hashedPassword,
4950
ip: hashedIp,
5051
},
52+
select: {
53+
id: true,
54+
email: true,
55+
},
5156
});
5257

53-
const verificationToken = await generateVerificationToken(email);
58+
if (!createdUser?.id || !createdUser?.email) return { error: 'Something went wrong!' };
59+
60+
const verificationToken = await generateVerificationToken(createdUser.email, createdUser.id);
5461
await sendVerificationEmail(verificationToken.email, verificationToken.token);
5562

5663
return { success: 'Confirmation email sent!' };

actions/reset-password.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const resetPassword = async (values: zod.infer<typeof ResetPasswordSchema
4343
}
4444
}
4545

46-
const passwordResetToken = await generatePasswordResetToken(email);
46+
const passwordResetToken = await generatePasswordResetToken(email, existingUser.id);
4747

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

actions/settings.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const settings = async (values: zod.infer<typeof SettingsSchema>) => {
6363
}
6464
}
6565

66-
const verificationToken = await generateVerificationToken(values.email, dbUser.email);
66+
const verificationToken = await generateVerificationToken(values.email, dbUser.id, dbUser.email);
6767
await sendVerificationEmail(verificationToken.email, verificationToken.token);
6868

6969
return { success: 'Verification email sent!' };

auth.config.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import { getUserByEmail } from '@/data/user';
1010
export default {
1111
providers: [
1212
Google({
13-
clientId: process.env.GOOGLE_CLIENT_ID,
14-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
13+
clientId: process.env.AUTH_GOOGLE_CLIENT_ID,
14+
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,
1515
}),
1616
Github({
17-
clientId: process.env.GITHUB_CLIENT_ID,
18-
clientSecret: process.env.GITHUB_CLIENT_SECRET,
17+
clientId: process.env.AUTH_GITHUB_CLIENT_ID,
18+
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET,
1919
}),
2020
Credentials({
2121
async authorize(credentials) {

e2e-tests/config/test-config.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const TEST_CONFIG = {
2+
MAILSAC_API_KEY: process.env.MAILSAC_API_KEY!,
3+
DATABASE_URL: process.env.DATABASE_URL!,
4+
TEST_EMAIL: '[email protected]',
5+
TEST_PASSWORD: '1234567',
6+
TEST_NAME: 'faketesting',
7+
};
8+
9+
if (!TEST_CONFIG.MAILSAC_API_KEY) {
10+
throw new Error('MAILSAC_API_KEY is required');
11+
}
12+
13+
if (!TEST_CONFIG.DATABASE_URL) {
14+
throw new Error('DATABASE_URL is required');
15+
}

e2e-tests/credentials-2FA.spec.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { expect, Page, test } from '@playwright/test';
2+
3+
import { TEST_CONFIG } from '@/e2e-tests/config/test-config';
4+
import { cleanupTestUserFromDB, createCredentialsTestUser } from '@/e2e-tests/helpers/helper-functions';
5+
import { cleanupMailsacInbox, getEmailContent } from '@/e2e-tests/helpers/mailsac/mailsac';
6+
import { fillLoginForm } from '@/e2e-tests/helpers/tests';
7+
8+
test.describe('2FA Authentication Flow', () => {
9+
const { MAILSAC_API_KEY, TEST_EMAIL, TEST_PASSWORD, TEST_NAME } = TEST_CONFIG;
10+
11+
async function cleanupState() {
12+
await cleanupTestUserFromDB(TEST_EMAIL);
13+
const mailsacResponseStatus = await cleanupMailsacInbox(TEST_EMAIL, MAILSAC_API_KEY);
14+
expect(mailsacResponseStatus).toBe(204);
15+
}
16+
17+
async function createTwoFactorUser() {
18+
await createCredentialsTestUser(TEST_NAME, TEST_EMAIL, TEST_PASSWORD, {
19+
isTwoFactorEnabled: true,
20+
emailVerified: true,
21+
});
22+
}
23+
24+
async function initiateLogin(page: Page) {
25+
await page.goto('/login');
26+
await fillLoginForm(page, {
27+
email: TEST_EMAIL,
28+
password: TEST_PASSWORD,
29+
});
30+
await page.locator('button[type="submit"]').click();
31+
}
32+
33+
async function getTwoFactorCode(): Promise<string> {
34+
const emailContent = await getEmailContent(TEST_EMAIL, MAILSAC_API_KEY, '2FA Code', {
35+
retries: 5,
36+
delay: 2000,
37+
exactMatch: false,
38+
});
39+
40+
const twoFactorCode = emailContent.match(/(\d{6})/)?.[1];
41+
42+
if (!twoFactorCode) {
43+
throw new Error('Could not extract 2FA code from email');
44+
}
45+
46+
return twoFactorCode;
47+
}
48+
49+
async function submitTwoFactorCode(page: Page, code: string) {
50+
await page.locator('input[name="code"]').fill(code);
51+
await page.locator('button[type="submit"]').click();
52+
}
53+
54+
test('should successfully authenticate user with valid 2FA code', async ({ page }) => {
55+
await test.step('Setup test environment', async () => {
56+
await cleanupState();
57+
await createTwoFactorUser();
58+
});
59+
60+
await test.step('Initiate login process', async () => {
61+
await initiateLogin(page);
62+
});
63+
64+
await test.step('Process 2FA verification', async () => {
65+
const twoFactorCode = await getTwoFactorCode();
66+
await submitTwoFactorCode(page, twoFactorCode);
67+
await page.waitForURL('**/settings');
68+
await expect(page).toHaveURL('/settings');
69+
});
70+
});
71+
72+
test('should reject login attempt with invalid 2FA code', async ({ page }) => {
73+
await test.step('Setup test environment', async () => {
74+
await cleanupState();
75+
await createTwoFactorUser();
76+
});
77+
78+
await test.step('Attempt login with invalid 2FA code', async () => {
79+
await initiateLogin(page);
80+
await submitTwoFactorCode(page, '000000');
81+
await expect(page.getByText('Invalid code')).toBeVisible();
82+
});
83+
});
84+
});

0 commit comments

Comments
 (0)