88 lines
5.7 KiB
JavaScript
88 lines
5.7 KiB
JavaScript
// Production Vue component + RPC + actual Nextcloud, not login/install UAT.
|
|
const { chromium, expect, request } = require('@playwright/test')
|
|
const { exerciseNextcloudPagination } = require('./pagination.cjs')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs/promises')
|
|
const base = new URL(process.argv[2])
|
|
assert.equal(base.hostname, '127.0.0.1')
|
|
assert.equal(base.protocol, 'http:')
|
|
;(async () => {
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_EXECUTABLE,
|
|
headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] })
|
|
try {
|
|
const context = await browser.newContext({ acceptDownloads: true, serviceWorkers: 'block' })
|
|
await context.route('**/*', route => new URL(route.request().url()).origin === base.origin ? route.continue() : route.abort())
|
|
await context.routeWebSocket('**/*', socket => socket.close())
|
|
const page = await context.newPage()
|
|
const errors = []
|
|
page.on('pageerror', error => errors.push(error.message))
|
|
await page.goto(base.href)
|
|
await expect(page.getByRole('button', { name: 'Connect a library', exact: true })).toBeVisible()
|
|
const connect = async (login, password) => {
|
|
await page.getByRole('button', { name: 'Connect a library', exact: true }).click()
|
|
await page.getByLabel('Nextcloud login', { exact: true }).fill(login)
|
|
await page.getByLabel('Nextcloud app password', { exact: true }).fill(password)
|
|
await page.getByRole('button', { name: 'Connect', exact: true }).click()
|
|
await expect(page.getByLabel('Library', { exact: true })).toContainText(`Nextcloud · ${login}`)
|
|
await expect(page.getByLabel('Nextcloud app password', { exact: true })).toHaveCount(0)
|
|
}
|
|
await connect('alice', 'Synthetic-Cloud-A-Only-2026!')
|
|
const downloadButton = page.getByRole('button', { name: 'Download contract.txt', exact: true })
|
|
await expect(downloadButton).toBeVisible()
|
|
const pendingDownload = page.waitForEvent('download')
|
|
await downloadButton.click()
|
|
const download = await pendingDownload
|
|
assert.equal(download.suggestedFilename(), 'contract.txt')
|
|
assert.equal(await fs.readFile(await download.path(), 'utf8'), 'abcdefgh')
|
|
await connect('bob', 'Synthetic-Cloud-B-Only-2026!')
|
|
await expect(downloadButton).toHaveCount(0)
|
|
await page.reload()
|
|
await expect(page.getByLabel('Library', { exact: true }).locator('option')).toHaveCount(2)
|
|
await page.getByLabel('Library', { exact: true }).selectOption({ label: 'Nextcloud · alice' })
|
|
await expect(downloadButton).toBeVisible()
|
|
const alice = await page.getByLabel('Library', { exact: true }).inputValue()
|
|
const rpc = (method, params) => page.evaluate(async ({ method, params }) => {
|
|
const csrf = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)[1]
|
|
const r = await fetch('/rpc/v1', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
|
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) })
|
|
if (!r.ok) throw new Error(`RPC HTTP ${r.status}`)
|
|
const body = await r.json()
|
|
if (body.error) throw new Error(body.error.message)
|
|
return body.result
|
|
}, { method, params })
|
|
await exerciseNextcloudPagination({ page, request, expect, connectionId: alice, rpc })
|
|
const listing = await rpc('cloud.sources.browse', { connection_id: alice, folder: [], page: 1 })
|
|
const item = listing.items.find(item => item.name === 'contract.txt')
|
|
assert.ok(item)
|
|
const { url } = await rpc('cloud.sources.prepare-stream', { connection_id: alice, upstream_id: item.upstream_id,
|
|
revision: item.revision, locator: item.locator, download: true })
|
|
const range = await context.request.get(new URL(url, base).href, { headers: { Range: 'bytes=2-5' } })
|
|
assert.equal(range.status(), 206)
|
|
assert.equal(range.headers()['content-range'], 'bytes 2-5/8')
|
|
assert.equal(await range.text(), 'cdef')
|
|
const foreign = await context.request.get(new URL(url, base).href, { headers: { Origin: 'http://other.invalid' } })
|
|
assert.equal(foreign.status(), 403)
|
|
const unauth = await browser.newContext()
|
|
const denied = await unauth.request.get(new URL(url, base).href)
|
|
assert.equal(denied.status(), 401)
|
|
const deniedRpc = await unauth.request.post(new URL('/rpc/v1', base).href, { data: { jsonrpc: '2.0', id: 1, method: 'cloud.sources.list' } })
|
|
assert.equal(deniedRpc.status(), 401)
|
|
await unauth.close()
|
|
const badCsrf = await context.request.post(new URL('/rpc/v1', base).href, {
|
|
headers: { Origin: base.origin, 'X-CSRF-Token': 'invalid-synthetic' },
|
|
data: { jsonrpc: '2.0', id: 1, method: 'cloud.sources.disconnect', params: { connection_id: alice } },
|
|
})
|
|
assert.equal(badCsrf.status(), 403)
|
|
await page.getByRole('button', { name: 'Disconnect', exact: true }).click()
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Disconnect', exact: true }).click()
|
|
await expect(page.getByLabel('Library', { exact: true }).locator('option')).toHaveCount(1)
|
|
await expect(page.getByLabel('Library', { exact: true })).toContainText('Nextcloud · bob')
|
|
const revoked = await context.request.get(new URL(url, base).href)
|
|
assert.equal(revoked.status(), 409)
|
|
assert.ok((await rpc('cloud.sources.browse', { connection_id: await page.getByLabel('Library', { exact: true }).inputValue(), folder: [], page: 1 })).items)
|
|
assert.deepEqual(errors, [])
|
|
console.log('PASS production Cloud UI→RPC→Nextcloud: connect two accounts, exact download/range, reload, session/origin/CSRF denial, disconnect revocation and other-account survival; no File Browser')
|
|
await context.close()
|
|
} finally { await browser.close() }
|
|
})().catch(error => { console.error(error); process.exitCode = 1 })
|