provide E2E testing infrastructure (closes #21)
This commit is contained in:
parent
dafa5e6b23
commit
ec332284d4
15 changed files with 2296 additions and 1694 deletions
47
cypress.config.ts
Normal file
47
cypress.config.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
23
cypress/e2e/register.cy.ts
Normal file
23
cypress/e2e/register.cy.ts
Normal 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');
|
||||||
|
});
|
||||||
|
})
|
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal 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"
|
||||||
|
}
|
46
cypress/support/commands.ts
Normal file
46
cypress/support/commands.ts
Normal 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
18
cypress/support/e2e.ts
Normal 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
11
cypress/support/index.ts
Normal 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 { }
|
|
@ -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;
|
|
||||||
|
|
|
@ -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
3770
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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;
|
Loading…
Add table
Reference in a new issue