Per-user read state and subscriptions
Read, starred and position move to entry_state; subscriptions carry each person's keywords, auto-download, explicit and per-scan limit. The feed list and unread counts are per person, and the existing library is adopted by the admin on first start. The feed URL, folder and schedule stay shared and admin-only: one file serves everyone, so they describe the file rather than a preference. Scanning merges subscribers' wants -- anyone wanting an item is enough -- via merge_policy, which is pure and tested. Also: the test fixture wiped its data directory from every Playwright worker, deleting the database out from under the running daemon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -256,3 +256,47 @@ test('opening an item marks it read, and the toggle flips it back', async ({ pag
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('a second person has their own feeds and their own read state', async ({ browser }) => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const setup = require('./global-setup');
|
||||
const env = {
|
||||
...process.env,
|
||||
IPX_CONFIG: `${setup.root}/config/config.toml`,
|
||||
IPX_DATA_DIR: `${setup.root}/data`,
|
||||
};
|
||||
try {
|
||||
execFileSync('./target/debug/ipx', ['user', 'add', 'sam'], { input: 'sampassword', env });
|
||||
} catch (e) {
|
||||
if (!String(e.stderr || e.stdout).includes('already exists')) throw e;
|
||||
}
|
||||
|
||||
// A fresh context, so none of the admin's cookies come along.
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
await page.goto('/login');
|
||||
await page.locator('#name').fill('sam');
|
||||
await page.locator('#pw').fill('sampassword');
|
||||
await page.locator('button[type=submit]').click();
|
||||
await expect(page.locator('#feedlist')).toBeVisible();
|
||||
|
||||
// Sam subscribes to nothing yet, so sees nothing -- the admin's feeds are not theirs.
|
||||
await expect(page.locator('#feedlist')).toContainText('No feeds.');
|
||||
await expect(page.locator('#prefs')).toBeHidden(); // not an admin
|
||||
|
||||
// Subscribing to a feed the admin already has costs no second fetch: same feed, same
|
||||
// files, but Sam's own read state.
|
||||
await page.locator('#addFeed').click();
|
||||
await page.locator('#nurl').fill('http://127.0.0.1:8792/show.xml');
|
||||
await page.locator('#nsave').click();
|
||||
await expect(page.locator('.feed', { hasText: 'Test Show' })).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||
await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 });
|
||||
// The admin read these earlier in this file; for Sam they are all still unread.
|
||||
const rows = await page.locator('.ep').count();
|
||||
await page.locator('.tabs button', { hasText: 'Unread' }).click();
|
||||
await expect(page.locator('.ep')).toHaveCount(rows);
|
||||
|
||||
await ctx.close();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,15 @@ const TOKEN = 'testtokentesttokentesttoken12345'; // fixed, so tests need not
|
||||
// Called from playwright.config.js at load time, NOT as globalSetup: Playwright starts
|
||||
// webServer *before* globalSetup, so a config written there does not exist yet when the
|
||||
// daemon launches -- it would fall back to the real config and fight the live daemon.
|
||||
// Playwright imports this config again in every worker process, so prepare() runs more
|
||||
// than once per suite. Wiping on the second call deleted the data directory out from under
|
||||
// the running daemon: it kept serving from the unlinked inode, while anything else opening
|
||||
// that path -- the CLI, a query -- got a brand new empty database and disagreed with it.
|
||||
function prepare() {
|
||||
// Only the process that launches the run may wipe. A worker gets TEST_WORKER_INDEX.
|
||||
if (process.env.TEST_WORKER_INDEX !== undefined || process.env.PW_WORKER_INDEX !== undefined) {
|
||||
return;
|
||||
}
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
for (const d of ['config', 'data', 'downloads']) {
|
||||
fs.mkdirSync(path.join(root, d), { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user