Files
Multi-Debrid-Downloader/tests/account-edit.test.ts
T
Sucukdeluxe 9f0140dd3c feat: refine account management and targeted editing
Add local provider icons, larger compact account rows, the requested column order, and full username display while keeping secrets masked. Replace provider-wide editing with a row-specific editor that preserves sibling accounts, exact limits, live usage counters, and status metadata. Add targeted credential validation, release notices, and regression coverage.
2026-08-09 21:32:09 +02:00

299 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { defaultSettings } from "../src/main/constants";
import {
applyAccountEdit,
buildAccountEditCheckSettings,
createAccountEditState,
removeAccountTarget,
validateAccountEdit,
validateAccountEditStatuses,
type AccountEditTarget
} from "../src/renderer/account-edit";
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
const GIB = 1024 * 1024 * 1024;
function megaTarget(login: string): AccountEditTarget {
return {
type: "mega",
rowKey: `mega-${getMegaDebridAccountId(login)}`,
kind: "megadebrid-api",
service: "megadebrid-api",
accountId: getMegaDebridAccountId(login)
};
}
function debridLinkTarget(token: string): AccountEditTarget {
return {
type: "debridlink",
rowKey: `dl-${getDebridLinkApiKeyId(token)}`,
kind: "debridlink-api",
service: "debridlink",
keyId: getDebridLinkApiKeyId(token)
};
}
describe("account-specific editing", () => {
it("changes only the selected Mega-Debrid account and preserves sibling order and mode settings", () => {
const oldId = getMegaDebridAccountId("second@example.com");
const newId = getMegaDebridAccountId("renamed@example.com");
const firstId = getMegaDebridAccountId("first@example.com");
const settings = {
...defaultSettings(),
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass",
megaLogin: "first@example.com",
megaPassword: "first-pass",
megaDebridApiEnabled: true,
megaDebridWebEnabled: true,
megaDebridPreferApi: false,
megaDebridDisabledAccountIds: [oldId, firstId],
megaDebridAccountDailyLimitBytes: { [oldId]: 15 * GIB, [firstId]: 9 * GIB },
megaDebridAccountDailyUsageBytes: { [oldId]: 4 * GIB, [firstId]: 2 * GIB },
megaDebridAccountTotalUsageBytes: { [oldId]: 40 * GIB, [firstId]: 20 * GIB },
debridAccountStatuses: {
[oldId]: { accountId: oldId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 },
[firstId]: { accountId: firstId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
}
};
const state = {
...createAccountEditState(megaTarget("second@example.com"), settings),
login: "renamed@example.com",
password: "new-pass",
dailyLimitGb: "25,5"
};
expect(validateAccountEdit(state, settings)).toBeNull();
const next = applyAccountEdit(settings, state);
expect(next.megaCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass");
expect(next.megaLogin).toBe("first@example.com");
expect(next.megaPassword).toBe("first-pass");
expect(next.megaDebridApiEnabled).toBe(true);
expect(next.megaDebridWebEnabled).toBe(true);
expect(next.megaDebridPreferApi).toBe(false);
expect(next.megaDebridDisabledAccountIds).toEqual([firstId, newId]);
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [firstId]: 9 * GIB, [newId]: Math.floor(25.5 * GIB) });
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [firstId]: 2 * GIB });
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [firstId]: 20 * GIB });
expect(next.debridAccountStatuses).toEqual({
[firstId]: { accountId: firstId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
});
});
it("keeps Mega-Debrid usage when only the password changes", () => {
const id = getMegaDebridAccountId("user@example.com");
const settings = {
...defaultSettings(),
megaCredentials: "user@example.com:old-pass",
megaLogin: "user@example.com",
megaPassword: "old-pass",
megaDebridAccountDailyLimitBytes: { [id]: 8 * GIB },
megaDebridAccountDailyUsageBytes: { [id]: 3 * GIB },
megaDebridAccountTotalUsageBytes: { [id]: 33 * GIB }
};
const state = { ...createAccountEditState(megaTarget("user@example.com"), settings), password: "new-pass" };
const next = applyAccountEdit(settings, state);
expect(next.megaCredentials).toBe("user@example.com:new-pass");
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [id]: 3 * GIB });
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [id]: 33 * GIB });
});
it("preserves exact account limits when the rounded display value is left unchanged", () => {
const megaLogin = "user@example.com";
const megaId = getMegaDebridAccountId(megaLogin);
const key = "debrid-link-token";
const keyId = getDebridLinkApiKeyId(key);
const exactLimit = Math.floor(10.05 * GIB);
const settings = {
...defaultSettings(),
megaCredentials: `${megaLogin}:pass`,
megaLogin,
megaPassword: "pass",
megaDebridAccountDailyLimitBytes: { [megaId]: exactLimit },
debridLinkApiKeys: key,
debridLinkApiKeyDailyLimitBytes: { [keyId]: exactLimit }
};
const megaNext = applyAccountEdit(settings, createAccountEditState(megaTarget(megaLogin), settings));
const debridLinkNext = applyAccountEdit(settings, createAccountEditState(debridLinkTarget(key), settings));
expect(megaNext.megaDebridAccountDailyLimitBytes[megaId]).toBe(exactLimit);
expect(debridLinkNext.debridLinkApiKeyDailyLimitBytes[keyId]).toBe(exactLimit);
});
it("rejects whitespace-only Mega-Debrid passwords and empty or mismatched check results", () => {
const login = "user@example.com";
const settings = {
...defaultSettings(),
megaCredentials: `${login}:pass`,
megaLogin: login,
megaPassword: "pass"
};
const state = { ...createAccountEditState(megaTarget(login), settings), password: " " };
expect(validateAccountEdit(state, settings)).toMatch(/Passwort/i);
expect(validateAccountEditStatuses(state, [])).toMatch(/keinen Account/i);
expect(validateAccountEditStatuses(state, [{
accountId: "mda_other",
provider: "megadebrid",
label: "Account 1",
maskedLogin: "ot***er",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "OK",
checkedAt: 1
}])).toMatch(/falschen Account/i);
});
it("rejects duplicate Mega-Debrid logins and missing row targets", () => {
const settings = {
...defaultSettings(),
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass"
};
const duplicate = {
...createAccountEditState(megaTarget("second@example.com"), settings),
login: "first@example.com"
};
expect(validateAccountEdit(duplicate, settings)).toMatch(/bereits vorhanden/i);
expect(() => createAccountEditState(megaTarget("missing@example.com"), settings)).toThrow(/nicht gefunden/i);
});
it("replaces only the selected Debrid-Link key and migrates its own metadata", () => {
const keyA = "token-a";
const keyB = "token-b";
const keyC = "token-c";
const newKey = "token-b-new";
const idA = getDebridLinkApiKeyId(keyA);
const idB = getDebridLinkApiKeyId(keyB);
const idNew = getDebridLinkApiKeyId(newKey);
const settings = {
...defaultSettings(),
debridLinkApiKeys: `${keyA}\n${keyB}\n${keyC}`,
debridLinkDisabledKeyIds: [idB, idA],
debridLinkApiKeyDailyLimitBytes: { [idA]: 5 * GIB, [idB]: 10 * GIB },
debridLinkApiKeyDailyUsageBytes: { [idA]: 2 * GIB, [idB]: 4 * GIB },
debridLinkApiKeyTotalUsageBytes: { [idA]: 12 * GIB, [idB]: 24 * GIB }
};
const state = {
...createAccountEditState(debridLinkTarget(keyB), settings),
token: newKey,
dailyLimitGb: "12"
};
const next = applyAccountEdit(settings, state);
expect(next.debridLinkApiKeys).toBe(`${keyA}\n${newKey}\n${keyC}`);
expect(next.debridLinkDisabledKeyIds).toEqual([idA, idNew]);
expect(next.debridLinkApiKeyDailyLimitBytes).toEqual({ [idA]: 5 * GIB, [idNew]: 12 * GIB });
expect(next.debridLinkApiKeyDailyUsageBytes).toEqual({ [idA]: 2 * GIB });
expect(next.debridLinkApiKeyTotalUsageBytes).toEqual({ [idA]: 12 * GIB });
});
it("edits a single login without changing unrelated credentials", () => {
const settings = {
...defaultSettings(),
ddownloadLogin: "old@example.com",
ddownloadPassword: "old-pass",
linkSnappyLogin: "keep@example.com",
linkSnappyPassword: "keep-pass"
};
const target: AccountEditTarget = {
type: "single",
rowKey: "svc-ddownload",
kind: "ddownload-login",
service: "ddownload",
provider: "ddownload"
};
const state = {
...createAccountEditState(target, settings),
login: "new@example.com",
password: "new-pass",
dailyLimitGb: "7"
};
const next = applyAccountEdit(settings, state);
expect(next.ddownloadLogin).toBe("new@example.com");
expect(next.ddownloadPassword).toBe("new-pass");
expect(next.linkSnappyLogin).toBe("keep@example.com");
expect(next.linkSnappyPassword).toBe("keep-pass");
expect(next.providerDailyLimitBytes.ddownload).toBe(7 * GIB);
});
it("rejects whitespace-only passwords for direct login accounts", () => {
const settings = {
...defaultSettings(),
ddownloadLogin: "user@example.com",
ddownloadPassword: "password",
linkSnappyLogin: "member@example.com",
linkSnappyPassword: "secret"
};
const ddownloadTarget: AccountEditTarget = {
type: "single",
rowKey: "svc-ddownload",
kind: "ddownload-login",
service: "ddownload",
provider: "ddownload"
};
const linkSnappyTarget: AccountEditTarget = {
type: "single",
rowKey: "svc-linksnappy",
kind: "linksnappy-login",
service: "linksnappy",
provider: "linksnappy"
};
expect(validateAccountEdit({ ...createAccountEditState(ddownloadTarget, settings), password: " " }, settings)).toMatch(/Passwort/i);
expect(validateAccountEdit({ ...createAccountEditState(linkSnappyTarget, settings), password: "\t" }, settings)).toMatch(/Passwort/i);
});
it("builds a targeted check snapshot without invalid sibling accounts", () => {
const settings = {
...defaultSettings(),
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
megaLogin: "first@example.com",
megaPassword: "first-pass",
debridLinkApiKeys: "one\ntwo"
};
const target = megaTarget("second@example.com");
const state = createAccountEditState(target, settings);
const checkSettings = buildAccountEditCheckSettings(settings, state);
expect(checkSettings.megaCredentials).toBe("second@example.com:second-pass");
expect(checkSettings.megaLogin).toBe("second@example.com");
expect(checkSettings.megaPassword).toBe("second-pass");
expect(checkSettings.debridLinkApiKeys).toBe("");
});
it("removes only the selected Mega-Debrid account and all metadata belonging to it", () => {
const removeId = getMegaDebridAccountId("second@example.com");
const keepId = getMegaDebridAccountId("first@example.com");
const settings = {
...defaultSettings(),
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
megaLogin: "first@example.com",
megaPassword: "first-pass",
megaDebridDisabledAccountIds: [removeId, keepId],
megaDebridAccountDailyLimitBytes: { [removeId]: 2, [keepId]: 1 },
megaDebridAccountDailyUsageBytes: { [removeId]: 4, [keepId]: 3 },
megaDebridAccountTotalUsageBytes: { [removeId]: 6, [keepId]: 5 },
debridAccountStatuses: {
[removeId]: { accountId: removeId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: false, premiumUntilMs: null, message: "Free", checkedAt: 1 },
[keepId]: { accountId: keepId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
}
};
const next = removeAccountTarget(settings, megaTarget("second@example.com"));
expect(next.megaCredentials).toBe("first@example.com:first-pass");
expect(next.megaDebridDisabledAccountIds).toEqual([keepId]);
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [keepId]: 1 });
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [keepId]: 3 });
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [keepId]: 5 });
expect(next.debridAccountStatuses).toEqual({
[keepId]: { accountId: keepId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
});
});
});