cowrite-dev / test /share-link.js
lvwerra's picture
lvwerra HF Staff
Close four holes in the share link, from review
830cb4d
Raw
History Blame Contribute Delete
6.89 kB
// Public share links, driven through a real browser: the owner creates one in
// the Share dialog, and a visitor with NO account opens it.
// node test/share-link.js
// Its own file rather than a block inside browser.js: it has to clear the
// session cookie to become a stranger, and that is a rude thing to do in the
// middle of a suite that stays signed in throughout.
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright-core'
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
const PORT = 3311
const BASE = `http://localhost:${PORT}`
const DATA = path.join(root, '.share-link-data')
const SHOT = process.env.SHOT_DIR || '/tmp/claude-1000/ui-shots'
fs.rmSync(DATA, { recursive: true, force: true })
fs.mkdirSync(SHOT, { recursive: true })
const server = spawn('node', ['server/index.js'], {
cwd: root,
// HOST is what the server puts in the prompt's curl examples; the link the
// dialog shows is built from the origin the browser is actually on
env: { ...process.env, PORT: String(PORT), DATA_DIR: DATA, OAUTH_CLIENT_ID: '', OAUTH_CLIENT_SECRET: '', HOST: BASE },
stdio: ['ignore', 'pipe', 'pipe'],
})
server.stderr.on('data', d => process.stderr.write('[server] ' + d))
const wait = ms => new Promise(r => setTimeout(r, ms))
async function waitFor(fn, what, ms = 15000) {
const t0 = Date.now()
while (Date.now() - t0 < ms) {
try {
if (await fn()) return
} catch {}
await wait(150)
}
throw new Error('timeout: ' + what)
}
async function main() {
await waitFor(async () => (await fetch(`${BASE}/healthz`)).ok, 'server')
const browser = await chromium.launch({
headless: true,
executablePath: process.env.PW_EXECUTABLE || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--single-process'],
})
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } })
const page = await context.newPage()
const consoleErrors = []
page.on('pageerror', e => consoleErrors.push('PAGEERROR: ' + e.message))
page.on('console', m => {
if (m.type() === 'error' && !m.text().includes('fonts.g')) consoleErrors.push(m.text())
})
// --- the owner writes something and publishes a link ---
await page.goto(`${BASE}/auth/dev?u=alice&next=/`)
const doc = await (await page.request.post(`${BASE}/api/docs`, { data: { title: 'Link Shared' } })).json()
await page.goto(`${BASE}/d/${doc.id}`, { waitUntil: 'networkidle' })
await page.waitForSelector('.tiptap')
await page.click('.tiptap h1')
await page.keyboard.press('End')
await page.keyboard.press('Enter')
await page.keyboard.type('Readable by anyone holding the link.')
await wait(1200)
await page.click('#share-btn')
await page.waitForSelector('#share-pop:not(.hidden)')
await page.waitForSelector('#link-share:not(.hidden)')
await page.click('#link-toggle')
await page.waitForSelector('#link-row:not(.hidden)')
await waitFor(async () => /\/p\//.test(await page.inputValue('#link-url')), 'the link appears in the dialog')
const link = await page.inputValue('#link-url')
assert.ok(link.startsWith(BASE), 'the link uses the origin the browser is on: ' + link)
assert.ok(/\/p\/[A-Za-z0-9_-]{32,}$/.test(link), 'long unguessable path: ' + link)
await page.screenshot({ path: path.join(SHOT, 'share-dialog.png') })
console.log('✓ owner creates a link from the Share dialog')
// --- a stranger: no session cookie, no account ---
await context.clearCookies()
await page.goto(link, { waitUntil: 'networkidle' })
await page.waitForSelector('.tiptap')
await waitFor(async () => (await page.textContent('.tiptap')).includes('Readable by anyone holding the link'), 'a visitor reads the document')
const visitor = await page.evaluate(() => ({
editable: window.__editor?.isEditable,
signin: !document.getElementById('signin')?.classList.contains('hidden'),
share: getComputedStyle(document.getElementById('share-btn')).display,
agents: getComputedStyle(document.getElementById('agents-btn')).display,
presence: getComputedStyle(document.getElementById('presence')).display,
whoami: document.getElementById('whoami')?.textContent || '',
}))
assert.equal(visitor.editable, false, 'the document is not editable for a visitor')
assert.equal(visitor.signin, false, 'no sign-in wall for a link visitor')
assert.equal(visitor.share, 'none', 'no Share button')
assert.equal(visitor.agents, 'none', 'no Agents button')
assert.equal(visitor.presence, 'none', 'the presence row is not rendered for a stranger (hidden, not withheld — see app.css)')
assert.match(visitor.whoami, /Read-only/, 'the header says what this is: ' + visitor.whoami)
await page.screenshot({ path: path.join(SHOT, 'share-visitor.png') })
console.log('✓ visitor reads it: no account, no sign-in wall, no write UI')
// typing must change nothing — the socket refuses writes from a link
await page.click('.tiptap p').catch(() => {})
await page.keyboard.type('VANDALISM')
await wait(1500)
await page.goto(`${BASE}/auth/dev?u=alice&next=/d/${doc.id}`, { waitUntil: 'networkidle' })
const after = await (await page.request.get(`${BASE}/api/docs/${doc.id}`)).json()
assert.ok(!after.markdown.includes('VANDALISM'), 'a visitor cannot write: ' + after.markdown.slice(0, 90))
console.log('✓ a visitor typing into it changes nothing')
// checked here: the revoke step below visits a dead link on purpose, and a
// 404 in the console is the correct outcome of that, not a defect
assert.deepEqual(consoleErrors, [], 'no console errors: ' + JSON.stringify(consoleErrors))
console.log('✓ zero console errors')
// --- turning it off closes the door on the link already handed out ---
await page.waitForSelector('.tiptap')
await page.click('#share-btn')
await page.waitForSelector('#link-share:not(.hidden)')
await page.click('#link-toggle')
await waitFor(async () => (await page.textContent('#link-hint')).startsWith('Off'), 'dialog shows the link is off')
await context.clearCookies()
const revoked = await page.goto(link, { waitUntil: 'domcontentloaded' })
assert.equal(revoked.status(), 404, 'a revoked link is a 404')
const stillDead = await page.evaluate(() => !document.getElementById('signin')?.classList.contains('hidden'))
assert.ok(stillDead, 'and it shows the sign-in shell, not the document')
console.log('✓ revoking kills a link that was already handed out')
await browser.close()
server.kill()
fs.rmSync(DATA, { recursive: true, force: true })
console.log('\nSHARE LINK TEST PASSED')
process.exit(0)
}
main().catch(async err => {
console.error('\nSHARE LINK TEST FAILED:', err.message)
server.kill()
process.exit(1)
})