Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Registration tests #597

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
playwright-test-runner:
strategy:
matrix:
test_case: ['@login']
test_case: ['@login', '@registration']
timeout-minutes: 20
runs-on:
- ubuntu-22.04
Expand Down
107 changes: 107 additions & 0 deletions e2e/data/reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Client } from "pg";

async function getDatabaseClient() {
testEnvChecks();
const env = getEnv();
const client = new Client(env.superuser);
await client.connect();
return client;
}

function getEnv() {
return {
host: process.env.TEST_POSTGRES_HOST || "localhost",
port: process.env.TEST_POSTGRES_HOST
? Number(process.env.TEST_POSTGRES_HOST)
: 5432,
user: process.env.TEST_POSTGRES_USER || "test_linkwarden_user",
testDatabase: process.env.TEST_POSTGRES_DATABASE || "test_linkwarden_db",
testDatabaseTemplate:
process.env.TEST_POSTGRES_DATABASE_TEMPLATE || "test_linkwarden_db_template",
productionDatabase: process.env.PRODUCTION_POSTGRES_DATABASE || "linkwarden",
superuser: {
host: process.env.PGHOST || "localhost",
port: process.env.PGPORT ? Number(process.env.PGPORT) : 5432,
user: process.env.PGUSER || "postgres",
password: process.env.PGPASSWORD || "password",
database: process.env.PGDATABASE || "postgres",
},
};
}

function testEnvChecks() {
const env = getEnv();
if (!env.testDatabase.startsWith("test_")) {
const msg =
"Please make sure your test environment database name starts with test_";
console.error(msg);
throw new Error(msg);
}
if (env.testDatabase === env.productionDatabase) {
const msg =
"Please make sure your test environment database and production environment database names are not equal";
console.error(msg);
throw new Error(msg);
}
}

async function createTemplateDatabase(client: Client) {
const { user, testDatabase, testDatabaseTemplate } = getEnv();
try {
// close current connections
await client.query(`
ALTER DATABASE ${testDatabase} WITH ALLOW_CONNECTIONS false;
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname='${testDatabase}';
`);
await client.query(`
CREATE DATABASE ${testDatabaseTemplate} WITH
OWNER=${user}
TEMPLATE=${testDatabase}
IS_TEMPLATE=true;
`);
} catch (e: any) {
if (e.code === "42P04") {
return;
}
throw e;
}
}

async function createTestDatabase(client: Client) {
const { user, testDatabase, testDatabaseTemplate } = getEnv();
const deleteDatabase = `${testDatabase}_del`;
// drop connections and alter database name
await client.query(`
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname='${testDatabase}';
ALTER DATABASE ${testDatabase}
RENAME TO ${deleteDatabase};
`);
await client.query(`
CREATE DATABASE ${testDatabase}
WITH OWNER ${user}
TEMPLATE=${testDatabaseTemplate};
`);
await client.query(`DROP DATABASE ${deleteDatabase}`);
}

export async function resetDatabase() {
const client = await getDatabaseClient();
await createTemplateDatabase(client);
await createTestDatabase(client);
await client.end();
}

export async function dropTemplate() {
const client = await getDatabaseClient();
const env = getEnv();
try {
await client.query(
`ALTER DATABASE ${env.testDatabaseTemplate} is_template false`
);
await client.query(`DROP DATABASE ${env.testDatabaseTemplate}`);
} catch (e) {}
await client.end();
}
6 changes: 3 additions & 3 deletions e2e/data/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ axios.defaults.baseURL = "http://localhost:3000"
export async function seedUser (username?: string, password?: string, name?: string) {
try {
return await axios.post("/api/v1/users", {
username: username || "test",
password: password || "password",
username: username || process.env["TEST_USERNAME"] || "test",
password: password || process.env["TEST_PASSWORD"] || "password",
name: name || "Test User",
})
} catch (e: any) {
if (e instanceof AxiosError) {
if (e.response?.status === 400) {
if ([400, 500].includes(e.response?.status)) {
return
}
}
Expand Down
36 changes: 36 additions & 0 deletions e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,48 @@ import { test as baseTest } from "@playwright/test";
import { LoginPage } from "./login-page";
import { RegistrationPage } from "./registration-page";
import { DashboardPage } from "./base/dashboard-page";
import { resetDatabase } from "../data/reset";
import axios, { AxiosError } from "axios";
import { signIn } from "next-auth/react";
import { seedUser } from "../data/user";

export const test = baseTest.extend<{
resetDatabaseFixture: void;
dashboardPage: DashboardPage;
loginPage: LoginPage;
registrationPage: RegistrationPage;
}>({
resetDatabaseFixture: [
async function ({ loginPage }, use) {
await resetDatabase();
await seedUser();

await loginPage.goto();
await loginPage.page.reload();
await loginPage.usernameInput.fill("test-fake");
await loginPage.passwordInput.fill("password-fake");
await loginPage.submitLoginButton.click();
await (
await loginPage.getLatestToast()
).locator.waitFor({ state: "visible" });
await loginPage.page.reload();
try {
// await axios.get("")
await signIn("credentials", {
username: process.env["TEST_USERNAME"] || "test",
password: process.env["TEST_PASSWORD"] || "password",
redirect: false,
});
//await axios.get("http://localhost:3000/login");
} catch (e) {
// console.log("error", e);
}
// await page.waitForTimeout(1000);

await use();
},
{ auto: true, timeout: 10000 },
],
page: async ({ page }, use) => {
await page.goto("/");
use(page);
Expand Down
1 change: 1 addition & 0 deletions e2e/fixtures/login-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export class LoginPage extends BasePage {

async goto() {
await this.page.goto("/login");
await this.loginForm.waitFor({ state: "visible" });
}
}
5 changes: 5 additions & 0 deletions e2e/fixtures/registration-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ export class RegistrationPage extends BasePage {
this.passwordInput = page.getByTestId("password-input");
this.usernameInput = page.getByTestId("username-input");
}

async goto() {
await this.page.goto("/register");
await this.registrationForm.waitFor({ state: "visible" });
}
}
4 changes: 1 addition & 3 deletions e2e/tests/global/setup.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@ import { seedUser } from "@/e2e/data/user";
import { test as setup } from "../../index";

setup("Setup the default user", async () => {
const username = process.env["TEST_USERNAME"] || "";
const password = process.env["TEST_PASSWORD"] || "";
await seedUser(username, password);
await seedUser();
});
7 changes: 7 additions & 0 deletions e2e/tests/global/teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test } from "@playwright/test";
import { dropTemplate, resetDatabase } from "../../data/reset";

test("Reset the database and the drop the template database", async () => {
await resetDatabase();
await dropTemplate();
});
109 changes: 109 additions & 0 deletions e2e/tests/public/register.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { expect, test } from "../../index";

test.describe(
"Registration test suite",
{
tag: "@registration",
},
async () => {
test("Registration page is accessible from login page", async ({
loginPage,
registrationPage,
}) => {
await loginPage.goto();
await loginPage.registerLink.click();
await expect(registrationPage.registrationForm).toBeVisible();
});

test("Login page is accessible from registration page", async ({
loginPage,
registrationPage,
}) => {
await registrationPage.goto();
await registrationPage.loginLink.click();
await expect(loginPage.loginForm).toBeVisible();
});

test("Ensure filling out all the fields is required and registration works after errors", async ({
loginPage,
registrationPage,
}) => {
await registrationPage.goto();
await test.step("An empty form gives an error", async () => {
await registrationPage.registerButton.click();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "error");
await toast.closeButton.click();
await toast.locator.waitFor({ state: "hidden" });
});
await test.step("Filling in only the display name gives an error", async () => {
await registrationPage.displayNameInput.fill("Display name");
await registrationPage.registerButton.click();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "error");
await toast.closeButton.click();
await toast.locator.waitFor({ state: "hidden" });
});
await test.step("Filling in only the display name and username gives and error", async () => {
await registrationPage.usernameInput.fill("testing-username-123");
await registrationPage.registerButton.click();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "error");
await toast.closeButton.click();
await toast.locator.waitFor({ state: "hidden" });
});
await test.step("Filling in only the display name, username, and password gives and error", async () => {
await registrationPage.passwordInput.fill("pP24^%$boau");
await registrationPage.registerButton.click();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "error");
await toast.closeButton.click();
await toast.locator.waitFor({ state: "hidden" });
});
await test.step("Ensure after filling in the form correctly, it still let's the user register", async () => {
await registrationPage.passwordConfirmInput.fill("pP24^%$boau");
await registrationPage.registerButton.click();
await expect(loginPage.loginForm).toBeVisible();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "success");
});
});

test("After successful registration user can successfully login", async ({
dashboardPage,
loginPage,
registrationPage,
}) => {
await test.step("Register the user", async () => {
await registrationPage.goto();
await registrationPage.displayNameInput.fill("Test name");
await registrationPage.usernameInput.fill("new-registration-username");
await registrationPage.passwordInput.fill("Pp4seword@1");
await registrationPage.passwordConfirmInput.fill("Pp4seword@1");
await registrationPage.registerButton.click();
await expect(loginPage.loginForm).toBeVisible();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "success");
});
await test.step("Login the user", async () => {
await loginPage.usernameInput.fill("new-registration-username");
await loginPage.passwordInput.fill("Pp4seword@1");
await loginPage.submitLoginButton.click();
await expect(dashboardPage.container).toBeVisible();
});
});

test("Ensure mismatching passwords gives an error", async ({
registrationPage,
}) => {
await registrationPage.goto();
await registrationPage.displayNameInput.fill("Test name");
await registrationPage.usernameInput.fill("new-test-username");
await registrationPage.passwordInput.fill("Pp4seword@1");
await registrationPage.passwordConfirmInput.fill("Pp4seword@1333");
await registrationPage.registerButton.click();
const toast = await registrationPage.getLatestToast();
await expect(toast.locator).toHaveAttribute("data-type", "error");
});
}
);
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"next-auth": "^4.22.1",
"node-fetch": "^2.7.0",
"nodemailer": "^6.9.3",
"pg": "^8.11.5",
"playwright": "^1.43.1",
"react": "18.2.0",
"react-colorful": "^5.6.1",
Expand All @@ -75,6 +76,7 @@
"@types/dompurify": "^3.0.4",
"@types/jsdom": "^21.1.3",
"@types/node-fetch": "^2.6.10",
"@types/pg": "^8.11.5",
"@types/shelljs": "^0.8.15",
"autoprefixer": "^10.4.14",
"daisyui": "^4.4.2",
Expand Down
9 changes: 8 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
workers: 1, // process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
expect: { timeout: 15000 },
/* 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('/')`. */
Expand All @@ -32,10 +33,16 @@ export default defineConfig({
{
name: "setup dashboard",
testMatch: /global\/setup\.dashboard\.ts/,
teardown: "cleanup test database",
},
{
name: "setup public",
testMatch: /global\/setup\.public\.ts/,
teardown: "cleanup test database",
},
{
name: "cleanup test database",
testMatch: /global\/teardown\.ts/,
},
{
name: "chromium dashboard",
Expand Down