Red: listCollections must drop deleted-collection tombstones
/collections/v2 is a sync API: deleted collections remain in the response forever with isDeleted: true, and their /collections/v2/diff endpoint returns HTTP 404. A long-lived account accumulates hundreds of tombstones, so any caller that iterates listCollections() output (backup, backup-metadata) dies on the first one.
This commit is contained in:
@@ -241,6 +241,40 @@ const buildServer = async (): Promise<ServerState> => {
|
|||||||
updationTime: 1700000000000000,
|
updationTime: 1700000000000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A DELETED collection. /collections/v2 is a sync API: deleted
|
||||||
|
// collections stay in the response forever as tombstones with
|
||||||
|
// isDeleted: true (a long-lived real account accumulates hundreds).
|
||||||
|
// Their keys still decrypt, but /collections/v2/diff returns
|
||||||
|
// HTTP 404 for them, so they must never surface from
|
||||||
|
// listCollections(); a client that naively iterates them dies on
|
||||||
|
// the first deleted album.
|
||||||
|
const deletedKey = sodium.crypto_secretbox_keygen();
|
||||||
|
const dkNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||||
|
const encDeletedKey = sodium.crypto_secretbox_easy(
|
||||||
|
deletedKey,
|
||||||
|
dkNonce,
|
||||||
|
masterKey,
|
||||||
|
);
|
||||||
|
const deletedNameNonce = sodium.randombytes_buf(
|
||||||
|
sodium.crypto_secretbox_NONCEBYTES,
|
||||||
|
);
|
||||||
|
const encDeletedName = sodium.crypto_secretbox_easy(
|
||||||
|
new TextEncoder().encode("Old Album"),
|
||||||
|
deletedNameNonce,
|
||||||
|
deletedKey,
|
||||||
|
);
|
||||||
|
const rawDeletedCollection = {
|
||||||
|
id: 3,
|
||||||
|
owner: { id: 42 },
|
||||||
|
encryptedKey: toBase64(encDeletedKey),
|
||||||
|
keyDecryptionNonce: toBase64(dkNonce),
|
||||||
|
encryptedName: toBase64(encDeletedName),
|
||||||
|
nameDecryptionNonce: toBase64(deletedNameNonce),
|
||||||
|
type: "album",
|
||||||
|
updationTime: 1700000000000000,
|
||||||
|
isDeleted: true,
|
||||||
|
};
|
||||||
|
|
||||||
const rawFile = {
|
const rawFile = {
|
||||||
id: 100,
|
id: 100,
|
||||||
collectionID: 1,
|
collectionID: 1,
|
||||||
@@ -262,6 +296,8 @@ const buildServer = async (): Promise<ServerState> => {
|
|||||||
(globalThis as Record<string, unknown>).__mockRawCollection = rawCollection;
|
(globalThis as Record<string, unknown>).__mockRawCollection = rawCollection;
|
||||||
(globalThis as Record<string, unknown>).__mockRawSharedCollection =
|
(globalThis as Record<string, unknown>).__mockRawSharedCollection =
|
||||||
rawSharedCollection;
|
rawSharedCollection;
|
||||||
|
(globalThis as Record<string, unknown>).__mockRawDeletedCollection =
|
||||||
|
rawDeletedCollection;
|
||||||
(globalThis as Record<string, unknown>).__mockRawFile = rawFile;
|
(globalThis as Record<string, unknown>).__mockRawFile = rawFile;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -341,6 +377,7 @@ const buildMockFetch = (s: ServerState) => {
|
|||||||
collections: [
|
collections: [
|
||||||
g.__mockRawCollection,
|
g.__mockRawCollection,
|
||||||
g.__mockRawSharedCollection,
|
g.__mockRawSharedCollection,
|
||||||
|
g.__mockRawDeletedCollection,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -469,7 +506,11 @@ describe("quak Client usage guide", () => {
|
|||||||
|
|
||||||
const collections = await client.listCollections();
|
const collections = await client.listCollections();
|
||||||
|
|
||||||
|
// The mock server also returns a deleted collection (id 3).
|
||||||
|
// Deleted collections are tombstones in the sync protocol: their
|
||||||
|
// file diff endpoint 404s, so listCollections must drop them.
|
||||||
expect(collections.length).toBe(2);
|
expect(collections.length).toBe(2);
|
||||||
|
expect(collections.find((c) => c.id === 3)).toBeUndefined();
|
||||||
|
|
||||||
const owned = collections.find((c) => c.id === 1)!;
|
const owned = collections.find((c) => c.id === 1)!;
|
||||||
expect(owned.name).toBe("Vacation");
|
expect(owned.name).toBe("Vacation");
|
||||||
|
|||||||
Reference in New Issue
Block a user