From cb55d95f74eb3cadc21acae73edc615d6394d3f2 Mon Sep 17 00:00:00 2001 From: Sebastien DUMETZ Date: Thu, 6 Feb 2025 15:21:48 +0100 Subject: [PATCH] more end to end tests --- source/e2e/fixtures.ts | 89 +++++++++++++++++++++++ source/e2e/tests/admin.spec.ts | 94 +++++++++++++++++++++++++ source/e2e/tests/admin.test.ts | 21 ------ source/e2e/tests/history.spec.ts | 64 +++++++++++++++++ source/e2e/tests/scene_settings.spec.ts | 92 ++++++++++++++++++++++++ source/e2e/tests/userSettings.spec.ts | 89 +++++++++++++---------- 6 files changed, 390 insertions(+), 59 deletions(-) create mode 100644 source/e2e/fixtures.ts create mode 100644 source/e2e/tests/admin.spec.ts delete mode 100644 source/e2e/tests/admin.test.ts create mode 100644 source/e2e/tests/history.spec.ts create mode 100644 source/e2e/tests/scene_settings.spec.ts diff --git a/source/e2e/fixtures.ts b/source/e2e/fixtures.ts new file mode 100644 index 00000000..5aa2e5fb --- /dev/null +++ b/source/e2e/fixtures.ts @@ -0,0 +1,89 @@ +import path from "node:path"; +import { expect, Page, test as base } from '@playwright/test'; +import { randomBytes, randomUUID } from 'node:crypto'; + + +const fixtures = path.resolve(import.meta.dirname, "./__test_fixtures"); + +export type CreateSceneOptions = { + permissions?: Record, + autoDelete?:boolean, +} + +type TestFixture = { + adminPage:Page, + userPage:Page, + createScene:(opts?:CreateSceneOptions)=>Promise, + uniqueAccount: {username:string, password:string, uid:number}, + +} + +export {expect} from "@playwright/test"; + +export const test = base.extend({ + adminPage: async ({browser}, use)=>{ + const ctx = await browser.newContext({ storageState: 'playwright/.auth/admin.json', locale: "cimode" }); + const adminPage = await ctx.newPage(); + await use(adminPage); + await ctx.close(); + }, + userPage: async ({browser}, use)=>{ + const ctx = await browser.newContext({ storageState: 'playwright/.auth/user.json', locale: "cimode" }); + const userPage = await ctx.newPage(); + await use(userPage); + await ctx.close(); + }, + /** + * Factory function to create new scenes with random names + * Will _generally_ clean up scenes afterwards + */ + createScene: async ({browser}, use)=>{ + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path.join(fixtures, "cube.glb")) + const ctx = await browser.newContext({ storageState: 'playwright/.auth/user.json', locale: "cimode" }); + const request = ctx.request; + const names :string[] = []; + await use(async ({permissions, autoDelete=true} :CreateSceneOptions={})=>{ + const name = randomUUID(); + if(autoDelete) names.push(name); + let res = await request.post(`/scenes/${encodeURIComponent(name)}`, { + data, + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + //Set expected permissions + if(permissions){ + res = await request.patch(`/scenes/${encodeURIComponent(name)}`, { + data: { + permissions: permissions + } + }); + } + return name; + }); + await Promise.all(names.map(name=> request.delete(`/scenes/${encodeURIComponent(name)}?archive=false`))); + await ctx.close(); + }, + uniqueAccount: async ({browser}, use)=>{ + let username = `testUserLogin${randomBytes(2).readUInt16LE().toString(36)}`; + let password = randomBytes(16).toString("base64"); + let adminContext = await browser.newContext({storageState: "playwright/.auth/admin.json"}); + //Create a user for this specific test + let res = await adminContext.request.post("/users", { + data: JSON.stringify({ + username, + email: `${username}@example.com`, + password, + isAdministrator: false, + }), + headers:{ + "Content-Type": "application/json", + } + }); + let body = JSON.parse(await res.text()); + expect(body).toHaveProperty("uid"); + let uid :number =body.uid; + await use({username, password, uid}); + await adminContext.close(); + }, +}); \ No newline at end of file diff --git a/source/e2e/tests/admin.spec.ts b/source/e2e/tests/admin.spec.ts new file mode 100644 index 00000000..35dd5fac --- /dev/null +++ b/source/e2e/tests/admin.spec.ts @@ -0,0 +1,94 @@ +import path from "node:path"; + + +import { expect, test } from '@playwright/test'; +import { randomUUID } from "node:crypto"; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as admin +test.use({ storageState: 'playwright/.auth/admin.json', locale: "cimode" }); + + + +test.skip("can create a new user", async ({page})=>{ + await page.goto("/ui/admin/users"); + +}); + +test.skip("can delete a non-admin user", async ({page})=>{ + +}); + +test("can force-delete archived scenes", async ({page, request})=>{ + + const name = randomUUID(); + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path.join(fixtures, "cube.glb")) + let res = await page.request.post(`/scenes/${encodeURIComponent(name)}`, { + data, + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + + res = await page.request.delete(`/scenes/${encodeURIComponent(name)}?archive=true`); + await expect(res).toBeOK(); + + res = await request.get(`/scenes?archived=any&match=${name}`); + let body = await res.json(); + expect(body.scenes).toHaveLength(1); + expect(body.scenes[0]).toHaveProperty("archived", true); + + //There can be any number of archived scenes shown here that leaks from other tests + //But we expect "our" scene to be there and that's what we are looking for + await page.goto("/ui/admin/archives"); + + await expect(page.getByRole("link", {name})).toBeVisible(); + + await page.getByRole("row", {name}).getByRole("button", {name: "labels.delete"}).click(); + + + await expect(page.getByRole("link", {name})).not.toBeVisible(); + + + res = await request.get(`/scenes?archived=any&match=${name}`); + body = await res.json(); + expect(body.scenes).toHaveLength(0); +}); + +test("can restore archived scenes", async ({page, request})=>{ + + const name = randomUUID(); + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path.join(fixtures, "cube.glb")) + let res = await page.request.post(`/scenes/${encodeURIComponent(name)}`, { + data, + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + + res = await page.request.delete(`/scenes/${encodeURIComponent(name)}?archive=true`); + await expect(res).toBeOK(); + + res = await request.get(`/scenes?archived=any&match=${name}`); + let body = await res.json(); + expect(body.scenes).toHaveLength(1); + expect(body.scenes[0]).toHaveProperty("archived", true); + + //There can be any number of archived scenes shown here that leaks from other tests + //But we expect "our" scene to be there and that's what we are looking for + await page.goto("/ui/admin/archives"); + + await expect(page.getByRole("link", {name})).toBeVisible(); + + await page.getByRole("row", {name}).getByRole("button", {name: "labels.restore"}).click(); + + + await expect(page.getByRole("link", {name})).not.toBeVisible(); + + + res = await request.get(`/scenes?archived=any&match=${name}`); + body = await res.json(); + expect(body.scenes).toHaveLength(1); + expect(body.scenes[0]).toHaveProperty("archived", false); +}); \ No newline at end of file diff --git a/source/e2e/tests/admin.test.ts b/source/e2e/tests/admin.test.ts deleted file mode 100644 index 7fce58f3..00000000 --- a/source/e2e/tests/admin.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path"; - - -import { test } from '@playwright/test'; - -const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); - -//Authenticated as admin -test.use({ storageState: 'playwright/.auth/admin.json' }); - - - -test.skip("can create a new user", async ({page})=>{ - await page.goto("/ui/admin/users"); - -}); - -test.skip("can delete a non-admin user", async ({page})=>{ - -}); - diff --git a/source/e2e/tests/history.spec.ts b/source/e2e/tests/history.spec.ts new file mode 100644 index 00000000..666a6b3b --- /dev/null +++ b/source/e2e/tests/history.spec.ts @@ -0,0 +1,64 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { randomUUID } from "node:crypto"; + +import { test, expect, Page } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as user +test.use({ storageState: 'playwright/.auth/user.json', locale: "cimode" }); +test.describe.configure({ mode: 'serial' }); + +let scenePage: Page; + +const name = randomUUID(); + + +let initial_doc :string; +/** + * Tests in this suite are run in serial mode with no page reload, unless otherwise specified + */ +test.beforeAll(async ({request, browser})=>{ + //Create a scene + let res = await request.post(`/scenes/${encodeURIComponent(name)}?language=FR`, { + data: await fs.readFile(path.join(fixtures, "cube.glb")), + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + + res = await request.get(`/scenes/${encodeURIComponent(name)}/scene.svx.json`); + initial_doc = await res.text(); + + expect(initial_doc).toBeTruthy(); + expect(initial_doc.slice(0,1)).toEqual("{"); //Checks that it appears to be JSON... + + //Make a bunch of changes + res = await request.put(`/scenes/${encodeURIComponent(name)}/articles/new-article-OKiTjtY6zrbJ-EN.html`, { + data: await fs.readFile(path.join(fixtures, "new-article-OKiTjtY6zrbJ-EN.html")), + headers: {"Content-Type": "text/html"} + }); + await expect(res).toBeOK(); + + res = await request.put(`/scenes/${encodeURIComponent(name)}/scene.svx.json`, { + data: await fs.readFile(path.join(fixtures, "scene.svx.json")), + headers: {"Content-Type": "application/json"} + }); + await expect(res).toBeOK(); + + + scenePage = await browser.newPage(); + await scenePage.goto(`/ui/scenes/${name}`); +}); + + +test.afterAll(async () => { + await scenePage.close(); +}); + + +test("can navigate to history page", async ()=>{ + await scenePage.getByRole("link", {name: "buttons.history"}).click(); + await scenePage.waitForURL(`/ui/scenes/${name}/history`); + +}); diff --git a/source/e2e/tests/scene_settings.spec.ts b/source/e2e/tests/scene_settings.spec.ts new file mode 100644 index 00000000..19f27cb0 --- /dev/null +++ b/source/e2e/tests/scene_settings.spec.ts @@ -0,0 +1,92 @@ +import path from "node:path"; +import fs from "node:fs/promises"; + + +import { expect, test } from '../fixtures'; +import { randomUUID } from "node:crypto"; + + + +test.use({ storageState: {cookies:[], origins: []}, locale: "cimode"}); + + +//Authenticated as user + + +test.describe("author", ()=>{ + + test("can edit his scenes", async ({userPage, createScene})=>{ + let name = await createScene(); + await userPage.goto(`/ui/scenes/${encodeURIComponent(name)}`); + + //Expect "edit" and "history" buttons to be present + await expect(userPage.getByRole("link", {name: "labels.edit", exact: true})).toHaveAttribute("href", `/ui/scenes/${encodeURIComponent(name)}/edit`) + await expect(userPage.getByRole("link", {name: "buttons.history", exact: true})).toHaveAttribute("href", `/ui/scenes/${encodeURIComponent(name)}/history`) + }); + + test("can archive and restore his scenes", async ({userPage, createScene})=>{ + let name = await createScene(); + await userPage.goto(`/ui/scenes/${encodeURIComponent(name)}`); + await userPage.getByRole('button', { name: 'buttons.archive' }).click(); + + //We are now on the archived scene's page + await userPage.waitForURL(/\/ui\/scenes\/.+/); + + //Check it is truly archived + let res = await userPage.request.get(`/ui/scenes/${encodeURIComponent(name)}`); + expect(res.status()).toEqual(404); + + //Default rename target should be our original name + await expect(userPage.locator("#scene-name-input")).toHaveValue(name); + //Decide of a new name for the scene + let newName = randomUUID(); + await userPage.locator("#scene-name-input").fill(newName); + await userPage.getByRole("button", {name: "labels.restore"}).click(); + + await userPage.waitForURL(`/ui/scenes/${newName}`); + }); +}); + +test.describe("admin", ()=>{ + test("can edit other users' scenes", async ({adminPage, createScene})=>{ + let name = await createScene(); + await adminPage.goto(`/ui/scenes/${encodeURIComponent(name)}`); + + //Expect "edit" and "history" buttons to be present + await expect(adminPage.getByRole("link", {name: "labels.edit", exact: true})).toHaveAttribute("href", `/ui/scenes/${encodeURIComponent(name)}/edit`) + await expect(adminPage.getByRole("link", {name: "buttons.history", exact: true})).toHaveAttribute("href", `/ui/scenes/${encodeURIComponent(name)}/history`) + }); + + test("can archive other user's scene", async ({adminPage, createScene})=>{ + let name = await createScene(); + await adminPage.goto(`/ui/scenes/${encodeURIComponent(name)}`); + await adminPage.getByRole('button', { name: 'buttons.archive' }).click() + await adminPage.waitForURL(new RegExp(`/ui/scenes/${name}`)); + + let res = await adminPage.request.get(`/ui/scenes/${encodeURIComponent(name)}`); + expect(res.status()).toEqual(404); + }); + +}); + +test("read-only view", async ({page, createScene})=>{ + let name = await createScene(); + await page.goto(`/ui/scenes/${encodeURIComponent(name)}`); + + //Expect "edit" and "history" buttons to be present + await expect(page.getByRole("link", {name: "labels.view", exact: true})).toBeVisible(); + await expect(page.getByRole("link", {name: "labels.edit", exact: true})).not.toBeVisible(); + await expect(page.getByRole("link", {name: "buttons.history", exact: true})).not.toBeVisible(); + + let res= await page.goto(`/ui/scenes/${encodeURIComponent(name)}/edit`); + expect(res?.status()).toEqual(404); +}); + + +test("404 view", async ({page, createScene})=>{ + let name = await createScene({permissions: {default: "none"}}); + for (let p of ["", "edit", "view", "history"]) { + let res= await page.goto(`/ui/scenes/${encodeURIComponent(name)}/${p}`); + expect(res?.status()).toEqual(404); + } +}); \ No newline at end of file diff --git a/source/e2e/tests/userSettings.spec.ts b/source/e2e/tests/userSettings.spec.ts index cd4db211..90f4c441 100644 --- a/source/e2e/tests/userSettings.spec.ts +++ b/source/e2e/tests/userSettings.spec.ts @@ -2,47 +2,13 @@ import path from "node:path"; import fs, { readFile } from "node:fs/promises"; import { randomBytes, randomUUID } from "node:crypto"; -import { test as _test, expect, Page, BrowserContext, APIRequestContext } from '@playwright/test'; +import { test, expect } from '../fixtures'; const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); type Account = {username: string, password: string, uid: number}; -const test = _test.extend<{account:Account}>({ - account: async ({browser}, use)=>{ - let username = `testUserLogin${randomBytes(2).readUInt16LE().toString(36)}`; - let password = randomBytes(16).toString("base64"); - let adminContext = await browser.newContext({storageState: "playwright/.auth/admin.json"}); - //Create a user for this specific test - let res = await adminContext.request.post("/users", { - data: JSON.stringify({ - username, - email: `${username}@example.com`, - password, - isAdministrator: false, - }), - headers:{ - "Content-Type": "application/json", - } - }); - let body = JSON.parse(await res.text()); - expect(body).toHaveProperty("uid"); - let uid :number =body.uid; - await use({username, password, uid}); - await adminContext.close(); - }, - page: async({page, account:{username, password}}, use)=>{ - let res = await page.request.post("/auth/login", { - data: JSON.stringify({username, password}), - headers:{ - "Content-Type": "application/json", - } - }); - expect(res.status()).toEqual(200); - await use(page); - }, -}) @@ -51,6 +17,17 @@ const test = _test.extend<{account:Account}>({ //Runs with a per-test storageState, in locale "cimode" test.use({ storageState: { cookies: [], origins: [] }, locale: "cimode" }); +test.beforeEach(async ({page, uniqueAccount:{username, password}})=>{ + let res = await page.request.post("/auth/login", { + data: JSON.stringify({username, password}), + headers:{ + "Content-Type": "application/json", + } + }); + expect(res.status()).toEqual(200); +}) + + test("can read user settings page", async function({page}){ await page.goto("/ui/"); @@ -60,9 +37,9 @@ test("can read user settings page", async function({page}){ }); -test("can change email", async ({page, account})=>{ +test("can change email", async ({page, uniqueAccount})=>{ //Ensure this is unique, otherwise it is rejected - let new_email = `${account.username}-replacement@example2.com` + let new_email = `${uniqueAccount.username}-replacement@example2.com` await page.goto("/ui/user/"); const form = page.getByRole("form", {name: "titles.userProfile"}); await expect(form).toBeVisible(); @@ -80,7 +57,7 @@ test("can change email", async ({page, account})=>{ await expect(emailField).toHaveValue(new_email); }); -test("can change password", async ({baseURL, page, account:{username, password}})=>{ +test("can change password", async ({baseURL, page, uniqueAccount:{username, password}})=>{ const new_password = randomBytes(10).toString("base64"); let res = await fetch(new URL(`/auth/login`, baseURL), { @@ -134,4 +111,40 @@ test("can logout", async ({page})=>{ res = await page.request.get(`/auth/login`); expect(res).toBeOK(); expect(await res.json()).toHaveProperty("isDefaultUser", true); +}); + +test("can show archived scenes", async ({page, uniqueAccount:{username}})=>{ + const name = randomUUID(); + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path.join(fixtures, "cube.glb")) + let res = await page.request.post(`/scenes/${encodeURIComponent(name)}`, { + data, + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + + res = await page.request.delete(`/scenes/${encodeURIComponent(name)}`); + await expect(res).toBeOK(); + + res = await page.request.get(`/scenes?archived=true&author=${username}`); + await expect(res).toBeOK(); + const {scenes} = await res.json(); + expect(scenes).toHaveLength(1); + + await page.goto("/ui/user"); + + await expect(page.getByRole("link", {name})).toBeVisible(); + await page.getByLabel('labels.restore').click(); + await expect(page.getByRole("link", {name})).not.toBeVisible(); + + res = await page.request.get(`/scenes?archived=true&author=${username}`); + await expect(res).toBeOK(); + let body = await res.json(); + expect(body.scenes).toHaveLength(0); + + res = await page.request.get(`/scenes?archived=false&author=${username}`); + await expect(res).toBeOK(); + body = await res.json(); + console.log(name, body); + expect(body.scenes).toHaveLength(1); }); \ No newline at end of file