Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f8f85fff5 |
No files matched your search
+25
-4
@@ -4,11 +4,12 @@
|
||||
* each with the callbacks that log its requests and retries, for `quak files`
|
||||
* taking its collection ID as an optional argument, for `quak get` and
|
||||
* `quak get-thumb` refusing `--collection`, and for `quak login` taking
|
||||
* `--server`.
|
||||
* `--server` and sending its first request to the server it names.
|
||||
*
|
||||
* Each test runs `bin/quak.ts`, as the smoke test does, with its session loader
|
||||
* replaced by one that records the options it is given and reports no session,
|
||||
* so the command stops before it makes any request.
|
||||
* so the command stops before it makes any request. `quak login` reads no
|
||||
* session, so its test stubs `fetch` instead and records each request.
|
||||
*
|
||||
* The first load of the sources `bin/quak.ts` imports compiles them, and the
|
||||
* first `init()` starts libsodium. On a loaded host that takes longer than a
|
||||
@@ -59,6 +60,8 @@ afterEach(() => {
|
||||
process.argv = argv;
|
||||
loaded.length = 0;
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -119,12 +122,30 @@ describe("bin/quak.ts get and get-thumb", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// The command refuses an address that is not a URL before it asks for anything
|
||||
// or sends any request.
|
||||
describe("bin/quak.ts login", () => {
|
||||
// The command refuses an address that is not a URL before it asks for
|
||||
// anything or sends any request.
|
||||
it("hands --server to the command", async () => {
|
||||
expect(await quak("login", "--server", "ente.example.com")).toMatch(
|
||||
/^Invalid server address "ente\.example\.com"/,
|
||||
);
|
||||
});
|
||||
|
||||
// `fetch` answers 404, so the login fails at its first request. A login
|
||||
// that succeeded would save its session in this user's real data
|
||||
// directory, since env-paths is not mocked here.
|
||||
it("sends its first request to the server --server names", async () => {
|
||||
const requests: string[] = [];
|
||||
vi.stubGlobal("fetch", async (input: string) => {
|
||||
const { origin, pathname } = new URL(input);
|
||||
requests.push(origin + pathname);
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
vi.stubEnv("QUAK_EMAIL", "login@example.com");
|
||||
vi.stubEnv("QUAK_PASSWORD", "password");
|
||||
await quak("login", "--server", "https://ente.example.com");
|
||||
expect(requests).toEqual([
|
||||
"https://ente.example.com/users/srp/attributes",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -445,6 +445,21 @@ describe("login", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses an empty QUAK_SERVER before asking for anything", async () => {
|
||||
vi.stubEnv("QUAK_SERVER", "");
|
||||
const ctx = loginContext(async () => {
|
||||
throw new Error("login must not be called");
|
||||
});
|
||||
expect(await loginCommand(ctx, {})).toBe(1);
|
||||
|
||||
expect(prompts).toEqual([]);
|
||||
expect(stderr.text).toBe(
|
||||
`Invalid server address "": it must be an http or https URL ` +
|
||||
`with no query or fragment\n`,
|
||||
);
|
||||
expect(existsSync(join(ctx.sessionDir, "session.json"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// These use a real client read from the session file, over a fake API that
|
||||
|
||||
@@ -55,10 +55,25 @@ describe("Client.toJSON", () => {
|
||||
expect(Client.fromJSON(snapshot).toJSON()).toEqual(snapshot);
|
||||
});
|
||||
|
||||
it("names the server apiOptions gave fromJSON", () => {
|
||||
it("names the server apiOptions gave fromJSON", async () => {
|
||||
const apiOrigin = "https://ente.example.com";
|
||||
const client = Client.fromJSON(validSnapshot(), { apiOrigin });
|
||||
const urls: string[] = [];
|
||||
const client = Client.fromJSON(validSnapshot(), {
|
||||
apiOrigin,
|
||||
fetch: async (input) => {
|
||||
urls.push(String(input));
|
||||
return new Response(JSON.stringify({ collections: [] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(client.toJSON().apiOrigin).toBe(apiOrigin);
|
||||
|
||||
await client.listCollections();
|
||||
expect(urls).toEqual([
|
||||
"https://ente.example.com/collections/v2?sinceTime=0",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws instead of emitting a snapshot without a token", () => {
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
* fetched.
|
||||
* 10. A cache another account filled starts empty. One filled from another
|
||||
* server also loses its originals and thumbnails, since two servers can
|
||||
* give two files the same ID.
|
||||
* give two files the same ID. `thumbnailOf` on an empty cache saves the
|
||||
* account's user ID and server. A refresh saves the account's server, so a
|
||||
* self-hosted account keeps its own cache from one `open()` to the next.
|
||||
*
|
||||
* The client is a mock: no crypto, no network. It serves scripted pages and
|
||||
* records the `sinceTime` each call carried so cursor threading is provable.
|
||||
@@ -981,4 +983,75 @@ describe("Library.open and background refresh", () => {
|
||||
await lib.close();
|
||||
}
|
||||
});
|
||||
|
||||
// `quak get-thumb` on an empty cache stores a thumbnail before any refresh
|
||||
// runs. `open()` for an account on another server deletes that thumbnail
|
||||
// only if the cache names the account that stored it.
|
||||
it("thumbnailOf on an empty cache saves the account's user ID and server in metadata.json", async () => {
|
||||
const client = new MockClient();
|
||||
client.apiOrigin = "https://ente.example.com";
|
||||
const lib = await Library.open({
|
||||
client,
|
||||
cacheDirectory,
|
||||
refreshOnOpen: false,
|
||||
refreshIntervalSeconds: Infinity,
|
||||
contentSource: {
|
||||
original: async () => {
|
||||
throw new Error("original not expected");
|
||||
},
|
||||
thumbnail: async ({ destination }) => {
|
||||
writeFileSync(destination, "thumb");
|
||||
return { bytesWritten: 5 };
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
await lib.thumbnailOf(file(1001, 1, 400));
|
||||
} finally {
|
||||
await lib.close();
|
||||
}
|
||||
|
||||
const saved = await MetadataStore.load(
|
||||
join(cacheDirectory, "metadata.json"),
|
||||
);
|
||||
expect(saved.userID).toBe(USER_ID);
|
||||
expect(saved.apiOrigin).toBe("https://ente.example.com");
|
||||
});
|
||||
|
||||
// A refresh saves the account's server in `metadata.json`. Without it,
|
||||
// `open()` would take a self-hosted account's own cache for another
|
||||
// server's and delete it.
|
||||
it("keeps a self-hosted account's own cache from one open to the next", async () => {
|
||||
const client = new MockClient();
|
||||
client.apiOrigin = "https://ente.example.com";
|
||||
client.collectionsQueue.push({
|
||||
collections: [collection(1, 100)],
|
||||
deleted: [],
|
||||
cursor: 100,
|
||||
});
|
||||
const first = await Library.open({
|
||||
client,
|
||||
cacheDirectory,
|
||||
refreshIntervalSeconds: Infinity,
|
||||
});
|
||||
await first.close();
|
||||
mkdirSync(join(cacheDirectory, "originals"));
|
||||
writeFileSync(join(cacheDirectory, "originals", "1001.jpg"), "1001");
|
||||
|
||||
const lib = await Library.open({
|
||||
client,
|
||||
cacheDirectory,
|
||||
refreshIntervalSeconds: Infinity,
|
||||
});
|
||||
try {
|
||||
expect(lib.listCollections().map((c) => c.id)).toEqual([1]);
|
||||
expect(
|
||||
existsSync(join(cacheDirectory, "originals", "1001.jpg")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await lib.close();
|
||||
}
|
||||
// The second open's refresh started from the saved cursor.
|
||||
expect(client.collectionsSinceTimes).toEqual([0, 100]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user