provide E2E testing infrastructure (closes #21)

This commit is contained in:
Markus Brueckner 2025-01-27 21:58:55 +01:00
parent dafa5e6b23
commit ec332284d4
15 changed files with 2296 additions and 1694 deletions

47
cypress.config.ts Normal file
View file

@ -0,0 +1,47 @@
import { defineConfig } from "cypress";
import { drizzle } from "drizzle-orm/mysql2";
import * as schema from './src/db/schema';
import dotenv from 'dotenv';
import { sql } from "drizzle-orm";
import { migrate } from "drizzle-orm/mysql2/migrator";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
on('task', {
clearDatabase: async () => {
dotenv.config({
path: '.env.e2e-test'
});
console.log(process.env.DATABASE_URL);
const db = drizzle(process.env.DATABASE_URL ?? '', {
schema,
mode: 'default',
});
await db.execute(sql`DROP TABLE IF EXISTS ${schema.sessions}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveyAnswersTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveySkillsTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveyCommentsTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveyPermissionsTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveyAccessTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.surveysTable}`);
await db.execute(sql`DROP TABLE IF EXISTS ${schema.usersTable}`);
await db.execute(sql`DROP TABLE IF EXISTS __drizzle_migrations`);
await migrate(db, {
migrationsFolder: './drizzle',
});
return null;
}
});
},
},
});

View file

@ -0,0 +1,23 @@
/// <reference types="cypress-get-by-label" />
describe('Account Registration', () => {
before(() => cy.task('clearDatabase'));
it('creates an account successfully', () => {
cy.registerUser('e2e-testuser@example.com', 'test-pwd');
cy.get('h1').should('contain', 'Registration successful');
});
it('cannot register with an existing email', () => {
cy.registerUser('e2e-testuser@example.com', 'plah');
cy.get('#error').should('contain', 'Could not register user');
});
it('cannot register with a wrong domain email', () => {
cy.registerUser('e2e-testuser@example.de', 'plah');
cy.get('#error').should('contain', 'Invalid email domain');
});
})

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -0,0 +1,46 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
Cypress.Commands.add('registerUser', (email: string, password: string) => {
cy.visit('http://localhost:5173');
cy.get('a').contains('Click here to register').click();
cy.get('label').contains(/Email address/).parent().next().type(email);
cy.getByLabel('Password').type(password);
cy.getByLabel('Password (repeat)').type(password);
cy.get('button').contains('Register').click();
});
Cypress.Commands.add('loginUser', (email: string, password: string) => {
cy.visit('http://localhost:5173/login');
cy.getByLabel('Email address').type(email);
cy.getByLabel('Password').type(password);
cy.get('button').contains('Log In').click();
});

18
cypress/support/e2e.ts Normal file
View file

@ -0,0 +1,18 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import 'cypress-get-by-label/commands'

11
cypress/support/index.ts Normal file
View file

@ -0,0 +1,11 @@
declare global {
namespace Cypress {
interface Chainable {
registerUser(email: string, password: string): Chainable<void>,
loginUser(email: string, password: string): Chainable<void>
}
}
}
export { }

View file

@ -12,6 +12,5 @@ ALTER TABLE `survey_permissions_table` ADD CONSTRAINT `survey_permissions_table_
CREATE INDEX `user_index` ON `survey_permissions_table` (`user`);--> statement-breakpoint CREATE INDEX `user_index` ON `survey_permissions_table` (`user`);--> statement-breakpoint
CREATE INDEX `survey_index` ON `survey_permissions_table` (`surveyId`); CREATE INDEX `survey_index` ON `survey_permissions_table` (`surveyId`);
--> statement-breakpoint
-- Create the owner permission for each existing survey INSERT INTO `survey_permissions_table` (`surveyId`, `user`, `access`) SELECT `id`,`owner`,255 FROM `surveys_table`;
INSERT INTO survey_permissions_table (surveyId, user, access) SELECT id,owner,255 FROM surveys_table;

View file

@ -9,13 +9,6 @@
"tag": "0000_harsh_warlock", "tag": "0000_harsh_warlock",
"breakpoints": true "breakpoints": true
}, },
{
"idx": 1,
"version": "5",
"when": 1734971924726,
"tag": "0001_lucky_proteus",
"breakpoints": true
},
{ {
"idx": 2, "idx": 2,
"version": "5", "version": "5",

3770
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"e2e": "vite dev --mode=e2e-test",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@ -26,6 +27,9 @@
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cypress": "^14.0.1",
"cypress-get-by-label": "^2.5.0",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.28.1", "drizzle-kit": "^0.28.1",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",

View file

@ -10,7 +10,9 @@ import { generateRandomToken } from "$lib/helpers/shared/randomToken";
const log = debug('db:users'); const log = debug('db:users');
export async function createNewUser(email: Email, password: Password): Promise<{ verificationCode: VerificationCode | undefined }> { type UserError = 'USER_EXISTS' | 'UNKNOWN_ERROR';
export async function createNewUser(email: Email, password: Password): Promise<Result<{ verificationCode: VerificationCode | undefined }, UserError>> {
const hashedPassword = await hash(password); const hashedPassword = await hash(password);
if (config.emailVerificationDisabled) { if (config.emailVerificationDisabled) {
@ -19,16 +21,25 @@ export async function createNewUser(email: Email, password: Password): Promise<{
const verificationCode = config.emailVerificationDisabled ? undefined : generateRandomToken() as VerificationCode; const verificationCode = config.emailVerificationDisabled ? undefined : generateRandomToken() as VerificationCode;
await db.insert(usersTable).values({ try {
email, await db.insert(usersTable).values({
password_hash: hashedPassword, email,
verification_code: verificationCode, password_hash: hashedPassword,
verifcationCodeExpires: verificationCode ? new Date(Date.now() + 1000 * 60 * 60 * 24 * 3) : undefined verification_code: verificationCode,
}); verifcationCodeExpires: verificationCode ? new Date(Date.now() + 1000 * 60 * 60 * 24 * 3) : undefined
});
return {
verificationCode
} }
catch (e) {
log('Failed to create new user: %s', e);
if ((e as any).code === 'ER_DUP_ENTRY') {
return err('USER_EXISTS');
}
return err('UNKNOWN_ERROR');
}
return ok({
verificationCode
})
} }
export async function loadUser(id: number) { export async function loadUser(id: number) {

View file

@ -62,7 +62,7 @@ export type SurveyParticipant = {
export type SurveySkill = { export type SurveySkill = {
id: SkillId; id: SkillId;
title: string; title: string;
description: string | undefined | null; description: string | null;
} }
/// Complete data for a survey including skills & participants /// Complete data for a survey including skills & participants

View file

@ -7,5 +7,5 @@
<Navbar title="Error" /> <Navbar title="Error" />
<div class="flex h-screen items-center justify-center"> <div class="flex h-screen items-center justify-center">
<p class="mw-10"><WarningIcon /> {page.error?.message}</p> <p id="error" class="mw-10"><WarningIcon /> {page.error?.message}</p>
</div> </div>

View file

@ -9,7 +9,7 @@
<div class="flex h-screen items-center justify-center"> <div class="flex h-screen items-center justify-center">
<form class="grid grid-cols-2 gap-1" method="POST"> <form class="grid grid-cols-2 gap-1" method="POST">
<label for="name" class="justify-self-end">Email address</label> <label for="email" class="justify-self-end">Email address</label>
<input type="email" name="email" id="email" class="justify-self-start" required /> <input type="email" name="email" id="email" class="justify-self-start" required />
<label for="password" class="justify-self-end">Password</label> <label for="password" class="justify-self-end">Password</label>
<input type="password" name="password" id="password" class="justify-self-start" required /> <input type="password" name="password" id="password" class="justify-self-start" required />

View file

@ -4,6 +4,7 @@ import { sendVerificationEmail } from '$lib/emails/verification';
import { createNewUser } from '../../db/users'; import { createNewUser } from '../../db/users';
import type { Email, Password } from '$lib/types'; import type { Email, Password } from '$lib/types';
import { config } from '$lib/configuration'; import { config } from '$lib/configuration';
import { isErr, isOk } from '$lib/result';
export const load: PageServerLoad = () => { export const load: PageServerLoad = () => {
return { return {
@ -31,10 +32,18 @@ export const actions = {
const result = await createNewUser(email, password); const result = await createNewUser(email, password);
if (result.verificationCode) { if (isErr(result)) {
sendVerificationEmail(email, result.verificationCode, getBaseUrl(event.url)); if (result.error === 'USER_EXISTS') {
error(400, 'Could not register user');
}
else {
error(500, 'Unknown error');
}
}
else if (isOk(result) && result.value.verificationCode) {
sendVerificationEmail(email, result.value.verificationCode, getBaseUrl(event.url));
} }
redirect(303, 'register/success'); redirect(303, 'register/success');
} }
} satisfies Actions; } satisfies Actions;