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.
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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 }
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user