quak login --server: log in to a self-hosted Ente server (closes #243)
check / check (push) Successful in 4m25s

`quak login` takes the server's API address from `--server` or
`QUAK_SERVER`, refusing anything that is not an http or https URL. The
client's snapshot now keeps a non-default `apiOrigin`, so the session
file names the server and every later command uses it. A snapshot's
`apiOrigin` wins over the one in `fromJSON`'s `apiOptions`, since the
token belongs to that server.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-13 18:48:55 +00:00
1 parent 9bd7e8f099
commit b6020aca4e
7 files changed
+241 -15

No files matched your search

+40
View File
@@ -47,6 +47,20 @@ describe("Client.toJSON", () => {
expect(Client.fromJSON(snapshot).toJSON()).toEqual(snapshot);
});
it("round-trips a snapshot that names a server", () => {
const snapshot = {
...validSnapshot(),
apiOrigin: "https://ente.example.com",
};
expect(Client.fromJSON(snapshot).toJSON()).toEqual(snapshot);
});
it("names the server apiOptions gave fromJSON", () => {
const apiOrigin = "https://ente.example.com";
const client = Client.fromJSON(validSnapshot(), { apiOrigin });
expect(client.toJSON().apiOrigin).toBe(apiOrigin);
});
it("throws instead of emitting a snapshot without a token", () => {
const client = Client.fromJSON(validSnapshot());
client.getApiClient().clearAuthToken();
@@ -77,6 +91,8 @@ describe("Client.fromJSON", () => {
["publicKey", undefined],
["publicKey", "not base64!"],
["publicKey", shortKey],
["apiOrigin", 7],
["apiOrigin", ""],
])("rejects %s = %j, naming the field", (field, value) => {
const snapshot: Record<string, unknown> = { ...validSnapshot() };
snapshot[field] = value;
@@ -85,6 +101,30 @@ describe("Client.fromJSON", () => {
);
});
// The token belongs to the snapshot's server, so that server wins over
// the one in `apiOptions`.
it("sends requests to the snapshot's server", async () => {
const urls: string[] = [];
const client = Client.fromJSON(
{ ...validSnapshot(), apiOrigin: "https://ente.example.com/" },
{
apiOrigin: "https://other.example.com",
fetch: async (input) => {
urls.push(String(input));
return new Response(JSON.stringify({ collections: [] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
},
);
await client.listCollections();
expect(urls).toEqual([
"https://ente.example.com/collections/v2?sinceTime=0",
]);
});
it.each([null, "a string", 42])("rejects a non-object %j", (value) => {
expect(() => Client.fromJSON(value)).toThrow(
/^Invalid session data: not a JSON object/,