Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ jobs:
strategy:
matrix:
server_version: [stable29, stable30, stable31, stable32, master]
shard: [1, 2, 3]
fail-fast: false
timeout-minutes: 60
runs-on: ubuntu-latest
env:
SERVER_VERSION: ${{ matrix.server_version }}
name: Playwright Tests on Nextcloud ${{ matrix.server_version }}
name: Playwright Tests on Nextcloud ${{ matrix.server_version }} (${{ matrix.shard }}/3)
steps:
- name: Checkout app
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -57,11 +58,11 @@ jobs:

- name: Run Playwright tests
run: |
npx playwright test
npx playwright test --reporter=line,html --shard=${{ matrix.shard }}/3

- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: playwright-report-${{ matrix.server_version }}
name: playwright-report-${{ matrix.server_version }}-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 30
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"license": "AGPL",
"require": {
"php": "^8.0",
"firebase/php-jwt": "^6.10"
"firebase/php-jwt": "^7"
},
"require-dev": {
"nextcloud/coding-standard": "^1.3.2",
Expand Down
15 changes: 8 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { defineConfig, devices } from '@playwright/test'
import { PLAYWRIGHT_SHARED_SECRET } from './playwright/support/test-secrets'

/**
* See https://playwright.dev/docs/test-configuration.
Expand Down Expand Up @@ -33,10 +34,10 @@ export default defineConfig({
baseURL: 'http://localhost:8089/index.php/',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on',
trace: process.env.CI ? 'on-first-retry' : 'on',

/* Capture video of test runs - only keep videos for failed tests */
video: 'on',
/* Record videos only when CI retries a failing test to keep the suite lighter. */
video: process.env.CI ? 'on-first-retry' : 'on',
},

projects: [
Expand Down Expand Up @@ -67,7 +68,7 @@ export default defineConfig({
},
{
// Starts the whiteboard websocket server without TLS for tests
command: 'TLS=false METRICS_TOKEN=secret JWT_SECRET_KEY=secret NEXTCLOUD_URL=http://localhost:8089 npm run server:start',
command: `TLS=false METRICS_TOKEN=secret JWT_SECRET_KEY=${PLAYWRIGHT_SHARED_SECRET} NEXTCLOUD_URL=http://localhost:8089 npm run server:start`,
reuseExistingServer: !process.env.CI,
url: 'http://localhost:3002',
stderr: 'pipe',
Expand Down
3 changes: 2 additions & 1 deletion playwright/support/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { readFileSync } from 'fs'
import { test as setup } from '@playwright/test'
import { configureNextcloud, runOcc, runExec } from '@nextcloud/e2e-test-server'
import { PLAYWRIGHT_SHARED_SECRET } from './test-secrets'

type AppList = {
enabled: Record<string, string>
Expand Down Expand Up @@ -105,5 +106,5 @@ setup('Configure Nextcloud', async () => {
await ensureAssistantInstalled()
await runOcc(['app:disable', 'firstrunwizard'])
await runOcc(['config:app:set', 'whiteboard', 'collabBackendUrl', '--value', 'http://localhost:3002'])
await runOcc(['config:app:set', 'whiteboard', 'jwt_secret_key', '--value', 'secret'])
await runOcc(['config:app:set', 'whiteboard', 'jwt_secret_key', '--value', PLAYWRIGHT_SHARED_SECRET])
})
6 changes: 6 additions & 0 deletions playwright/support/test-secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export const PLAYWRIGHT_SHARED_SECRET = 'test-secret-key-with-at-least-32-bytes'
51 changes: 51 additions & 0 deletions tests/Unit/Service/JWTServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Whiteboard\Service;

use OCA\Whiteboard\Consts\JWTConsts;
use OCA\Whiteboard\Exception\UnauthorizedException;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IConfig;

class JWTServiceTest extends \Test\TestCase {
private const SECRET = 'test-secret-key-with-at-least-32-bytes';

private function createJWTService(): JWTService {
$appConfig = $this->createMock(IAppConfig::class);
$appConfig->method('getAppValueString')
->with('jwt_secret_key')
->willReturn(self::SECRET);

$config = $this->createMock(IConfig::class);

return new JWTService(new ConfigService($appConfig, $config));
}

public function testGenerateAndDecodeJWT(): void {
$service = $this->createJWTService();
$payload = [
'userid' => 'alice',
'iat' => time(),
'exp' => time() + JWTConsts::EXPIRATION_TIME,
];

$jwt = $service->generateJWTFromPayload($payload);

self::assertIsString($jwt);
self::assertSame('alice', $service->getUserIdFromJWT($jwt));
}

public function testGetUserIdFromJWTRejectsInvalidToken(): void {
$service = $this->createJWTService();

$this->expectException(UnauthorizedException::class);
$service->getUserIdFromJWT('invalid-token');
}
}
6 changes: 3 additions & 3 deletions tests/integration/multinode-redis.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ const Config = ConfigModule

vi.setConfig({ testTimeout: 30000 })

const waitFor = (socket, event) => {
const waitFor = (socket, event, timeoutMs = 5000) => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const lastError = event === 'connect' && socket?.lastConnectError
? `: ${socket.lastConnectError.message || socket.lastConnectError}`
: ''
reject(new Error(`Timeout waiting for ${event}${lastError}`))
}, 5000)
}, timeoutMs)
socket.once(event, (data) => {
clearTimeout(timer)
resolve(data)
Expand Down Expand Up @@ -463,7 +463,7 @@ describe('Multi node websocket cluster with redis streams', () => {
const followerDesignation = await waitFor(followerSocket, 'sync-designate')
expect(followerDesignation.isSyncer).toBe(false)

const newSyncerNotice = waitFor(followerSocket, 'sync-designate')
const newSyncerNotice = waitFor(followerSocket, 'sync-designate', 10000)
await serverA.gracefulShutdown()
serverA = null

Expand Down
Loading