Skip to content

Commit

Permalink
test(e2e): Improve e2e testing configuration and add more auth.spec.t…
Browse files Browse the repository at this point in the history
…s and health.spec.ts
  • Loading branch information
alepefe committed Nov 13, 2024
1 parent 3caf3f8 commit 9f8ac6a
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 19 deletions.
2 changes: 2 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore temporary pid files
*.pid
117 changes: 117 additions & 0 deletions e2e/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { spawn } from "child_process";
import { Process } from "e2e/process";
import * as http from 'http';

export const API_PID_FILE = 'api.pid';
export const CLIENT_PID_FILE = 'client.pid';
export const DEFAULT_START_TIMEOUT = 120_000;
export const API_URL = 'http://localhost:4000';
export const CLIENT_URL = 'http://localhost:3000';

const checkIfServerIsUp = (url: string) => {
return new Promise<void>((resolve, reject) => {
const check = () => {
http.get(url, (res) => {
if (res.statusCode === 200) {
resolve();
} else {
setTimeout(check, 300);
}
}).on('error', () => {
setTimeout(check, 300);
});
};
check();
});
};

const checkIfServerIsDown = (url: string) => {
return new Promise<void>((resolve) => {
const check = () => {
http.get(url, () => {
console.log(`Server is still up. Waiting for it to shutdown... ${url}`);
setTimeout(check, 300);
}).on('error', () => {
resolve();
});
};
check();
});
};

const startClientServer = async () => {
const currentPid = Process.getPid(CLIENT_PID_FILE);
if(currentPid !== undefined) return;

const { pid } = spawn('pnpm', ['--filter', 'client', 'run', 'start'], {
env: { ...process.env, NODE_ENV: 'test' },
stdio: 'ignore',
detached: true
});
Process.setPid(CLIENT_PID_FILE, pid);
await checkIfServerIsUp(CLIENT_URL);
}

const stopClientServer = async () => {
const currentPid = Process.getPid(CLIENT_PID_FILE);
if(currentPid === undefined) return;

Process.kill(-currentPid);
Process.setPid(CLIENT_PID_FILE, undefined);
}

async function startAPIServer() {
const currentPid = Process.getPid(API_PID_FILE);
if(currentPid !== undefined) return;

const { pid } = spawn('pnpm', ['--filter', 'api', 'run', 'start:prod'], {
env: { ...process.env, NODE_ENV: 'test' },
stdio: 'ignore',
detached: true
});
Process.setPid(API_PID_FILE, pid);
await checkIfServerIsUp(API_URL);
}

async function stopAPIServer() {
const currentPid = Process.getPid(API_PID_FILE);
if(currentPid === undefined) return;

Process.kill(-currentPid);
Process.setPid(API_PID_FILE, undefined);
await checkIfServerIsDown(API_URL);
}

export const Application = {
CLIENT_URL,
API_URL,
startClientServer,
stopClientServer,
startAPIServer,
stopAPIServer,
globalSetup: async () => {
Process.setPid(API_PID_FILE, undefined);
Process.setPid(CLIENT_PID_FILE, undefined);

await Promise.race([
Promise.all([
Application.startAPIServer(),
Application.startClientServer()
]),
new Promise((_, reject) => setTimeout(() => {
reject(new Error('globalSetup timed out'))
}, DEFAULT_START_TIMEOUT)),
])
},
globalTeardown: async () => {
await Promise.race([
Promise.all([
Application.stopAPIServer(),
Application.stopClientServer()
]),
new Promise((_, reject) => setTimeout(() => {
reject(new Error('globalTeardown timed out'))
}, DEFAULT_START_TIMEOUT)),
])
}
} as const;
3 changes: 3 additions & 0 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Application } from "e2e/application";

export default Application.globalSetup;
3 changes: 3 additions & 0 deletions e2e/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Application } from "e2e/application";

export default Application.globalTeardown;
24 changes: 5 additions & 19 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import { defineConfig, devices } from '@playwright/test';

const API_URL = 'http://localhost:4000';
const APP_URL = 'http://localhost:3000';
import { ChildProcess, spawn } from 'child_process';
import { Application } from 'e2e/application';

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: [
{
command: 'pnpm --filter api run build && NODE_ENV=test pnpm --filter api run start:prod',
url: API_URL,
reuseExistingServer: !process.env.CI,
timeout: 120_000
},
{
command: 'NODE_ENV=test pnpm --filter client run build && NODE_ENV=test pnpm --filter client run start',
url: APP_URL,
reuseExistingServer: !process.env.CI,
timeout: 120_000
},
],
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: false,
Expand All @@ -36,7 +22,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: APP_URL,
baseURL: Application.CLIENT_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
Expand Down
28 changes: 28 additions & 0 deletions e2e/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as fs from 'fs';

export const Process = {
setPid: (path: string, pid?: number) => {
if(pid === undefined) {
if (fs.existsSync(path) === true) {
fs.unlinkSync(path);
}
return;
}
fs.writeFileSync(path, pid.toString(), 'utf8');
},
getPid: (path: string) => {
if (fs.existsSync(path) === false) {
return undefined;
}
return parseInt(fs.readFileSync(path, 'utf8'));
},
kill: (pid: number, signal: NodeJS.Signals = 'SIGKILL') => {
try {
process.kill(pid, signal);
} catch (error) {
if (error.code !== 'ESRCH') {
throw error;
}
}
}
} as const;
11 changes: 11 additions & 0 deletions e2e/tests/auth/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,15 @@ test.describe('Authentication', () => {
await page.waitForURL('/profile');
await expect(page.locator('input[type="email"]')).toHaveValue(user.email);
});

test('a user tries to sign in with invalid credentials', async ({ page }) => {
await page.goto('/auth/signin');

await page.getByPlaceholder('Enter your email').fill('[email protected]');
await page.getByPlaceholder('*******').fill('12345678');
await page.getByRole('button', { name: 'Log in' }).click();

const errorMessage = page.getByText('Invalid credentials');
await expect(errorMessage).toBeVisible();
});
});
49 changes: 49 additions & 0 deletions e2e/tests/health/health.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test';
import { E2eTestManager } from '@shared/lib/e2e-test-manager';
import { Application } from 'e2e/application';

test.describe('Health', () => {
let testManager: E2eTestManager;

test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
testManager = await E2eTestManager.load(page);
});

test.afterAll(async () => {
await testManager.close();
});

test.describe('when the backend is up', () => {
test('an anonymous user tries to check the application\'s health (both frontend and backend)', async ({ page }) => {
// Given that backend application was succesfully started
// App already started in globalSetup. Just being explicit here.
await Application.startAPIServer();

// When
const response = await page.goto('/health');

// Then
expect(response?.status()).toBe(200);
await expect(page.getByText('OK')).toBeVisible();
});
})

test.describe('when the backend is down', () => {
test.afterAll(async () => {
await Application.startAPIServer();
})

test('an anonymous user tries to check the application\'s health (both frontend and backend)', async ({ page }) => {
// Given that the backend app is unavailable
await Application.stopAPIServer();

// When
const response = await page.goto('/health');

// Then
expect(response?.status()).toBe(503);
await expect(page.getByText('KO')).toBeVisible();
});
})
});

0 comments on commit 9f8ac6a

Please sign in to comment.