From c9cc9e406d0e2981789ca3897f3c96fdf051a6bb Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Mon, 24 Mar 2025 10:06:52 +0530 Subject: [PATCH 01/19] e2e with playwright --- .gitignore | 2 + packages/client/.gitignore | 8 ++ packages/client/package.json | 2 + packages/client/playwright.config.ts | 30 +++++ packages/client/tests/auth.spec.ts | 135 +++++++++++++++++++++++ packages/client/tests/home.spec.ts | 19 ++++ packages/client/tests/scoreboard.spec.ts | 14 +++ 7 files changed, 210 insertions(+) create mode 100644 packages/client/.gitignore create mode 100644 packages/client/playwright.config.ts create mode 100644 packages/client/tests/auth.spec.ts create mode 100644 packages/client/tests/home.spec.ts create mode 100644 packages/client/tests/scoreboard.spec.ts diff --git a/.gitignore b/.gitignore index 8a541ee..8df1b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ $RECYCLE.BIN # coverage coverage + +test-results \ No newline at end of file diff --git a/packages/client/.gitignore b/packages/client/.gitignore new file mode 100644 index 0000000..21328f5 --- /dev/null +++ b/packages/client/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +*.env \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index 765a6e6..3789a52 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -24,6 +24,7 @@ "@babel/preset-react": "7.16.0", "@babel/preset-typescript": "7.16.0", "@emotion/jest": "11.5.0", + "@playwright/test": "1.50.1", "@prefresh/babel-plugin": "0.4.1", "@prefresh/webpack": "3.3.2", "@reach/auto-id": "0.16.0", @@ -39,6 +40,7 @@ "@testing-library/preact": "2.0.1", "@testing-library/user-event": "13.5.0", "@theme-ui/preset-base": "0.9.1", + "@types/node": "16.11.6", "@types/react": "17.0.33", "@types/webpack-env": "1.16.3", "babel-loader": "8.2.3", diff --git a/packages/client/playwright.config.ts b/packages/client/playwright.config.ts new file mode 100644 index 0000000..9190590 --- /dev/null +++ b/packages/client/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test' +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + trace: 'on-first-retry', + + baseURL: 'http://localhost:8080', + headless: true, + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + timeout: 30000, +}) diff --git a/packages/client/tests/auth.spec.ts b/packages/client/tests/auth.spec.ts new file mode 100644 index 0000000..71f4aeb --- /dev/null +++ b/packages/client/tests/auth.spec.ts @@ -0,0 +1,135 @@ +import { test, expect } from '@playwright/test' +import config from '../../server/src/config/client' +import dotenv from 'dotenv' + +dotenv.config() +test.describe('rCTF Login Page Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/login') + }) + test('should have the correct page title', async ({ page }) => { + await expect(page).toHaveTitle(`Login | ${config.ctfName}`) + }) + + test('should display the Team Token input field', async ({ page }) => { + await expect(page.locator('input[name="teamToken"]')).toBeVisible() + }) + + test('should log in successfully with a valid Team Token', async ({ + page, + }) => { + await page.fill( + 'input[name="teamToken"]', + 'http://localhost:8080/login?token=KprJKTZTZwpQFYBw544qCVvlV77rbLF5FZfjLdbIDx%2FbsvpnS36IEUbGbGPxIYu5gNDSyhlQD3lU8E1nHHYldu1gCMZjNCphzapsnHntv%2FaVniS2HO%2FI70nhozdX' + ) + await page.click('button[type="submit"]') + await expect(page).not.toHaveURL(/login/) + }) + + test('should show error on invalid Team Token', async ({ page }) => { + await page.fill('input[name="teamToken"]', 'invalidTeamToken') + await page.click('button[type="submit"]') + await expect(page.locator('.text-danger')).toHaveText( + /The token provided is invalid./ + ) + }) + + test('should prevent submission if Team Token is empty', async ({ page }) => { + const teamTokenInput = page.locator('input[name="teamToken"]') + const submitButton = page.locator('button[type="submit"]') + + await submitButton.click() + + await expect(teamTokenInput).toBeFocused() + }) + + test('should navigate to recover page when clicking "Lost your team token?" link', async ({ + page, + }) => { + const recoverLink = page.locator('a[href="/recover"]') + await expect(recoverLink).toBeVisible() + await recoverLink.click() + await expect(page).toHaveURL('http://localhost:8080/recover') + }) + + test('should log in successfully with a valid Team URL', async ({ page }) => { + await page.goto( + 'http://localhost:8080/login?token=KprJKTZTZwpQFYBw544qCVvlV77rbLF5FZfjLdbIDx%2FbsvpnS36IEUbGbGPxIYu5gNDSyhlQD3lU8E1nHHYldu1gCMZjNCphzapsnHntv%2FaVniS2HO%2FI70nhozdX' + ) + const appDiv = page.locator('#app') + const verificationMessage = appDiv.locator('.row.u-center h3') + await expect(verificationMessage).toHaveText( + 'Login as meumarsheik@gmail.com?' + ) + }) + + test('should log in successfully with a invalid Team URL', async ({ + page, + }) => { + await page.goto('http://localhost:8080/login?token=invalid') + const appDiv = page.locator('#app') + const verificationMessage = appDiv.locator('.row.u-center h4') + await expect(verificationMessage).toContainText( + `Log in to ${config.ctfName}` + ) + }) + + test('should go to dashboard page without user login', async ({ page }) => { + await page.goto('http://localhost:8080/profile') + await expect(page).toHaveURL('http://localhost:8080/') + }) +}) + +test.describe('rCTF Register Page Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/register') + }) + + test('should have the correct page title', async ({ page }) => { + await expect(page).toHaveTitle(/Registration/) + }) + + test('should display the Team Name & Email input field', async ({ page }) => { + await expect(page.locator('input[name="name"]')).toBeVisible() + await expect(page.locator('input[name="email"]')).toBeVisible() + }) + + test('should log in successfully with a valid team name & email', async ({ + page, + }) => { + await page.fill('input[name="name"]', 'contactuauth@gmail.com') + await page.fill('input[name="email"]', 'contactuauth@gmail.com') + await page.click('button[type="submit"]') + const appDiv = page.locator('#app') + const verificationMessage = appDiv.locator('.row.u-center h3') + await expect(verificationMessage).toHaveText('Verification email sent!') + }) + + test('should prevent submission if Team name or email is empty', async ({ + page, + }) => { + const submitButton = page.locator('button[type="submit"]') + + const teamNameInput = page.locator('input[name="name"]') + await submitButton.click() + await expect(teamNameInput).toBeFocused() + + await page.fill('input[name="name"]', 'test_name') + const teamEmailInput = page.locator('input[name="email"]') + await submitButton.click() + await expect(teamEmailInput).toBeFocused() + }) + + test('should handle CTFTIME button click and OAuth page has to be opened', async ({ + page, + context, + }) => { + const [newPage] = await Promise.all([ + context.waitForEvent('page'), + page.locator('div.col-6 > button.c0126').click(), + ]) + await newPage.waitForLoadState() + expect(newPage.url()).toMatch(/^https:\/\/oauth\.ctftime\.org\/authorize/) + await newPage.close() + }) +}) diff --git a/packages/client/tests/home.spec.ts b/packages/client/tests/home.spec.ts new file mode 100644 index 0000000..59a8227 --- /dev/null +++ b/packages/client/tests/home.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test' +import config from '../../server/src/config/client' +import dotenv from 'dotenv' + +dotenv.config() +test.describe('rCTF Home Page Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(process.env.BASE_URL || 'http://localhost:8080') // Use env variable + }) + + test('should have the configured page description', async ({ page }) => { + const appDiv = page.locator('#app') + const pageDescription = appDiv.locator('.markup p') + + const expectedText = config.homeContent || '' // Ensure it's a string + + await expect(pageDescription).toHaveText(expectedText) + }) +}) diff --git a/packages/client/tests/scoreboard.spec.ts b/packages/client/tests/scoreboard.spec.ts new file mode 100644 index 0000000..b8e92b2 --- /dev/null +++ b/packages/client/tests/scoreboard.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test' + +test.describe('Scoreboard Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/scores') + }) + + test('should render scoreboard with teams', async ({ page }) => { + await expect(page.locator('table')).toBeVisible() + await expect(page.locator('thead tr')).toHaveCount(1) + const rowCount = await page.locator('table tr').count() + expect(rowCount).toBeGreaterThan(1) + }) +}) From 232fec08e55520f22ca6b9303b11efe6712f4b12 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Wed, 26 Mar 2025 17:34:59 +0530 Subject: [PATCH 02/19] feat: e2e --- packages/client/tests/admin.spec.ts | 16 +++ packages/client/tests/challenges.spec.ts | 98 +++++++++++++++ packages/client/tests/profile.spec.ts | 153 +++++++++++++++++++++++ packages/client/tests/scoreboard.spec.ts | 72 +++++++++++ 4 files changed, 339 insertions(+) create mode 100644 packages/client/tests/admin.spec.ts create mode 100644 packages/client/tests/challenges.spec.ts create mode 100644 packages/client/tests/profile.spec.ts diff --git a/packages/client/tests/admin.spec.ts b/packages/client/tests/admin.spec.ts new file mode 100644 index 0000000..1843ba5 --- /dev/null +++ b/packages/client/tests/admin.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test' + +test.describe('Profile Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/login') + await page.fill( + 'input[name="teamToken"]', + 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + ) + await page.click('button[type="submit"]') + await page.waitForNavigation() + await page.goto('http://localhost:8080/admin/challs') + }) + + +}) diff --git a/packages/client/tests/challenges.spec.ts b/packages/client/tests/challenges.spec.ts new file mode 100644 index 0000000..e758b5d --- /dev/null +++ b/packages/client/tests/challenges.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test' + +test.describe('Challenges Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/login') + + // Fill login token and submit + await page.fill( + 'input[name="teamToken"]', + 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + ) + await page.click('button[type="submit"]') + + await page.waitForNavigation() + + await page.goto('http://localhost:8080/challs') + }) + + test('should render challenges page correctly', async ({ page }) => { + await expect( + page.locator('.frame__title:has-text("Filters")') + ).toBeVisible() + await expect( + page.locator('.frame__title:has-text("Categories")') + ).toBeVisible() + }) + + test('should have categories and filters', async ({ page }) => { + await expect(page.locator('#show-solved')).toBeVisible() + await expect(page.locator('#category-College')).toBeVisible() + await expect(page.locator('#category-High\\ school')).toBeVisible() + }) + + test('should toggle "Show Solved" checkbox', async ({ page }) => { + const showSolvedCheckbox = page.locator('#show-solved') + + const showSolvedLabel = page.locator('label[for="show-solved"]') + + await expect(showSolvedCheckbox).not.toBeChecked() + await showSolvedLabel.click() // Click the label instead + await expect(showSolvedCheckbox).toBeChecked() + + await showSolvedLabel.click() + await expect(showSolvedCheckbox).not.toBeChecked() + }) + + test('should toggle all category filters', async ({ page }) => { + const categoryCheckboxes = page.locator( + '.form-ext-control.form-ext-checkbox input[type="checkbox"]' + ) + const count = await categoryCheckboxes.count() + + for (let i = 0; i < count; i++) { + const checkbox = categoryCheckboxes.nth(i) + + const label = page.locator( + `label[for="${await checkbox.getAttribute('id')}"]` + ) + + const categoryName = await label.innerText() + console.log(`Toggling filter: ${categoryName}`) + + await expect(checkbox).not.toBeChecked() + await label.click() + await expect(checkbox).toBeChecked() + + await label.click() + await expect(checkbox).not.toBeChecked() + } + }) + + test('should submit a flag for a challenge', async ({ page }) => { + const TEST_CHALLENGE = 'High school/1. Can you find root?' + const TEST_CHALLENGE_ANSWER = 'flag(linux_flag)' + const challengeTitle = page.locator( + `.frame__title:has-text("${TEST_CHALLENGE}")` + ) + await expect(challengeTitle).toBeVisible() + + const challengeContainer = challengeTitle.locator( + 'xpath=ancestor::div[contains(@class, "frame__body")]' + ) + await expect(challengeContainer).toBeVisible() + + const flagInput = challengeContainer.locator('input[placeholder="Flag"]') + await flagInput.waitFor({ state: 'visible' }) + + // Locate the submit button + const submitButton = challengeContainer.locator('button:has-text("Submit")') + await expect(submitButton).toBeVisible() + + await flagInput.fill(TEST_CHALLENGE_ANSWER) + await submitButton.click() + + const successToast = page.locator('text=Flag successfully submitted!') + await expect(successToast).toBeVisible() + }) +}) diff --git a/packages/client/tests/profile.spec.ts b/packages/client/tests/profile.spec.ts new file mode 100644 index 0000000..829f1ea --- /dev/null +++ b/packages/client/tests/profile.spec.ts @@ -0,0 +1,153 @@ +import { test, expect } from '@playwright/test' + +test.describe('Profile Page', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/login') + await page.fill( + 'input[name="teamToken"]', + 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + ) + await page.click('button[type="submit"]') + await page.waitForNavigation() + await page.goto('http://localhost:8080/profile') + }) + + test('Verify profile page is rendered correctly', async ({ page }) => { + await expect(page.getByText('Team Invite', { exact: true })).toBeVisible() + await expect(page.locator('text=Update Information')).toBeVisible() + await expect( + page.getByText('Team Information', { exact: true }) + ).toBeVisible() + await expect(page.getByText('Solves', { exact: true })).toBeVisible() + await expect( + page.getByText('CTFtime Integration', { exact: true }) + ).toBeVisible() + }) + + test('should copy invite URL to clipboard when Copy button is clicked', async ({ + page, + }) => { + const copyButton = page.locator('button:has-text("Copy")') + await expect(copyButton).toBeVisible() + + const clipboardHandle = await page.evaluateHandle(() => { + return { + _text: '', + writeText: function (text) { + this._text = text + }, + readText: function () { + return this._text + }, + } + }) + + await page.evaluate(clipboard => { + Object.defineProperty(navigator, 'clipboard', { + value: clipboard, + configurable: true, + }) + }, clipboardHandle) + + await copyButton.click() + + const copiedText = await page.evaluate(() => navigator.clipboard.readText()) + expect(copiedText).toContain('/login?token=') + }) + + test('should reveal and hide the team invite URL when Reveal/Hide button is clicked', async ({ + page, + }) => { + const revealButton = page.locator('button:has-text("Reveal")') + await expect(revealButton).toBeVisible() + await revealButton.click() + + const inviteBlockquote = page.locator('blockquote') + await expect(inviteBlockquote).toBeVisible() + await expect(inviteBlockquote).toContainText('/login?token=') + + const hideButton = page.locator('button:has-text("Hide")') + await expect(hideButton).toBeVisible() + await hideButton.click() + + await expect(page.locator('blockquote')).not.toBeVisible() + }) + + test('should allow updating team information', async ({ page }) => { + const TEST_EMAIL = 'contactuauth@gmail.com' + const TEST_NAME = `Test Team ${Math.random().toString(36).substring(7)}` + + const teamNameInput = page.locator('input[name="name"]') + const emailInput = page.locator('input[name="email"]').nth(0) + const divisionSelect = page.locator('select[name="division"]') + const updateButton = page.locator('button:has-text("Update")') + + await expect(teamNameInput).toBeVisible() + await expect(emailInput).toBeVisible() + await expect(divisionSelect).toBeVisible() + await expect(updateButton).toBeVisible() + + // Check empty name + await teamNameInput.fill('') + await updateButton.click() + await expect(teamNameInput).toBeFocused() + + await teamNameInput.fill(TEST_NAME) + + // Check empty email + await emailInput.fill('') + await updateButton.click() + await page.waitForTimeout(1000) + const errorToast = page.locator('.toast--error') + await expect(errorToast).toBeVisible() + + // Successfull profile update + await emailInput.fill(TEST_EMAIL) + await updateButton.click() + await page.waitForTimeout(1000) + const successToast = page.locator('.toast--undefined') + await expect(successToast).toBeVisible() + + await expect(successToast).toContainText('Profile updated') + }) + + test('should allow add & delete new team member', async ({ page }) => { + const TEST_EMAIL = 'contactuauth@gmail.com' + const TEST_NAME = `Test Team ${Math.random().toString(36).substring(7)}` + + const emailInput = page.locator('input[name="email"]').nth(1) + const updateButton = page.locator('button:has-text("Add member")') + + await expect(emailInput).toBeVisible() + await expect(updateButton).toBeVisible() + + // Check empty name + await emailInput.fill('') + await updateButton.click() + await expect(emailInput).toBeFocused() + + await emailInput.fill(TEST_EMAIL) + + // Successfull profile update + await emailInput.fill(TEST_EMAIL) + await updateButton.click() + await page.waitForTimeout(1000) + const addSuccessToast = page.locator('.toast--undefined') + await expect(addSuccessToast).toBeVisible() + await expect(addSuccessToast).toContainText( + 'Team member successfully added' + ) + + await page.waitForTimeout(4000) + + const deleteButton = page.locator('input[value="Delete"]') + await expect(deleteButton).toBeVisible() + await deleteButton.click() + await page.waitForTimeout(1000) + const deleteSuccessToast = page.locator('.toast--undefined') + await expect(deleteSuccessToast).toBeVisible() + await expect(addSuccessToast).toContainText( + 'Team member successfully deleted' + ) + }) +}) diff --git a/packages/client/tests/scoreboard.spec.ts b/packages/client/tests/scoreboard.spec.ts index b8e92b2..d7cb0aa 100644 --- a/packages/client/tests/scoreboard.spec.ts +++ b/packages/client/tests/scoreboard.spec.ts @@ -1,7 +1,15 @@ import { test, expect } from '@playwright/test' +import config from '../../server/src/config/client' test.describe('Scoreboard Page', () => { test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080/login') + await page.fill( + 'input[name="teamToken"]', + 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + ) + await page.click('button[type="submit"]') + await page.waitForNavigation() await page.goto('http://localhost:8080/scores') }) @@ -11,4 +19,68 @@ test.describe('Scoreboard Page', () => { const rowCount = await page.locator('table tr').count() expect(rowCount).toBeGreaterThan(1) }) + + test('should render scoreboard with teams after filter applied', async ({ + page, + }) => { + await expect(page.locator('table')).toBeVisible() + await expect(page.locator('thead tr')).toHaveCount(1) + + const initialRowCount = await page.locator('table tr').count() + expect(initialRowCount).toBeGreaterThan(1) + + await page.selectOption( + 'select[name="division"]', + Object.values(config.divisions || {})[0] + ) + + await page.waitForTimeout(500) + + const filteredRowCount = await page.locator('table tr').count() + + await expect(page.locator('table')).toBeVisible() + expect(filteredRowCount).toBeGreaterThan(0) + }) + + test('should render scoreboard with teams after limit is changed', async ({ + page, + }) => { + await expect(page.locator('table')).toBeVisible() + await expect(page.locator('thead tr')).toHaveCount(1) + + const initialRowCount = await page.locator('table tr').count() + expect(initialRowCount).toBeGreaterThan(1) + + await page.selectOption('select[name="pagesize"]', '50') + + await page.waitForTimeout(500) + + const filteredRowCount = await page.locator('table tr').count() + + await expect(page.locator('table')).toBeVisible() + expect(filteredRowCount).toBeGreaterThan(0) + }) + + test('should scroll to user’s team when "Go to my team" button is clicked', async ({ + page, + }) => { + const button = page.locator('button', { hasText: 'GO TO MY TEAM' }) + + await expect(button).toBeVisible() + + const isEnabled = await button.isEnabled() + if (isEnabled) { + const initialScrollY = await page.evaluate(() => window.scrollY) + + await button.click() + + await page.waitForTimeout(500) + + const newScrollY = await page.evaluate(() => window.scrollY) + + expect(newScrollY).toBeGreaterThanOrEqual(initialScrollY) + } else { + console.log('Button is disabled, skipping click.') + } + }) }) From 730b9c1339a2d3323f45c29b7aa5346d7c1fbe82 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Wed, 2 Apr 2025 07:48:01 +0530 Subject: [PATCH 03/19] add: tests for admin challenges --- packages/client/tests/admin.spec.ts | 86 ++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/client/tests/admin.spec.ts b/packages/client/tests/admin.spec.ts index 1843ba5..4a54c08 100644 --- a/packages/client/tests/admin.spec.ts +++ b/packages/client/tests/admin.spec.ts @@ -1,16 +1,98 @@ import { test, expect } from '@playwright/test' -test.describe('Profile Page', () => { +test.describe('Admin Challenges Page', () => { test.beforeEach(async ({ page }) => { await page.goto('http://localhost:8080/login') + await page.fill( 'input[name="teamToken"]', 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' ) + await page.click('button[type="submit"]') await page.waitForNavigation() await page.goto('http://localhost:8080/admin/challs') }) - + test('should display challenge form fields', async ({ page }) => { + const problemInput = page + .locator('input[placeholder="Problem Name"]') + .nth(0) + await expect(problemInput).toBeVisible() + + const authorInput = page.locator('input[placeholder="Author"]').nth(0) + await expect(authorInput).toBeVisible() + + const descriptionInput = page + .locator('textarea[placeholder="Description"]') + .nth(0) + await expect(descriptionInput).toBeVisible() + + const flagInput = page.locator('input[placeholder="Flag"]').nth(0) + await expect(flagInput).toBeVisible() + + const fileInput = page.locator('input[type="file"]').nth(0) + await expect(fileInput).toBeVisible() + + const updateButton = page.locator('button:has-text("Update")').nth(0) + await expect(updateButton).toBeVisible() + + const deleteButton = page.locator('button:has-text("Delete")').nth(0) + await expect(deleteButton).toBeVisible() + }) + + test('should allow filling challenge details', async ({ page }) => { + const randomName = `Challenge-${Math.random().toString(36).substring(7)}` + const randomAuthor = `Author-${Math.random().toString(36).substring(5)}` + const testDescription = 'this is testing challenge' + const testFlag = 'flag(test_flag)' + + const problemInput = page + .locator('input[placeholder="Problem Name"]') + .nth(0) + const authorInput = page.locator('input[placeholder="Author"]').nth(0) + + const descriptionInput = page + .locator('textarea[placeholder="Description"]') + .nth(0) + + const flagInput = page.locator('input[placeholder="Flag"]').nth(0) + + await problemInput.fill(randomName) + await authorInput.fill(randomAuthor) + await descriptionInput.fill(testDescription) + await flagInput.fill(testFlag) + + const tiebreakCheckbox = page.locator('input[type="checkbox"]').nth(0) + if (!(await tiebreakCheckbox.isChecked())) { + const label = page.locator( + `label[for="${await tiebreakCheckbox.getAttribute('id')}"]` + ) + await label.click() + } + + await page.click('button:has-text("Update")') + + const successToast = page.locator('.toast--undefined') + await expect(successToast).toBeVisible() + await expect(successToast).toContainText('Problem successfully updated') + }) + + test('should allow deleting a challenge', async ({ page }) => { + const deleteButton = page.locator('button:has-text("Delete")').nth(0) + await expect(deleteButton).toBeVisible() + + await deleteButton.click() + + await page.waitForTimeout(500) + + const confirmDialog = page.locator('button:has-text("Delete Challenge")') + if (await confirmDialog.isVisible()) { + await confirmDialog.click() + } + + const successToast = page.locator('.toast--success') + await expect(successToast).toBeVisible() + await expect(successToast).toContainText('successfully deleted') + }) }) From c40f71678786fd6f1d16a0bfd1e8bbb8ca140f38 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Wed, 2 Apr 2025 09:41:34 +0530 Subject: [PATCH 04/19] update: test config file added --- .gitignore | 1 + packages/client/testConfig.ts | 31 ++++++++++++++++++++ packages/client/tests/admin.spec.ts | 22 +++++--------- packages/client/tests/auth.spec.ts | 37 ++++++++---------------- packages/client/tests/challenges.spec.ts | 17 +++++------ packages/client/tests/home.spec.ts | 8 ++--- packages/client/tests/profile.spec.ts | 23 ++++++--------- packages/client/tests/scoreboard.spec.ts | 21 +++++++------- 8 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 packages/client/testConfig.ts diff --git a/.gitignore b/.gitignore index 8df1b8b..0a7289d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ node_modules dist /.env +/.env.* /data /rctf.d/* !/rctf.d/.keep diff --git a/packages/client/testConfig.ts b/packages/client/testConfig.ts new file mode 100644 index 0000000..e6e374d --- /dev/null +++ b/packages/client/testConfig.ts @@ -0,0 +1,31 @@ +import dotenv from 'dotenv' + +dotenv.config({ path: '.env.test' }) + + +const testConfig = { + baseUrl: process.env.BASE_URL || 'http://localhost:8080', + ctfName: process.env.NAME, + homeContent: process.env.HOME_CONTENT, + loginToken: process.env.LOGIN_TOKEN, + divisions: process.env.DIVISIONS ? JSON.parse(process.env.DIVISIONS) : {}, + + testChal: process.env.TEST_CHAL, + testChalAns: process.env.TEST_CHAL_ANSWER || '', + + testRegEmail: process.env.TEST_REG_EMAIL || '', + testRegName: process.env.TEST_REG_NAME || '', + + testUpdateEmail: process.env.TEST_UPDATE_EMAIL || '', + testUpdateName: process.env.TEST_UPDATE_NAME || '', + + testNewEmail: process.env.TEST_NEW_MEMBER || '', + + testNewChalName: process.env.TEST_NEW_CHAL_NAME || '', + testNewChalAuthor: process.env.TEST_NEW_CHAL_AUTHOR || '', + testNewChalDes: process.env.TEST_NEW_CHAL_DES || '', + testNewChalFlag: process.env.TEST_NEW_CHAL_FLAG || '' + +}; + +export default testConfig diff --git a/packages/client/tests/admin.spec.ts b/packages/client/tests/admin.spec.ts index 4a54c08..b5afc39 100644 --- a/packages/client/tests/admin.spec.ts +++ b/packages/client/tests/admin.spec.ts @@ -1,17 +1,16 @@ import { test, expect } from '@playwright/test' +import testConfig from '../testConfig' test.describe('Admin Challenges Page', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/login') - + await page.goto(`${testConfig.baseUrl}/login`) await page.fill( 'input[name="teamToken"]', - 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + `${testConfig.baseUrl}/login?token=${testConfig.loginToken}` ) - await page.click('button[type="submit"]') await page.waitForNavigation() - await page.goto('http://localhost:8080/admin/challs') + await page.goto(`${testConfig.baseUrl}/admin/challs`) }) test('should display challenge form fields', async ({ page }) => { @@ -42,11 +41,6 @@ test.describe('Admin Challenges Page', () => { }) test('should allow filling challenge details', async ({ page }) => { - const randomName = `Challenge-${Math.random().toString(36).substring(7)}` - const randomAuthor = `Author-${Math.random().toString(36).substring(5)}` - const testDescription = 'this is testing challenge' - const testFlag = 'flag(test_flag)' - const problemInput = page .locator('input[placeholder="Problem Name"]') .nth(0) @@ -58,10 +52,10 @@ test.describe('Admin Challenges Page', () => { const flagInput = page.locator('input[placeholder="Flag"]').nth(0) - await problemInput.fill(randomName) - await authorInput.fill(randomAuthor) - await descriptionInput.fill(testDescription) - await flagInput.fill(testFlag) + await problemInput.fill(testConfig.testNewChalName) + await authorInput.fill(testConfig.testNewChalAuthor) + await descriptionInput.fill(testConfig.testNewChalDes) + await flagInput.fill(testConfig.testNewChalFlag) const tiebreakCheckbox = page.locator('input[type="checkbox"]').nth(0) if (!(await tiebreakCheckbox.isChecked())) { diff --git a/packages/client/tests/auth.spec.ts b/packages/client/tests/auth.spec.ts index 71f4aeb..eae5d98 100644 --- a/packages/client/tests/auth.spec.ts +++ b/packages/client/tests/auth.spec.ts @@ -1,14 +1,12 @@ import { test, expect } from '@playwright/test' -import config from '../../server/src/config/client' -import dotenv from 'dotenv' +import testConfig from '../testConfig' -dotenv.config() test.describe('rCTF Login Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/login') + await page.goto(`${testConfig.baseUrl}/login`) }) test('should have the correct page title', async ({ page }) => { - await expect(page).toHaveTitle(`Login | ${config.ctfName}`) + await expect(page).toHaveTitle(`Login | ${testConfig.ctfName}`) }) test('should display the Team Token input field', async ({ page }) => { @@ -20,7 +18,7 @@ test.describe('rCTF Login Page Tests', () => { }) => { await page.fill( 'input[name="teamToken"]', - 'http://localhost:8080/login?token=KprJKTZTZwpQFYBw544qCVvlV77rbLF5FZfjLdbIDx%2FbsvpnS36IEUbGbGPxIYu5gNDSyhlQD3lU8E1nHHYldu1gCMZjNCphzapsnHntv%2FaVniS2HO%2FI70nhozdX' + `${testConfig.baseUrl}/login?token=${testConfig.loginToken}` ) await page.click('button[type="submit"]') await expect(page).not.toHaveURL(/login/) @@ -49,40 +47,29 @@ test.describe('rCTF Login Page Tests', () => { const recoverLink = page.locator('a[href="/recover"]') await expect(recoverLink).toBeVisible() await recoverLink.click() - await expect(page).toHaveURL('http://localhost:8080/recover') - }) - - test('should log in successfully with a valid Team URL', async ({ page }) => { - await page.goto( - 'http://localhost:8080/login?token=KprJKTZTZwpQFYBw544qCVvlV77rbLF5FZfjLdbIDx%2FbsvpnS36IEUbGbGPxIYu5gNDSyhlQD3lU8E1nHHYldu1gCMZjNCphzapsnHntv%2FaVniS2HO%2FI70nhozdX' - ) - const appDiv = page.locator('#app') - const verificationMessage = appDiv.locator('.row.u-center h3') - await expect(verificationMessage).toHaveText( - 'Login as meumarsheik@gmail.com?' - ) + await expect(page).toHaveURL(`${testConfig.baseUrl}/recover`) }) test('should log in successfully with a invalid Team URL', async ({ page, }) => { - await page.goto('http://localhost:8080/login?token=invalid') + await page.goto(`${testConfig.baseUrl}/login?token=invalid`) const appDiv = page.locator('#app') const verificationMessage = appDiv.locator('.row.u-center h4') await expect(verificationMessage).toContainText( - `Log in to ${config.ctfName}` + `Log in to ${testConfig.ctfName}` ) }) test('should go to dashboard page without user login', async ({ page }) => { - await page.goto('http://localhost:8080/profile') - await expect(page).toHaveURL('http://localhost:8080/') + await page.goto(`${testConfig.baseUrl}/profile`) + await expect(page).toHaveURL(`${testConfig.baseUrl}/`) }) }) test.describe('rCTF Register Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/register') + await page.goto(`${testConfig.baseUrl}/register`) }) test('should have the correct page title', async ({ page }) => { @@ -97,8 +84,8 @@ test.describe('rCTF Register Page Tests', () => { test('should log in successfully with a valid team name & email', async ({ page, }) => { - await page.fill('input[name="name"]', 'contactuauth@gmail.com') - await page.fill('input[name="email"]', 'contactuauth@gmail.com') + await page.fill('input[name="name"]', testConfig.testRegName) + await page.fill('input[name="email"]', testConfig.testRegEmail) await page.click('button[type="submit"]') const appDiv = page.locator('#app') const verificationMessage = appDiv.locator('.row.u-center h3') diff --git a/packages/client/tests/challenges.spec.ts b/packages/client/tests/challenges.spec.ts index e758b5d..ad331ff 100644 --- a/packages/client/tests/challenges.spec.ts +++ b/packages/client/tests/challenges.spec.ts @@ -1,19 +1,20 @@ import { test, expect } from '@playwright/test' +import testConfig from '../testConfig' -test.describe('Challenges Page', () => { +test.describe('Challenges Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/login') + await page.goto(`${testConfig.baseUrl}/login`) // Fill login token and submit await page.fill( 'input[name="teamToken"]', - 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + `${testConfig.baseUrl}/login?token=${testConfig.loginToken}` ) await page.click('button[type="submit"]') await page.waitForNavigation() - await page.goto('http://localhost:8080/challs') + await page.goto(`${testConfig.baseUrl}/challs`) }) test('should render challenges page correctly', async ({ page }) => { @@ -27,8 +28,6 @@ test.describe('Challenges Page', () => { test('should have categories and filters', async ({ page }) => { await expect(page.locator('#show-solved')).toBeVisible() - await expect(page.locator('#category-College')).toBeVisible() - await expect(page.locator('#category-High\\ school')).toBeVisible() }) test('should toggle "Show Solved" checkbox', async ({ page }) => { @@ -70,10 +69,8 @@ test.describe('Challenges Page', () => { }) test('should submit a flag for a challenge', async ({ page }) => { - const TEST_CHALLENGE = 'High school/1. Can you find root?' - const TEST_CHALLENGE_ANSWER = 'flag(linux_flag)' const challengeTitle = page.locator( - `.frame__title:has-text("${TEST_CHALLENGE}")` + `.frame__title:has-text("${testConfig.testChal}")` ) await expect(challengeTitle).toBeVisible() @@ -89,7 +86,7 @@ test.describe('Challenges Page', () => { const submitButton = challengeContainer.locator('button:has-text("Submit")') await expect(submitButton).toBeVisible() - await flagInput.fill(TEST_CHALLENGE_ANSWER) + await flagInput.fill(testConfig.testChalAns) await submitButton.click() const successToast = page.locator('text=Flag successfully submitted!') diff --git a/packages/client/tests/home.spec.ts b/packages/client/tests/home.spec.ts index 59a8227..38ea90c 100644 --- a/packages/client/tests/home.spec.ts +++ b/packages/client/tests/home.spec.ts @@ -1,18 +1,16 @@ import { test, expect } from '@playwright/test' -import config from '../../server/src/config/client' -import dotenv from 'dotenv' +import testConfig from '../testConfig' -dotenv.config() test.describe('rCTF Home Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto(process.env.BASE_URL || 'http://localhost:8080') // Use env variable + await page.goto(testConfig.baseUrl) }) test('should have the configured page description', async ({ page }) => { const appDiv = page.locator('#app') const pageDescription = appDiv.locator('.markup p') - const expectedText = config.homeContent || '' // Ensure it's a string + const expectedText = testConfig.homeContent || '' await expect(pageDescription).toHaveText(expectedText) }) diff --git a/packages/client/tests/profile.spec.ts b/packages/client/tests/profile.spec.ts index 829f1ea..63b0b3b 100644 --- a/packages/client/tests/profile.spec.ts +++ b/packages/client/tests/profile.spec.ts @@ -1,15 +1,16 @@ import { test, expect } from '@playwright/test' +import testConfig from '../testConfig' -test.describe('Profile Page', () => { +test.describe('Profile Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/login') + await page.goto(`${testConfig.baseUrl}/login`) await page.fill( 'input[name="teamToken"]', - 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + `${testConfig.baseUrl}/login?token=${testConfig.loginToken}` ) await page.click('button[type="submit"]') await page.waitForNavigation() - await page.goto('http://localhost:8080/profile') + await page.goto(`${testConfig.baseUrl}/profile`) }) test('Verify profile page is rendered correctly', async ({ page }) => { @@ -74,9 +75,6 @@ test.describe('Profile Page', () => { }) test('should allow updating team information', async ({ page }) => { - const TEST_EMAIL = 'contactuauth@gmail.com' - const TEST_NAME = `Test Team ${Math.random().toString(36).substring(7)}` - const teamNameInput = page.locator('input[name="name"]') const emailInput = page.locator('input[name="email"]').nth(0) const divisionSelect = page.locator('select[name="division"]') @@ -92,7 +90,7 @@ test.describe('Profile Page', () => { await updateButton.click() await expect(teamNameInput).toBeFocused() - await teamNameInput.fill(TEST_NAME) + await teamNameInput.fill(testConfig.testUpdateName) // Check empty email await emailInput.fill('') @@ -102,7 +100,7 @@ test.describe('Profile Page', () => { await expect(errorToast).toBeVisible() // Successfull profile update - await emailInput.fill(TEST_EMAIL) + await emailInput.fill(testConfig.testUpdateEmail) await updateButton.click() await page.waitForTimeout(1000) const successToast = page.locator('.toast--undefined') @@ -112,9 +110,6 @@ test.describe('Profile Page', () => { }) test('should allow add & delete new team member', async ({ page }) => { - const TEST_EMAIL = 'contactuauth@gmail.com' - const TEST_NAME = `Test Team ${Math.random().toString(36).substring(7)}` - const emailInput = page.locator('input[name="email"]').nth(1) const updateButton = page.locator('button:has-text("Add member")') @@ -126,10 +121,10 @@ test.describe('Profile Page', () => { await updateButton.click() await expect(emailInput).toBeFocused() - await emailInput.fill(TEST_EMAIL) + await emailInput.fill(testConfig.testNewEmail) // Successfull profile update - await emailInput.fill(TEST_EMAIL) + await emailInput.fill(testConfig.testNewEmail) await updateButton.click() await page.waitForTimeout(1000) const addSuccessToast = page.locator('.toast--undefined') diff --git a/packages/client/tests/scoreboard.spec.ts b/packages/client/tests/scoreboard.spec.ts index d7cb0aa..738c84f 100644 --- a/packages/client/tests/scoreboard.spec.ts +++ b/packages/client/tests/scoreboard.spec.ts @@ -1,16 +1,16 @@ import { test, expect } from '@playwright/test' -import config from '../../server/src/config/client' +import testConfig from '../testConfig' -test.describe('Scoreboard Page', () => { +test.describe('Scoreboard Page Tests', () => { test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/login') + await page.goto(`${testConfig.baseUrl}/login`) await page.fill( 'input[name="teamToken"]', - 'http://localhost:8080/login?token=1cweecsMOoefSKaxtUMKVHi3zd5vF0Qgk41PW8DXTQcJjI95Nw5gMYHE9uB5jFdsnippN25QeyKvRBQBXCdUsMCDXdF8yJC9FObNqiCE%2FjqrTheDXTAMe1Anqver' + `${testConfig.baseUrl}/login?token=${testConfig.loginToken}` ) await page.click('button[type="submit"]') await page.waitForNavigation() - await page.goto('http://localhost:8080/scores') + await page.goto(`${testConfig.baseUrl}/scores`) }) test('should render scoreboard with teams', async ({ page }) => { @@ -29,15 +29,16 @@ test.describe('Scoreboard Page', () => { const initialRowCount = await page.locator('table tr').count() expect(initialRowCount).toBeGreaterThan(1) - await page.selectOption( - 'select[name="division"]', - Object.values(config.divisions || {})[0] - ) + const divisionOptions = Object.values(testConfig.divisions) as string[] + if (divisionOptions.length > 0) { + await page.selectOption('select[name="division"]', divisionOptions[0]) + } else { + console.log('No divisions available in testConfig') + } await page.waitForTimeout(500) const filteredRowCount = await page.locator('table tr').count() - await expect(page.locator('table')).toBeVisible() expect(filteredRowCount).toBeGreaterThan(0) }) From 0925315053eb86a099e6e91167abcfb76acb9b20 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Mon, 7 Apr 2025 13:36:29 +0530 Subject: [PATCH 05/19] admin page access control --- packages/api-types/src/responses/goodLogin.yml | 2 ++ packages/client/src/api/auth.js | 4 +++- packages/client/src/app.js | 7 ++++++- packages/client/src/routes/login.js | 4 ++-- packages/client/testConfig.ts | 5 ++++- packages/client/tests/admin.spec.ts | 14 +++++++++++++- packages/server/src/api/auth/login.ts | 2 +- .../server/test/unit/api/buildResponseSchema.ts | 2 +- 8 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/api-types/src/responses/goodLogin.yml b/packages/api-types/src/responses/goodLogin.yml index 184d27f..0eb944a 100644 --- a/packages/api-types/src/responses/goodLogin.yml +++ b/packages/api-types/src/responses/goodLogin.yml @@ -5,4 +5,6 @@ data: properties: authToken: type: string + perms: + type: number required: ['authToken'] diff --git a/packages/client/src/api/auth.js b/packages/client/src/api/auth.js index abd3501..6a0bb65 100644 --- a/packages/client/src/api/auth.js +++ b/packages/client/src/api/auth.js @@ -1,8 +1,9 @@ import { request } from './util' import { route } from '../history-hack' -export const setAuthToken = ({ authToken }) => { +export const setAuthToken = ({ authToken, perms }) => { localStorage.token = authToken + localStorage.setItem('perms', perms) route('/profile') } @@ -15,6 +16,7 @@ export const login = async ({ teamToken, ctftimeToken }) => { case 'goodLogin': return { authToken: resp.data.authToken, + perms: resp.data.perms, } case 'badTokenVerification': return { diff --git a/packages/client/src/app.js b/packages/client/src/app.js index 585573e..81782ee 100644 --- a/packages/client/src/app.js +++ b/packages/client/src/app.js @@ -32,6 +32,7 @@ const LoggedInRedir = function App({ classes }) { const loggedOut = !localStorage.token + const hasPerms = localStorage.perms == 0 const loggedOutPaths = [ { @@ -61,8 +62,11 @@ function App({ classes }) { path: '/challs', name: 'Challenges', }, + ] + + const adminPaths = [ { - element: , + element: hasPerms ? : LoggedOutRedir, path: '/admin/challs', }, ] @@ -108,6 +112,7 @@ function App({ classes }) { const currentPaths = [ ...allPaths, ...(loggedOut ? loggedOutPaths : loggedInPaths), + ...adminPaths, ] const headerPaths = currentPaths.filter(route => route.name !== undefined) diff --git a/packages/client/src/routes/login.js b/packages/client/src/routes/login.js index adc0f4a..56c0b94 100644 --- a/packages/client/src/routes/login.js +++ b/packages/client/src/routes/login.js @@ -142,7 +142,7 @@ export default withStyles( }) const loginRes = await login({ ctftimeToken }) if (loginRes.authToken) { - setAuthToken({ authToken: loginRes.authToken }) + setAuthToken({ authToken: loginRes.authToken, perms: loginRes.perms }) } if (loginRes && loginRes.badUnknownUser) { this.setState({ @@ -175,7 +175,7 @@ export default withStyles( teamToken, }) if (result.authToken) { - setAuthToken({ authToken: result.authToken }) + setAuthToken({ authToken: result.authToken, perms: result.perms }) return } this.setState({ diff --git a/packages/client/testConfig.ts b/packages/client/testConfig.ts index e6e374d..c87418c 100644 --- a/packages/client/testConfig.ts +++ b/packages/client/testConfig.ts @@ -24,7 +24,10 @@ const testConfig = { testNewChalName: process.env.TEST_NEW_CHAL_NAME || '', testNewChalAuthor: process.env.TEST_NEW_CHAL_AUTHOR || '', testNewChalDes: process.env.TEST_NEW_CHAL_DES || '', - testNewChalFlag: process.env.TEST_NEW_CHAL_FLAG || '' + testNewChalFlag: process.env.TEST_NEW_CHAL_FLAG || '', + + + regularUserToken: process.env.REGULAR_USER_TOKEN || '' }; diff --git a/packages/client/tests/admin.spec.ts b/packages/client/tests/admin.spec.ts index b5afc39..a74e4e4 100644 --- a/packages/client/tests/admin.spec.ts +++ b/packages/client/tests/admin.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' import testConfig from '../testConfig' -test.describe('Admin Challenges Page', () => { +test.describe('Admin Challenges Page Tests', () => { test.beforeEach(async ({ page }) => { await page.goto(`${testConfig.baseUrl}/login`) await page.fill( @@ -90,3 +90,15 @@ test.describe('Admin Challenges Page', () => { await expect(successToast).toContainText('successfully deleted') }) }) + + +test.describe('Admin Challenges Page Access Control Tests', () => { + test('should redirect unauthorized user to home page', async ({ page }) => { + await page.goto(`${testConfig.baseUrl}/login`) + await page.fill('input[name="teamToken"]', testConfig.regularUserToken) + await page.click('button[type="submit"]') + await page.waitForNavigation() + await page.goto(`${testConfig.baseUrl}/admin/challs`) + await expect(page).toHaveURL(`${testConfig.baseUrl}/`) + }) +}) diff --git a/packages/server/src/api/auth/login.ts b/packages/server/src/api/auth/login.ts index 80c38a5..ac0840e 100644 --- a/packages/server/src/api/auth/login.ts +++ b/packages/server/src/api/auth/login.ts @@ -29,6 +29,6 @@ export default makeFastifyRoute( return res.badUnknownUser() } const authToken = await getToken(tokenKinds.auth, user.id) - return res.goodLogin({ authToken }) + return res.goodLogin({ authToken: authToken, perms: user.perms }) } ) diff --git a/packages/server/test/unit/api/buildResponseSchema.ts b/packages/server/test/unit/api/buildResponseSchema.ts index 7db4c2a..3eb51f1 100644 --- a/packages/server/test/unit/api/buildResponseSchema.ts +++ b/packages/server/test/unit/api/buildResponseSchema.ts @@ -301,7 +301,7 @@ describe('optimizeSchema', () => { oneOf: [ makeStandardResponse('goodLogin', 'The login was successful.', { type: 'object', - properties: { authToken: { type: 'string' } }, + properties: { authToken: { type: 'string' }, perms: { type: 'number' } }, required: ['authToken'], additionalProperties: false, }), From b158a790abfd60083850be9a60f8bd5e2f5f2bc4 Mon Sep 17 00:00:00 2001 From: Umar Sheik Date: Thu, 10 Apr 2025 14:11:00 +0530 Subject: [PATCH 06/19] admin page UI updates & access control issue fix --- packages/client/src/app.js | 2 +- .../client/src/components/admin/problem.js | 263 ++++++++++-------- 2 files changed, 141 insertions(+), 124 deletions(-) diff --git a/packages/client/src/app.js b/packages/client/src/app.js index 81782ee..e4ab86c 100644 --- a/packages/client/src/app.js +++ b/packages/client/src/app.js @@ -32,7 +32,7 @@ const LoggedInRedir = function App({ classes }) { const loggedOut = !localStorage.token - const hasPerms = localStorage.perms == 0 + const hasPerms = localStorage.perms === '3' const loggedOutPaths = [ { diff --git a/packages/client/src/components/admin/problem.js b/packages/client/src/components/admin/problem.js index 969c0c5..c728b70 100644 --- a/packages/client/src/components/admin/problem.js +++ b/packages/client/src/components/admin/problem.js @@ -229,137 +229,154 @@ const Problem = ({ classes, problem, update: updateClient }) => {
-
-
-
- - -
- +
+
+ {/* Section: Basic Info */} +
+
+ + + + + + +
+ +
+ + + + +
+ + +
+
+
+ + {/* Section: Description */} +
+ +