diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml
index 0c62487c..95636ed1 100644
--- a/.github/workflows/node-test.yml
+++ b/.github/workflows/node-test.yml
@@ -51,6 +51,8 @@ jobs:
CYPRESS_INSTALL_BINARY: 0
run: |
npm ci
+ # Install browsers for tests
+ npx playwright install chromium
npm run build --if-present
- name: Test
diff --git a/.gitignore b/.gitignore
index efa17b24..33fa2547 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,13 +6,16 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-
# Runtime data
pids
*.pid
*.seed
*.pid.lock
+# Tests
+.vitest*
+__screenshots__/
+
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
diff --git a/REUSE.toml b/REUSE.toml
index 6a77595a..481e25f8 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud GmbH "
SPDX-PackageDownloadLocation = "https://github.com/nextcloud-libraries/nextcloud-auth"
[[annotations]]
-path = ["package.json", "package-lock.json", "tsconfig.json"]
+path = ["package.json", "package-lock.json", "tsconfig.json", "test/tsconfig.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"
diff --git a/lib/globals.d.ts b/globals.d.ts
similarity index 70%
rename from lib/globals.d.ts
rename to globals.d.ts
index 8ec15dde..d6328bce 100644
--- a/lib/globals.d.ts
+++ b/globals.d.ts
@@ -4,6 +4,9 @@
*/
declare global {
+ // eslint-disable-next-line camelcase
+ var _nc_auth_requestToken: string | undefined
+
interface Window {
_oc_isadmin?: boolean
}
diff --git a/lib/csp-nonce.ts b/lib/csp-nonce.ts
index 29831cea..ec8853fd 100644
--- a/lib/csp-nonce.ts
+++ b/lib/csp-nonce.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { getRequestToken } from './requesttoken.ts'
+import { getRequestToken } from './requestToken.ts'
/**
* Get the CSP nonce for script loading
@@ -18,7 +18,7 @@ import { getRequestToken } from './requesttoken.ts'
*/
export function getCSPNonce(): string | undefined {
const meta = document?.querySelector('meta[name="csp-nonce"]')
- // backwards compatibility with older Nextcloud versions
+ // backwards compatibility with older Nextcloud versions (before Nextcloud 30)
if (!meta) {
const token = getRequestToken()
return token ? btoa(token) : undefined
diff --git a/lib/eventbus.d.ts b/lib/eventbus.d.ts
index cbd674c4..45ed99bb 100644
--- a/lib/eventbus.d.ts
+++ b/lib/eventbus.d.ts
@@ -7,6 +7,7 @@ import type { NextcloudUser } from './user.ts'
declare module '@nextcloud/event-bus' {
export interface NextcloudEvents {
+ 'csrf-token-update': { token: string, _internal?: true }
// mapping of 'event name' => 'event type'
'user:info:changed': NextcloudUser
}
diff --git a/lib/guest.ts b/lib/guest.ts
index 34fa9bee..2065a6bd 100644
--- a/lib/guest.ts
+++ b/lib/guest.ts
@@ -74,6 +74,17 @@ export function setGuestNickname(nickname: string): void {
getGuestUser().displayName = nickname
}
+/**
+ * Reset the guest user state.
+ *
+ * @internal
+ */
+export function resetGuestUser(): void {
+ currentUser = undefined
+ browserStorage.removeItem('guestUid')
+ browserStorage.removeItem('guestNickname')
+}
+
/**
* Generate a random UUID (version 4) if the crypto API is not available.
* If the crypto API is available, it uses the less secure `randomUUID` method.
diff --git a/lib/index.ts b/lib/index.ts
index bc6dea4c..cfcfd013 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-export type { CsrfTokenObserver } from './requesttoken.ts'
+export type { CsrfTokenObserver } from './requestToken.ts'
export type { NextcloudUser } from './user.ts'
export { getCSPNonce } from './csp-nonce.ts'
export { getGuestNickname, getGuestUser, setGuestNickname } from './guest.ts'
-export { getRequestToken, onRequestTokenUpdate } from './requesttoken.ts'
+export { fetchRequestToken, getRequestToken, onRequestTokenUpdate, setRequestToken } from './requestToken.ts'
export { getCurrentUser } from './user.ts'
diff --git a/lib/requestToken.ts b/lib/requestToken.ts
new file mode 100644
index 00000000..073e8350
--- /dev/null
+++ b/lib/requestToken.ts
@@ -0,0 +1,115 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { generateUrl } from '@nextcloud/router'
+
+export interface CsrfTokenObserver {
+ (token: string): void
+}
+
+_subscribeToTokenUpdates() // TODO: remove once we drop support for Nextcloud 33 and before
+
+/**
+ * Get current request token
+ *
+ * @return Current request token or null if not set
+ */
+export function getRequestToken(): string | null {
+ if (globalThis._nc_auth_requestToken) {
+ return globalThis._nc_auth_requestToken
+ }
+
+ if (globalThis.document) {
+ // for service workers or other contexts without DOM we need to safeguard this
+ return document.head.dataset.requesttoken ?? null
+ }
+ return null
+}
+
+/**
+ * Set a new CSRF token (e.g. because of session refresh).
+ * This also emits an event bus event for the updated token.
+ *
+ * @param token - The new token
+ * @throws {Error} - If the passed token is not a potential valid token
+ */
+export function setRequestToken(token: string): void {
+ if (!token || typeof token !== 'string') {
+ throw new Error('Invalid CSRF token given', { cause: { token } })
+ }
+
+ if (globalThis._nc_auth_requestToken === token) {
+ // token is the same as before, no need to update and especially no need to notify the observers
+ return
+ }
+
+ globalThis._nc_auth_requestToken = token
+ if (globalThis.document) {
+ // For DOM environments we also set the token to the DOM, so it is available for legacy code
+ document.head.dataset.requesttoken = token
+ }
+
+ emit('csrf-token-update', { token, _internal: true })
+}
+
+/**
+ * Fetch the request token from the API.
+ * This does also set it on the current context, see `setRequestToken`.
+ *
+ * @throws {Error} - If the request failed
+ */
+export async function fetchRequestToken(): Promise {
+ const url = generateUrl('/csrftoken')
+
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error('Could not fetch CSRF token from API', { cause: response })
+ }
+
+ try {
+ const { token } = await response.json()
+ setRequestToken(token)
+ return token
+ } catch (error) {
+ throw new Error('Could not parse CSRF token from API response', { cause: error })
+ }
+}
+
+/**
+ * Add an observer which is called when the CSRF token changes
+ *
+ * @param observer The observer
+ * @return A function to unsubscribe the observer
+ */
+export function onRequestTokenUpdate(observer: CsrfTokenObserver): () => void {
+ const wrapper = async ({ token }: { token: string }) => {
+ try {
+ observer(token)
+ } catch (error) {
+ // we cannot use the logger as the logger uses this library = circular dependency
+ // eslint-disable-next-line no-console
+ console.error('Error updating CSRF token observer', error)
+ }
+ }
+
+ subscribe('csrf-token-update', wrapper)
+ return () => unsubscribe('csrf-token-update', wrapper)
+}
+
+/**
+ * Subscribe to token update events from server.
+ *
+ * @todo - This is legacy and not needed once all supported server versions use `setRequestToken` of this library.
+ */
+function _subscribeToTokenUpdates(): void {
+ // Listen to server event and keep token in sync
+ subscribe('csrf-token-update', ({ token, _internal }) => {
+ if (!_internal) {
+ // Only update the token if the event is not emitted from this library, otherwise we would end in a loop
+ setRequestToken(token)
+ }
+ })
+}
diff --git a/lib/requesttoken.ts b/lib/requesttoken.ts
deleted file mode 100644
index 38cebe37..00000000
--- a/lib/requesttoken.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { subscribe } from '@nextcloud/event-bus'
-
-export interface CsrfTokenObserver {
- (token: string): void
-}
-
-let token: string | null | undefined
-const observers: CsrfTokenObserver[] = []
-
-/**
- * Get current request token
- *
- * @return Current request token or null if not set
- */
-export function getRequestToken(): string | null {
- if (token === undefined) {
- // Only on first load, try to get token from document
- token = document.head.dataset.requesttoken ?? null
- }
- return token
-}
-
-/**
- * Add an observer which is called when the CSRF token changes
- *
- * @param observer The observer
- */
-export function onRequestTokenUpdate(observer: CsrfTokenObserver): void {
- observers.push(observer)
-}
-
-// Listen to server event and keep token in sync
-subscribe('csrf-token-update', (e: unknown) => {
- token = (e as { token: string }).token
-
- observers.forEach((observer) => {
- try {
- observer(token!)
- } catch (error) {
- // we cannot use the logger as the logger uses this library = circular dependency
- // eslint-disable-next-line no-console
- console.error('Error updating CSRF token observer', error)
- }
- })
-})
diff --git a/package-lock.json b/package-lock.json
index fbd1d4ff..b6eac7d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,15 +10,17 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@nextcloud/browser-storage": "^0.5.0",
- "@nextcloud/event-bus": "^3.3.3"
+ "@nextcloud/event-bus": "^3.3.3",
+ "@nextcloud/router": "^3.1.0"
},
"devDependencies": {
"@nextcloud/eslint-config": "^9.0.0-rc.9",
"@nextcloud/typings": "^1.10.0",
"@nextcloud/vite-config": "^2.5.2",
+ "@vitest/browser-playwright": "^4.1.4",
"@vitest/coverage-v8": "^4.1.4",
"eslint": "^10.2.0",
- "happy-dom": "^20.8.9",
+ "msw": "^2.13.2",
"typedoc": "^0.28.18",
"typescript": "^6.0.2",
"vite": "^7.3.2",
@@ -88,6 +90,13 @@
"node": ">=18"
}
},
+ "node_modules/@blazediff/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@es-joy/jsdoccomment": {
"version": "0.86.0",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz",
@@ -844,6 +853,94 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@inquirer/ansi": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
+ "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.21",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
+ "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
+ "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
+ "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
+ "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -1023,6 +1120,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.41.3",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz",
+ "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@nextcloud/browser-storage": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.5.0.tgz",
@@ -1126,11 +1241,22 @@
"node": "^20 || ^22 || ^24"
}
},
+ "node_modules/@nextcloud/router": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz",
+ "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==",
+ "license": "GPL-3.0-or-later",
+ "dependencies": {
+ "@nextcloud/typings": "^1.10.0"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
+ }
+ },
"node_modules/@nextcloud/typings": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz",
"integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==",
- "dev": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@types/jquery": "3.5.16"
@@ -1168,6 +1294,31 @@
"vite": "^7.1.10"
}
},
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
@@ -1492,6 +1643,13 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.29",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
@@ -2235,7 +2393,6 @@
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
@@ -2248,16 +2405,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/node": {
- "version": "20.19.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz",
- "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
"node_modules/@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
@@ -2268,6 +2415,12 @@
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/statuses": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
+ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
"dev": true,
"license": "MIT"
},
@@ -2278,23 +2431,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/whatwg-mimetype": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
- "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
@@ -2594,6 +2730,53 @@
"vue": "^3.2.25"
}
},
+ "node_modules/@vitest/browser": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.4.tgz",
+ "integrity": "sha512-TrNaY/yVOwxtrxNsDUC/wQ56xSwplpytTeRAqF/197xV/ZddxxulBsxR6TrhVMyniJmp9in8d5u0AcDaNRY30w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@blazediff/core": "1.9.1",
+ "@vitest/mocker": "4.1.4",
+ "@vitest/utils": "4.1.4",
+ "magic-string": "^0.30.21",
+ "pngjs": "^7.0.0",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.1.0",
+ "ws": "^8.19.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "4.1.4"
+ }
+ },
+ "node_modules/@vitest/browser-playwright": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.4.tgz",
+ "integrity": "sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/browser": "4.1.4",
+ "@vitest/mocker": "4.1.4",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "vitest": "4.1.4"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": false
+ }
+ }
+ },
"node_modules/@vitest/coverage-v8": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz",
@@ -3017,6 +3200,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/are-docs-informative": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
@@ -3546,6 +3755,69 @@
"node": ">= 0.10"
}
},
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/comment-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz",
@@ -3597,6 +3869,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
@@ -3942,6 +4228,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -4668,6 +4961,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4763,35 +5066,14 @@
"dev": true,
"license": "ISC"
},
- "node_modules/happy-dom": {
- "version": "20.8.9",
- "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz",
- "integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==",
+ "node_modules/graphql": {
+ "version": "16.13.2",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz",
+ "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@types/node": ">=20.0.0",
- "@types/whatwg-mimetype": "^3.0.2",
- "@types/ws": "^8.18.1",
- "entities": "^7.0.1",
- "whatwg-mimetype": "^3.0.0",
- "ws": "^8.18.3"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/happy-dom/node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "dev": true,
- "license": "BSD-2-Clause",
"engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/has-flag": {
@@ -4894,6 +5176,13 @@
"he": "bin/he"
}
},
+ "node_modules/headers-polyfill": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -5059,6 +5348,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
@@ -5109,6 +5408,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -5614,6 +5920,16 @@
"node": "*"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5621,6 +5937,51 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/msw": {
+ "version": "2.13.2",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.13.2.tgz",
+ "integrity": "sha512-go2H1TIERKkC48pXiwec5l6sbNqYuvqOk3/vHGo1Zd+pq/H63oFawDQerH+WQdUw/flJFHDG7F+QdWMwhntA/A==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/confirm": "^5.0.0",
+ "@mswjs/interceptors": "^0.41.2",
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@types/statuses": "^2.0.6",
+ "cookie": "^1.0.2",
+ "graphql": "^16.12.0",
+ "headers-polyfill": "^4.0.2",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "path-to-regexp": "^6.3.0",
+ "picocolors": "^1.1.1",
+ "rettime": "^0.10.1",
+ "statuses": "^2.0.2",
+ "strict-event-emitter": "^0.5.1",
+ "tough-cookie": "^6.0.0",
+ "type-fest": "^5.2.0",
+ "until-async": "^3.0.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.8.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -5628,6 +5989,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5843,6 +6214,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -5979,6 +6357,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -6049,6 +6434,66 @@
"pathe": "^2.0.3"
}
},
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6264,6 +6709,16 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -6308,6 +6763,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/rettime": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz",
+ "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ripemd160": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
@@ -6737,6 +7199,34 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/sort-object-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.1.0.tgz",
@@ -6899,6 +7389,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/std-env": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
@@ -6930,6 +7430,13 @@
"xtend": "^4.0.2"
}
},
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -6950,6 +7457,34 @@
"node": ">=0.6.19"
}
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -7002,6 +7537,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tagged-tag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
+ "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/timers-browserify": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
@@ -7059,6 +7607,26 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.0.28",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
+ "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.28"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.28",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz",
+ "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -7106,6 +7674,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -7139,6 +7730,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-fest": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz",
+ "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "dependencies": {
+ "tagged-tag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -7269,13 +7876,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -7286,6 +7886,16 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/until-async": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
+ "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/kettanaito"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -8117,16 +8727,6 @@
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
}
},
- "node_modules/whatwg-mimetype": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
- "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8192,6 +8792,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
@@ -8234,6 +8849,16 @@
"node": ">=0.4"
}
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -8257,6 +8882,35 @@
"url": "https://github.com/sponsors/eemeli"
}
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -8269,6 +8923,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+ "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index adbdbb8d..364d93fc 100644
--- a/package.json
+++ b/package.json
@@ -36,15 +36,17 @@
},
"dependencies": {
"@nextcloud/browser-storage": "^0.5.0",
- "@nextcloud/event-bus": "^3.3.3"
+ "@nextcloud/event-bus": "^3.3.3",
+ "@nextcloud/router": "^3.1.0"
},
"devDependencies": {
"@nextcloud/eslint-config": "^9.0.0-rc.9",
"@nextcloud/typings": "^1.10.0",
"@nextcloud/vite-config": "^2.5.2",
+ "@vitest/browser-playwright": "^4.1.4",
"@vitest/coverage-v8": "^4.1.4",
"eslint": "^10.2.0",
- "happy-dom": "^20.8.9",
+ "msw": "^2.13.2",
"typedoc": "^0.28.18",
"typescript": "^6.0.2",
"vite": "^7.3.2",
diff --git a/test/csp-nonce.test.ts b/test/csp-nonce.test.ts
index e68daf1c..94458d7a 100644
--- a/test/csp-nonce.test.ts
+++ b/test/csp-nonce.test.ts
@@ -2,14 +2,15 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { randomBytes } from 'crypto'
+
import { beforeEach, describe, expect, test, vi } from 'vitest'
/**
* Mock `` element with nonce
*/
function mockNonce() {
- const nonce = randomBytes(16).toString('base64')
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))
+ const nonce = bytes.toBase64()
const el = document.createElement('meta')
el.name = 'csp-nonce'
el.nonce = nonce
diff --git a/test/guest.test.ts b/test/guest.test.ts
index d018ac9e..411c46dc 100644
--- a/test/guest.test.ts
+++ b/test/guest.test.ts
@@ -3,82 +3,47 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { getBuilder } from '@nextcloud/browser-storage'
import { emit } from '@nextcloud/event-bus'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-// Mock dependencies
-vi.mock('@nextcloud/browser-storage')
-vi.mock('@nextcloud/event-bus')
-
-let tmpBrowserStorage = {}
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { getGuestUser, resetGuestUser } from '../lib/guest.ts'
// Mock browser storage
-const mockBrowserStorage = {
+let tmpBrowserStorage: Record = {}
+const mockBrowserStorage = vi.hoisted(() => ({
getItem: vi.fn((key) => tmpBrowserStorage[key]),
setItem: vi.fn((key, value) => { tmpBrowserStorage[key] = value }),
removeItem: vi.fn((key) => { delete tmpBrowserStorage[key] }),
-}
+}))
-// Mock crypto for UUID generation
-const originalCrypto = globalThis.crypto
-const mockCrypto = {
- randomUUID: vi.fn(() => 'mock-uuid-' + Math.random().toString(36).slice(2, 10)),
-}
+// Mock dependencies
+vi.mock('@nextcloud/event-bus', { spy: true })
+vi.mock('@nextcloud/browser-storage', () => ({
+ getBuilder: vi.fn(() => ({
+ persist: () => ({ build: () => mockBrowserStorage }),
+ })),
+}))
describe('Guest User Module', () => {
beforeEach(() => {
// Setup mocks
vi.clearAllMocks()
- vi.resetModules()
+ resetGuestUser()
// Clear temporary browser storage
tmpBrowserStorage = {}
-
- // Mock getBuilder to return our mockBrowserStorage
- vi.mocked(getBuilder).mockReturnValue({
- persist: () => ({
- // @ts-expect-error Mocking builder
- build: () => mockBrowserStorage,
- }),
- })
-
- // Replace globalThis crypto with mock
- Object.defineProperty(globalThis, 'crypto', {
- value: mockCrypto,
- writable: true,
- })
- })
-
- afterEach(() => {
- // Restore original crypto
- Object.defineProperty(globalThis, 'crypto', {
- value: originalCrypto,
- writable: true,
- })
})
describe('getGuestUser', () => {
it('should create a new guest user with default values when no storage exists', async () => {
- const { getGuestUser } = await import('../lib/index.ts')
const guestUser = getGuestUser()
- const uid = guestUser.uid
-
- expect(guestUser.uid).toBeTruthy()
+ expect(guestUser.uid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
expect(guestUser.displayName).toBe('')
expect(guestUser.isAdmin).toBe(false)
expect(mockBrowserStorage.getItem).toHaveBeenNthCalledWith(1, 'guestUid')
expect(mockBrowserStorage.getItem).toHaveBeenNthCalledWith(2, 'guestNickname')
- expect(mockBrowserStorage.setItem).toHaveBeenCalledWith('guestUid', uid)
-
expect(mockBrowserStorage.getItem).toHaveBeenNthCalledWith(3, 'guestUid')
-
- expect(guestUser.uid).toBe(uid)
- expect(guestUser.displayName).toBe('')
- expect(guestUser.isAdmin).toBe(false)
-
- expect(mockCrypto.randomUUID).toHaveBeenCalledOnce()
+ expect(mockBrowserStorage.setItem).toHaveBeenCalledWith('guestUid', guestUser.uid)
})
it('should return the existing guest user if already created', async () => {
diff --git a/test/request-token.test.ts b/test/request-token.test.ts
deleted file mode 100644
index ba375e54..00000000
--- a/test/request-token.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { emit } from '@nextcloud/event-bus'
-import { beforeEach, describe, expect, test, vi } from 'vitest'
-
-describe('request token', () => {
- beforeEach(() => {
- vi.resetModules()
- vi.resetAllMocks()
- delete document.head.dataset.requesttoken
- })
-
- test('return null if no token found', async () => {
- const { getRequestToken } = await import('../lib/index.ts')
- expect(getRequestToken()).toBe(null)
- })
-
- test('read initial token', async () => {
- document.head.dataset.requesttoken = 'random-token'
- const { getRequestToken } = await import('../lib/index.ts')
- expect(getRequestToken()).toBe('random-token')
- })
-
- test('can update token by event', async () => {
- const { getRequestToken } = await import('../lib/index.ts')
-
- emit('csrf-token-update', {
- token: 'token123',
- })
-
- expect(getRequestToken()).toBe('token123')
- })
-
- test('request token observer is called', async () => {
- const { onRequestTokenUpdate } = await import('../lib/index.ts')
- const observer = vi.fn(() => { })
-
- onRequestTokenUpdate(observer)
- emit('csrf-token-update', {
- token: 'token123',
- })
-
- expect(observer.mock.calls.length).toBe(1)
- })
-
- test('handle exception in observer', async () => {
- const spy = vi.spyOn(window.console, 'error')
- const { onRequestTokenUpdate } = await import('../lib/index.ts')
- const observer = vi.fn(() => {
- throw new Error('!Error!')
- })
- // silence the console
- spy.mockImplementationOnce(() => {})
-
- onRequestTokenUpdate(observer)
- emit('csrf-token-update', {
- token: 'token123',
- })
-
- expect(observer.mock.calls.length).toBe(1)
- expect(spy).toHaveBeenCalledOnce()
- })
-})
diff --git a/test/requestToken-observers.test.ts b/test/requestToken-observers.test.ts
new file mode 100644
index 00000000..14140844
--- /dev/null
+++ b/test/requestToken-observers.test.ts
@@ -0,0 +1,83 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { emit } from '@nextcloud/event-bus'
+import { beforeEach, describe, expect, it, test, vi } from 'vitest'
+
+test('it does set the new token form legacy event', async () => {
+ const { getRequestToken } = await import('../lib/requestToken.ts')
+ mockToken('old-token')
+
+ // older server versions emitted the event directly without using this libary
+ emit('csrf-token-update', { token: 'new-token' })
+ expect(getRequestToken()).toBe('new-token')
+})
+
+describe('request token observers', () => {
+ beforeEach(() => {
+ vi.resetModules()
+ vi.resetAllMocks()
+ mockToken(undefined)
+ })
+
+ it('request token observer is called', async () => {
+ const { onRequestTokenUpdate, setRequestToken } = await import('../lib/requestToken.ts')
+ const observer = vi.fn(() => { })
+
+ onRequestTokenUpdate(observer)
+ expect(observer).not.toHaveBeenCalled()
+
+ setRequestToken('token123')
+ expect(observer).toHaveBeenCalledTimes(1)
+ expect(observer).toHaveBeenCalledWith('token123')
+ })
+
+ it('request token observer can be unsubscribed', async () => {
+ const { onRequestTokenUpdate, setRequestToken } = await import('../lib/requestToken.ts')
+ const observer = vi.fn(() => { })
+
+ const unsubscribe = onRequestTokenUpdate(observer)
+ expect(observer).not.toHaveBeenCalled()
+
+ setRequestToken('token123')
+ expect(observer).toHaveBeenCalledTimes(1)
+ expect(observer).toHaveBeenCalledWith('token123')
+
+ unsubscribe()
+ setRequestToken('token456')
+ expect(observer).toHaveBeenCalledTimes(1) // Should not be called again
+ })
+
+ it('handle exception in observer', async () => {
+ const { onRequestTokenUpdate, setRequestToken } = await import('../lib/requestToken.ts')
+ const spy = vi.spyOn(window.console, 'error')
+ const observer = vi.fn(() => {
+ throw new Error('!Error!')
+ })
+ // silence the console
+ spy.mockImplementationOnce(() => {})
+
+ onRequestTokenUpdate(observer)
+ setRequestToken('token123')
+
+ expect(observer).toHaveBeenCalledTimes(1)
+ expect(spy).toHaveBeenCalledOnce()
+ })
+})
+
+/**
+ * Mock the request token directly so we can it reading it.
+ *
+ * @param token - The CSRF token to mock
+ */
+function mockToken(token?: string) {
+ if (token === undefined) {
+ delete document.head.dataset.requesttoken
+ delete globalThis._nc_auth_requestToken
+ } else {
+ document.head.dataset.requesttoken = token
+ globalThis._nc_auth_requestToken = token
+ }
+}
diff --git a/test/requestToken.test.ts b/test/requestToken.test.ts
new file mode 100644
index 00000000..fcfff28c
--- /dev/null
+++ b/test/requestToken.test.ts
@@ -0,0 +1,165 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import * as eventbus from '@nextcloud/event-bus'
+import { http, HttpResponse } from 'msw'
+import { setupWorker } from 'msw/browser'
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+import { fetchRequestToken, getRequestToken, setRequestToken } from '../lib/requestToken.ts'
+
+vi.mock('@nextcloud/event-bus', { spy: true })
+
+const server = setupWorker()
+
+describe('getRequestToken', () => {
+ it('can read the token from DOM', () => {
+ mockToken('tokenmock-123', undefined)
+ expect(getRequestToken()).toBe('tokenmock-123')
+ })
+
+ it('can handle missing token', () => {
+ mockToken(undefined)
+ expect(getRequestToken()).toBeNull()
+ })
+
+ it('can handle cache token', () => {
+ mockToken('cached-token')
+ expect(getRequestToken()).toBe('cached-token')
+ })
+
+ it('prioritizes cached token', () => {
+ mockToken('dom-token', 'cached-token')
+ expect(getRequestToken()).toBe('cached-token')
+ })
+})
+
+describe('setRequestToken', () => {
+ beforeEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('does emit an event on change', () => {
+ setRequestToken('new-token')
+ expect(eventbus.emit).toHaveBeenCalledTimes(1)
+ expect(eventbus.emit).toHaveBeenCalledWith('csrf-token-update', expect.objectContaining({ token: 'new-token' }))
+ })
+
+ it('does set the new token to the DOM', () => {
+ setRequestToken('new-token')
+ expect(document.head.dataset.requesttoken).toBe('new-token')
+ })
+
+ it('does remember the new token', () => {
+ mockToken('old-token')
+ setRequestToken('new-token')
+ expect(getRequestToken()).toBe('new-token')
+ })
+
+ it('throws if the token is not a string', () => {
+ // @ts-expect-error it invalid values
+ expect(() => setRequestToken(123)).toThrowError('Invalid CSRF token given')
+ })
+
+ it('throws if the token is not valid', () => {
+ expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given')
+ })
+
+ it('does not emit an event if the token is not valid', () => {
+ expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given')
+ expect(eventbus.emit).not.toBeCalled()
+ })
+})
+
+describe('fetchRequestToken', () => {
+ const successfullCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json({ token: 'new-token' })
+ })
+ const forbiddenCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json([], { status: 403 })
+ })
+ const serverErrorCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json([], { status: 500 })
+ })
+ const networkErrorCsrf = http.get('/index.php/csrftoken', () => {
+ return new HttpResponse(null, { type: 'error' })
+ })
+
+ beforeAll(async () => {
+ // @ts-expect-error - mocking global variable for testing
+ globalThis._oc_webroot = ''
+ await server.start()
+ })
+
+ beforeEach(() => {
+ vi.resetAllMocks()
+ mockToken('oldToken')
+ })
+
+ it('correctly parses response', async () => {
+ server.resetHandlers(successfullCsrf)
+
+ const token = await fetchRequestToken()
+ expect(token).toBe('new-token')
+ })
+
+ it('sets the token', async () => {
+ server.use(successfullCsrf)
+
+ await fetchRequestToken()
+ expect(getRequestToken()).toBe('new-token')
+ })
+
+ it('does emit an event', async () => {
+ server.use(successfullCsrf)
+
+ await fetchRequestToken()
+ expect(eventbus.emit).toHaveBeenCalledOnce()
+ expect(eventbus.emit).toHaveBeenCalledWith('csrf-token-update', expect.objectContaining({ token: 'new-token' }))
+ })
+
+ it('handles 403 error due to invalid cookies', async () => {
+ server.use(forbiddenCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrow('Could not fetch CSRF token from API')
+ expect(getRequestToken()).toBe('oldToken')
+ })
+
+ it('handles server error', async () => {
+ server.use(serverErrorCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrow('Could not fetch CSRF token from API')
+ expect(getRequestToken()).toBe('oldToken')
+ })
+
+ it('handles network error', async () => {
+ server.use(networkErrorCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrow()
+ expect(getRequestToken()).toBe('oldToken')
+ })
+})
+
+/**
+ * Mock the request token directly so we can it reading it.
+ *
+ * @param token - The CSRF token to mock
+ * @param internalToken - The internal (cached version of the DOM token) token to mock, if null the `token` will be used as internal token
+ */
+function mockToken(token?: string, internalToken: string | undefined | null = null) {
+ if (token === undefined) {
+ delete document.head.dataset.requesttoken
+ } else {
+ document.head.dataset.requesttoken = token
+ }
+
+ if (internalToken === null) {
+ globalThis._nc_auth_requestToken = token
+ } else {
+ globalThis._nc_auth_requestToken = internalToken
+ }
+}
diff --git a/test/tsconfig.json b/test/tsconfig.json
new file mode 100644
index 00000000..f532f482
--- /dev/null
+++ b/test/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "include": [".", "../lib", "../*.d.ts"],
+ "compilerOptions": {
+ "rootDir": ".."
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index dc956383..6a9af061 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,5 @@
{
- "include": ["./lib"],
+ "include": ["./lib", "./*.d.ts"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
@@ -10,6 +10,7 @@
"lib": ["DOM", "ESNext"],
"declaration": true,
"strict": true,
+ "rootDir": "./lib",
"outDir": "./dist"
}
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 96d330d7..6ba53310 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,22 +1,23 @@
-/**
+/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import type { UserConfig } from 'vite'
+import { playwright } from '@vitest/browser-playwright'
+import { defineConfig } from 'vitest/config'
-import viteConfig from './vite.config'
-
-export default async (env) => {
- const config = typeof viteConfig === 'function' ? await viteConfig(env) : viteConfig
-
- return {
- ...config,
- test: {
- environment: 'happy-dom',
- coverage: {
- reporter: ['text', 'lcov'],
- },
+export default defineConfig({
+ test: {
+ browser: {
+ enabled: true,
+ headless: true,
+ provider: playwright(),
+ instances: [
+ { browser: 'chromium' },
+ ],
+ },
+ coverage: {
+ reporter: ['text', 'lcov'],
},
- } as UserConfig
-}
+ },
+})