feat(immich): ask nothing of a server at the door
Adding a server no longer opens with a tick list: a fresh node keeps every album its key can see — the empty album list the backend stores for "all of them" — and narrowing is a change made on the node that is already there. The album-ID field carries the warnings that were silent before it: a line with no id in it, and an id this key never named, both said where they are typed. A node's key can now be replaced in the dialog that narrows its albums; the backend probes it first, so a key that does not answer leaves the node and the dialog as they were.
This commit is contained in:
@@ -142,21 +142,29 @@ await context.route('**/api/**', (route) => {
|
||||
return send(200, { ok: true, servers: saved.map(view) });
|
||||
}
|
||||
const b = body();
|
||||
if (!b.url || !b.key) return send(400, { ok: false, error: 'invalid' });
|
||||
const box = HOSTS[b.url];
|
||||
// An edit names the node and says only what changes — a new key, or nothing
|
||||
// at all — so what it does not repeat is what the node already has.
|
||||
const at = b.id ? saved.findIndex((s) => s.id === b.id) : -1;
|
||||
if (b.id && at < 0) return send(404, { ok: false, error: 'invalid' });
|
||||
const had = at < 0 ? null : saved[at];
|
||||
const url0 = b.url ?? had?.url;
|
||||
const key0 = b.key ?? (had ? HOSTS[had.url]?.key : undefined);
|
||||
if (!url0 || !key0) return send(400, { ok: false, error: 'invalid' });
|
||||
const box = HOSTS[url0];
|
||||
if (!box) return send(200, { ok: false, error: 'unreachable' });
|
||||
if (box.key !== b.key) return send(200, { ok: false, error: 'auth' });
|
||||
if (box.key !== key0) return send(200, { ok: false, error: 'auth' });
|
||||
const server = {
|
||||
id: `s${++minted}`,
|
||||
name: String(b.name ?? '').trim() || new URL(b.url).hostname,
|
||||
url: b.url,
|
||||
type: b.type === 'share' ? 'share' : 'api',
|
||||
id: had?.id ?? `s${++minted}`,
|
||||
name: String(b.name ?? had?.name ?? '').trim() || new URL(url0).hostname,
|
||||
url: url0,
|
||||
type: (b.type ?? had?.type) === 'share' ? 'share' : 'api',
|
||||
version: box.version,
|
||||
canDownload: box.canDownload,
|
||||
albums: cleanAlbums(b.albums),
|
||||
keyMasked: `••••${String(b.key).slice(-4)}`,
|
||||
albums: had && b.albums === undefined ? had.albums : cleanAlbums(b.albums),
|
||||
keyMasked: `••••${String(key0).slice(-4)}`,
|
||||
};
|
||||
saved.push(server);
|
||||
if (had) saved[at] = server;
|
||||
else saved.push(server);
|
||||
return send(200, { ok: true, server: view(server) });
|
||||
}
|
||||
|
||||
@@ -299,15 +307,31 @@ await page.waitForSelector('[data-key="immich-status"]');
|
||||
const saidOk = await page.$eval('[data-key="immich-status"]', (e) => !e.className.includes('err'));
|
||||
step('a key that answers is said to be good', saidOk);
|
||||
step(
|
||||
'and what it offers is that server’s albums and no other server’s',
|
||||
(await page.$('[data-key="immich-album-' + A1 + '"]')) !== null &&
|
||||
(await page.$('[data-key="immich-album-' + A2 + '"]')) !== null &&
|
||||
(await page.$('[data-key="immich-album-' + B1 + '"]')) === null
|
||||
'and the door asks for nothing else: a fresh server is every album its key can see',
|
||||
(await page.$('[data-key="immich-albums"]')) === null &&
|
||||
(await page.$('[data-key="immich-album-' + A1 + '"]')) === null
|
||||
);
|
||||
// The ID field is the only place an album can be named at the door, and it says
|
||||
// what it made of the lines: one with no id in it, and one this key never named.
|
||||
await page.fill('[data-key="immich-ids"]', `khong-phai-uuid\n${B1}`);
|
||||
await page.waitForSelector('[data-key="immich-bad-ids"]', { timeout: 5_000 }).catch(() => {});
|
||||
step(
|
||||
'a line with no album id in it is a line the field says it dropped',
|
||||
(await page.$('[data-key="immich-bad-ids"]')) !== null &&
|
||||
(await page.$eval('[data-key="immich-bad-ids"]', (e) => e.textContent)).includes('1'),
|
||||
await page.$eval('[data-key="immich-bad-ids"]', (e) => e.textContent).catch(() => 'no warning')
|
||||
);
|
||||
step(
|
||||
'with every one of them ticked to begin with',
|
||||
(await page.isChecked('[data-key="immich-album-' + A1 + '"]')) &&
|
||||
(await page.isChecked('[data-key="immich-album-' + A2 + '"]'))
|
||||
'and an id this key cannot see is an id the field says it does not know',
|
||||
(await page.$('[data-key="immich-unknown-ids"]')) !== null,
|
||||
await page.$eval('[data-key="immich-unknown-ids"]', (e) => e.textContent).catch(() => 'no warning')
|
||||
);
|
||||
await page.fill('[data-key="immich-ids"]', '');
|
||||
await page.waitForTimeout(200);
|
||||
step(
|
||||
'and clearing the field takes both warnings with it',
|
||||
(await page.$('[data-key="immich-bad-ids"]')) === null &&
|
||||
(await page.$('[data-key="immich-unknown-ids"]')) === null
|
||||
);
|
||||
await page.click('[data-key="immich-save"]');
|
||||
await settle('Immich', 5);
|
||||
@@ -365,6 +389,32 @@ step(
|
||||
JSON.stringify(await counts())
|
||||
);
|
||||
|
||||
// --- a key given to a node that is already there -----------------------------
|
||||
// The one place a node's key is replaced: the same dialog that narrows its
|
||||
// albums. It is probed before it is stored, so a key that does not answer is a
|
||||
// dialog still open on a node unchanged.
|
||||
await editDialog('Immich');
|
||||
await page.fill('[data-key="immich-newkey"]', 'not-the-key');
|
||||
await page.click('[data-key="immich-save"]');
|
||||
await page.waitForSelector('[data-key="immich-error"]', { timeout: 5_000 }).catch(() => {});
|
||||
step(
|
||||
'a key given in place of the one it has is refused out loud, dialog still open',
|
||||
(await page.$('[data-key="immich-error"]')) !== null &&
|
||||
(await page.$('[data-key="immich-modal"]')) !== null,
|
||||
await page.$eval('[data-key="immich-error"]', (e) => e.textContent).catch(() => 'no word said')
|
||||
);
|
||||
await page.fill('[data-key="immich-newkey"]', 'good-key');
|
||||
await page.click('[data-key="immich-save"]');
|
||||
await page.waitForSelector('[data-key="immich-modal"]', { state: 'detached', timeout: 10_000 }).catch(() => {});
|
||||
await settle('Immich', 5);
|
||||
await open('Immich');
|
||||
step(
|
||||
'and one that answers is taken, the node and its albums exactly as they were',
|
||||
(await page.$('[data-key="immich-modal"]')) === null &&
|
||||
same(await counts(), { Immich: '5', 'Immich/A1': '2', 'Immich/A2': '3' }),
|
||||
JSON.stringify(await counts())
|
||||
);
|
||||
|
||||
// --- a share link ------------------------------------------------------------
|
||||
await addDialog('https://share.example.com', 'share-xyz', true);
|
||||
step('a share link is not offered an album picker: it is one album', (await page.$('[data-key="immich-albums"]')) === null);
|
||||
|
||||
Reference in New Issue
Block a user