Merge: skip deleted-collection tombstones in listCollections
This commit is contained in:
@@ -155,7 +155,11 @@ export class Client {
|
||||
const { collections } = await this.api.getJSON<{
|
||||
collections: RawCollection[];
|
||||
}>("/collections/v2", { sinceTime: 0 });
|
||||
return collections.map((raw) =>
|
||||
// The sync API keeps returning deleted collections as tombstones
|
||||
// (isDeleted: true); their diff endpoint 404s, so drop them.
|
||||
return collections
|
||||
.filter((raw) => !raw.isDeleted)
|
||||
.map((raw) =>
|
||||
decryptCollection(
|
||||
raw,
|
||||
{
|
||||
|
||||
@@ -241,6 +241,40 @@ const buildServer = async (): Promise<ServerState> => {
|
||||
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 = {
|
||||
id: 100,
|
||||
collectionID: 1,
|
||||
@@ -262,6 +296,8 @@ const buildServer = async (): Promise<ServerState> => {
|
||||
(globalThis as Record<string, unknown>).__mockRawCollection = rawCollection;
|
||||
(globalThis as Record<string, unknown>).__mockRawSharedCollection =
|
||||
rawSharedCollection;
|
||||
(globalThis as Record<string, unknown>).__mockRawDeletedCollection =
|
||||
rawDeletedCollection;
|
||||
(globalThis as Record<string, unknown>).__mockRawFile = rawFile;
|
||||
|
||||
return {
|
||||
@@ -341,6 +377,7 @@ const buildMockFetch = (s: ServerState) => {
|
||||
collections: [
|
||||
g.__mockRawCollection,
|
||||
g.__mockRawSharedCollection,
|
||||
g.__mockRawDeletedCollection,
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -469,7 +506,11 @@ describe("quak Client usage guide", () => {
|
||||
|
||||
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.find((c) => c.id === 3)).toBeUndefined();
|
||||
|
||||
const owned = collections.find((c) => c.id === 1)!;
|
||||
expect(owned.name).toBe("Vacation");
|
||||
|
||||
Reference in New Issue
Block a user