feat(qbittorrent): login compat for v4/v5.0-5.2 and three auth modes
- Login success = HTTP 200 "Ok." or 204 (5.2+); session cookie taken from
Set-Cookie by name (SID / QBT_SID_<port> / custom) and sent back as-is.
- Failure classes: "Fails." / 401 -> auth; 403 IP ban / 429 -> rate;
wrong API Key (403) -> auth with hint; 5xx -> protocol.
- Downloader instance auth kind: password / api_key (qB 5.2+ Bearer, no
/auth/login) / none (whitelisted subnet); stored in settings.authKind,
credential write-only in secret_ref, cleared when switching to none.
One qbAuthConfig() shared by sync, agent capability and connection test.
- Settings form: auth kind select, password / API Key fields (no echo).
- testkit: mock qB 5.2 login behaviour, IP ban, bypassAuth;
makeTorrent({ realPieces }) builds real piece hashes for real qB checks.
- Real-container matrix (QB_REAL, skipped by default): 5.1.2 and 5.2.4,
login / add / query / tags / pause-resume / delete keeping data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c846e91cc4
commit
74f1566985
14 files changed
+756
-59
No files matched your search
@@ -188,6 +188,66 @@ describe.skipIf(!hasPg)("W2-A 管理 API(真实 PG)", () => {
|
||||
noLeak("qb-secret-pass");
|
||||
});
|
||||
|
||||
it("downloader_instance 认证方式三选一:API Key(不登录)/ 无认证(内网白名单)/ 账密;切换校验与凭证清理", async () => {
|
||||
const k52 = await createMockQb({ version: "5.2.4", apiKey: "qbt_live_key" });
|
||||
const open = await createMockQb({ version: "5.2.4", bypassAuth: true });
|
||||
try {
|
||||
expect(
|
||||
(await admin("POST", "/downloader-instances", { name: "k0", baseUrl: k52.url, authKind: "api_key" })).status,
|
||||
).toBe(422);
|
||||
expect(
|
||||
(await admin("POST", "/downloader-instances", { name: "p0", baseUrl: k52.url, authKind: "password" })).status,
|
||||
).toBe(422);
|
||||
const k = (
|
||||
await json(
|
||||
await admin("POST", "/downloader-instances", {
|
||||
name: "k52",
|
||||
baseUrl: k52.url,
|
||||
authKind: "api_key",
|
||||
credential: "qbt_live_key",
|
||||
}),
|
||||
)
|
||||
).data;
|
||||
expect(k.settings.authKind).toBe("api_key");
|
||||
expect(JSON.stringify(k)).not.toContain("qbt_live_key");
|
||||
expect((await json(await admin("POST", `/downloader-instances/${k.id}/test`))).data.ok).toBe(true);
|
||||
expect(k52.callsTo("auth/login")).toHaveLength(0);
|
||||
await admin("PATCH", `/downloader-instances/${k.id}`, { credential: "qbt_wrong" });
|
||||
const bad = (await json(await admin("POST", `/downloader-instances/${k.id}/test`))).data;
|
||||
expect(bad.ok).toBe(false);
|
||||
expect(bad.message).toContain("API Key");
|
||||
|
||||
const n = (
|
||||
await json(
|
||||
await admin("POST", "/downloader-instances", { name: "open", baseUrl: open.url, authKind: "none" }),
|
||||
)
|
||||
).data;
|
||||
expect(n.secretRef).toBeNull();
|
||||
expect((await json(await admin("POST", `/downloader-instances/${n.id}/test`))).data.ok).toBe(true);
|
||||
expect(open.callsTo("auth/login")).toHaveLength(0);
|
||||
|
||||
// 切到无认证:凭证删除;无认证连需要认证的实例 → ok=false
|
||||
const sw = (await json(await admin("PATCH", `/downloader-instances/${k.id}`, { authKind: "none" }))).data;
|
||||
expect(sw.secretRef).toBeNull();
|
||||
await expect(h.ctx.secrets.resolve(k.secretRef)).rejects.toThrow(/不存在/);
|
||||
expect((await json(await admin("POST", `/downloader-instances/${k.id}/test`))).data.ok).toBe(false);
|
||||
// 再切回 API Key 必须带 Key
|
||||
expect((await admin("PATCH", `/downloader-instances/${k.id}`, { authKind: "api_key" })).status).toBe(422);
|
||||
const back = (
|
||||
await json(
|
||||
await admin("PATCH", `/downloader-instances/${k.id}`, { authKind: "api_key", credential: "qbt_live_key" }),
|
||||
)
|
||||
).data;
|
||||
expect(back.secretRef).toBeTruthy();
|
||||
expect((await json(await admin("POST", `/downloader-instances/${k.id}/test`))).data.ok).toBe(true);
|
||||
for (const id of [k.id, n.id]) await admin("DELETE", `/downloader-instances/${id}`);
|
||||
noLeak("qbt_live_key");
|
||||
} finally {
|
||||
await k52.close();
|
||||
await open.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("media_server_instance:无 key 时 test 报未配置;有 key 时 ping 成功", async () => {
|
||||
const a = (await json(await admin("POST", "/media-server-instances", { name: "emby-nokey", baseUrl: emby.url })))
|
||||
.data;
|
||||
|
||||
Reference in new issue
Block a user