Files
dckc 860bbee2b5 diigo-cache: public list shows each bookmark's date first
The list is in diigo's updated order; each item now leads with its
updated_at as Date#toISOString() (diigo sends epoch seconds; updatedISO
converts, tested), or "(no date)".

Generated-by: Claude Opus 5.5
2026-10-07 04:19:54 -05:00

84 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { publicOnly, toRows, updatedISO, usernameOf } from "../src/items.js";
import type { Item } from "../src/items.js";
/** a load_user_items row: diigo's actual field names (observed
* 2026-10-07), made-up values */
function diigoItem(linkId: number, url: string): Item {
return {
link_id: linkId,
url,
title: `title ${linkId}`,
description: "",
private: false,
u_name: "alice_99",
first_by: "bob-the-first",
user_id: 4242,
real_name: "Alice A.",
tags: "",
created_at: 1700000000,
updated_at: 1700000100 + linkId,
annotations: [],
comments: [],
};
}
function makeLog() {
const entries: { level: string; args: unknown[] }[] = [];
const at = (level: string) => (...args: unknown[]) => entries.push({ level, args });
return { log: { warn: at("warn") }, entries };
}
describe("toRows", () => {
it("keys each item on diigo's link_id, so a page of distinct items stays distinct", () => {
const { log } = makeLog();
const rows = toRows([diigoItem(11, "https://a.example/"), diigoItem(12, "https://b.example/")], log);
expect(rows.map((r) => r.itemId)).toEqual(["11", "12"]);
});
it("logs, rather than silently drops or merges, items it cannot key", () => {
const { log, entries } = makeLog();
const noId = diigoItem(13, "https://c.example/");
delete noId.link_id;
const noUrl = diigoItem(14, "");
const rows = toRows([noId, noUrl, diigoItem(15, "https://d.example/")], log);
expect(rows.map((r) => r.itemId)).toEqual(["15"]);
expect(entries.map((e) => e.level)).toEqual(["warn", "warn"]);
// diagnostics name the fields present, never their values
expect(JSON.stringify(entries)).not.toContain("https://c.example/");
});
});
describe("usernameOf", () => {
it("reads the owner's username from diigo's u_name", () => {
expect(usernameOf(diigoItem(1, "https://a.example/"))).toBe("alice_99");
});
it("answers empty for an item without one", () => {
expect(usernameOf(undefined)).toBe("");
expect(usernameOf({ u_name: "has spaces" })).toBe("");
});
});
describe("publicOnly", () => {
it("keeps shared rows and drops private ones, before anything is stored", () => {
const { log } = makeLog();
const shared = diigoItem(21, "https://shared.example/");
const secret = { ...diigoItem(22, "https://secret.example/"), private: true };
const rows = publicOnly(toRows([shared, secret], log));
expect(rows.map((r) => r.itemId)).toEqual(["21"]);
expect(JSON.stringify(rows)).not.toContain("secret.example");
});
});
describe("updatedISO", () => {
it("reads diigo's updated_at as epoch seconds", () => {
expect(updatedISO({ updated_at: 1700000000 })).toBe("2023-11-14T22:13:20.000Z");
});
it("answers empty when there is no usable date", () => {
expect(updatedISO({})).toBe("");
expect(updatedISO({ updated_at: "yesterday" })).toBe("");
});
});