feat(realdebrid): isolate browser account sessions
This commit is contained in:
@@ -33,10 +33,11 @@ const {
|
||||
isMinimized: vi.fn(() => false),
|
||||
restore: vi.fn(),
|
||||
show,
|
||||
focus,
|
||||
close: vi.fn(() => {
|
||||
destroyed = true;
|
||||
windowEvents.closed?.();
|
||||
focus,
|
||||
close: vi.fn(() => {
|
||||
windowEvents.close?.();
|
||||
destroyed = true;
|
||||
windowEvents.closed?.();
|
||||
}),
|
||||
setMenuBarVisibility: vi.fn(),
|
||||
loadURL,
|
||||
@@ -88,7 +89,9 @@ vi.mock("electron", () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
import { RealDebridWebFallback, extractPrivateTokenFromHtml } from "../src/main/realdebrid-web";
|
||||
import { RealDebridWebFallback, extractPrivateTokenFromHtml } from "../src/main/realdebrid-web";
|
||||
import { AppController } from "../src/main/app-controller";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
|
||||
describe("realdebrid-web", () => {
|
||||
const mockSession = {
|
||||
@@ -130,7 +133,7 @@ describe("realdebrid-web", () => {
|
||||
|
||||
mockExecuteJavaScript.mockResolvedValue("token-from-window");
|
||||
|
||||
const fallback = new RealDebridWebFallback(() => true);
|
||||
const fallback = new RealDebridWebFallback("persist:realdebrid-web", () => true);
|
||||
await fallback.openLoginWindow();
|
||||
|
||||
const result = await fallback.unrestrict("https://rapidgator.net/file/abc");
|
||||
@@ -173,7 +176,7 @@ describe("realdebrid-web", () => {
|
||||
}), { status: 200 }));
|
||||
vi.stubGlobal("fetch", apiFetch);
|
||||
|
||||
const fallback = new RealDebridWebFallback(() => true);
|
||||
const fallback = new RealDebridWebFallback("persist:realdebrid-web", () => true);
|
||||
await fallback.openLoginWindow();
|
||||
const status = await fallback.probeLoginState();
|
||||
|
||||
@@ -197,9 +200,244 @@ describe("realdebrid-web", () => {
|
||||
it("notifies the controller when a new browser token is detected", async () => {
|
||||
mockExecuteJavaScript.mockResolvedValue("new-browser-token");
|
||||
const onAuthenticated = vi.fn();
|
||||
const fallback = new RealDebridWebFallback(() => true, onAuthenticated);
|
||||
const fallback = new RealDebridWebFallback("persist:realdebrid-web", () => true, onAuthenticated);
|
||||
|
||||
await fallback.openLoginWindow();
|
||||
await vi.waitFor(() => expect(onAuthenticated).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("uses isolated persistent and transient partitions for separate browser accounts", async () => {
|
||||
const first = new RealDebridWebFallback("persist:realdebrid-web-rdw_first", () => true);
|
||||
const second = new RealDebridWebFallback("persist:realdebrid-web-rdw_second", () => true);
|
||||
|
||||
await first.openLoginWindow();
|
||||
await second.openLoginWindow();
|
||||
|
||||
expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(2);
|
||||
expect(mockBrowserWindowCtor.mock.calls.map((call) => (call[0] as any).webPreferences.partition)).toEqual([
|
||||
"persist:realdebrid-web-rdw_first",
|
||||
"persist:realdebrid-web-rdw_second"
|
||||
]);
|
||||
|
||||
await first.clearSessions();
|
||||
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_first");
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_first");
|
||||
expect(mockFromPartition).not.toHaveBeenCalledWith("persist:realdebrid-web-rdw_second");
|
||||
});
|
||||
|
||||
it("rejects unsafe browser partitions", () => {
|
||||
expect(() => new RealDebridWebFallback("persist:realdebrid-web-../shared", () => true)).toThrow(/Partition/i);
|
||||
});
|
||||
|
||||
it("persists a reserved browser account only after a successful probe", async () => {
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
const applyStatuses = vi.fn();
|
||||
const updateSettings = vi.fn((settings) => {
|
||||
controller.settings = settings;
|
||||
});
|
||||
const fallback = {
|
||||
probeLoginState: vi.fn().mockResolvedValueOnce({
|
||||
valid: false,
|
||||
username: "",
|
||||
email: "",
|
||||
isPremium: false,
|
||||
premiumUntilMs: null,
|
||||
message: "Nicht angemeldet"
|
||||
}).mockResolvedValueOnce({
|
||||
valid: true,
|
||||
username: "fixture-user",
|
||||
email: "fixture@example.test",
|
||||
isPremium: true,
|
||||
premiumUntilMs: Date.parse("2030-01-02T03:04:05.000Z"),
|
||||
message: "Premium aktiv"
|
||||
}),
|
||||
dispose: vi.fn()
|
||||
};
|
||||
controller.settings = defaultSettings();
|
||||
controller.pendingRealDebridWebAccountIds = new Map([["rdw_reserved", 0]]);
|
||||
controller.realDebridWebGenerations = new Map([["rdw_reserved", 0]]);
|
||||
controller.realDebridWebFallbacks = new Map([["rdw_reserved", fallback]]);
|
||||
controller.manager = { applyDebridAccountStatuses: applyStatuses };
|
||||
controller.updateSettings = updateSettings;
|
||||
controller.getRealDebridWebFallback = () => fallback;
|
||||
|
||||
await controller.refreshRealDebridWebStatus("rdw_reserved");
|
||||
|
||||
expect(updateSettings).not.toHaveBeenCalled();
|
||||
expect(applyStatuses).not.toHaveBeenCalled();
|
||||
expect(controller.settings.realDebridWebAccountIds).toEqual([]);
|
||||
|
||||
await controller.refreshRealDebridWebStatus("rdw_reserved");
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||
expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_reserved"]);
|
||||
expect(applyStatuses).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ accountId: "rdw_reserved", valid: true, username: "fixture-user" })
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores a successful probe that completes after its account was deleted", async () => {
|
||||
let resolveProbe!: (value: Record<string, unknown>) => void;
|
||||
const probe = new Promise<Record<string, unknown>>((resolve) => {
|
||||
resolveProbe = resolve;
|
||||
});
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
const settings = defaultSettings();
|
||||
settings.realDebridWebAccountIds = ["rdw_existing"];
|
||||
const applyStatuses = vi.fn();
|
||||
controller.settings = settings;
|
||||
controller.pendingRealDebridWebAccountIds = new Map();
|
||||
controller.realDebridWebGenerations = new Map([["rdw_existing", 0]]);
|
||||
controller.manager = { applyDebridAccountStatuses: applyStatuses };
|
||||
controller.getRealDebridWebFallback = () => ({ probeLoginState: () => probe, dispose: vi.fn() });
|
||||
|
||||
const running = controller.refreshRealDebridWebStatus("rdw_existing", 0);
|
||||
controller.settings = { ...settings, realDebridWebAccountIds: [] };
|
||||
controller.realDebridWebGenerations.set("rdw_existing", 1);
|
||||
resolveProbe({
|
||||
valid: true,
|
||||
username: "deleted-user",
|
||||
email: "deleted@example.test",
|
||||
isPremium: true,
|
||||
premiumUntilMs: Date.now() + 60_000,
|
||||
message: "Premium aktiv"
|
||||
});
|
||||
await running;
|
||||
|
||||
expect(applyStatuses).not.toHaveBeenCalled();
|
||||
expect(controller.settings.realDebridWebAccountIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("clears cold account partitions without retaining a fallback instance", async () => {
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
controller.settings = defaultSettings();
|
||||
controller.realDebridWebFallbacks = new Map();
|
||||
controller.pendingRealDebridWebAccountIds = new Map();
|
||||
controller.realDebridWebGenerations = new Map();
|
||||
controller.realDebridWebAuthenticationTasks = new Map();
|
||||
|
||||
await controller.cleanupRealDebridWebAccount("rdw_cold", true);
|
||||
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_cold");
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_cold");
|
||||
expect(controller.realDebridWebFallbacks.size).toBe(0);
|
||||
});
|
||||
|
||||
it("cleans a reserved account completely when opening its login window fails", async () => {
|
||||
mockLoadURL.mockRejectedValueOnce(new Error("load failed"));
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
controller.settings = defaultSettings();
|
||||
controller.realDebridWebFallbacks = new Map();
|
||||
controller.pendingRealDebridWebAccountIds = new Map();
|
||||
controller.realDebridWebGenerations = new Map();
|
||||
controller.realDebridWebAuthenticationTasks = new Map();
|
||||
controller.audit = vi.fn();
|
||||
|
||||
await expect(controller.openRealDebridLoginWindow({ accountId: "rdw_failed", create: true })).rejects.toThrow("load failed");
|
||||
|
||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||
expect(controller.realDebridWebFallbacks.size).toBe(0);
|
||||
expect(mockBrowserWindow.close).toHaveBeenCalled();
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_failed");
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_failed");
|
||||
});
|
||||
|
||||
it("cleans a reserved account when the user closes its login window", async () => {
|
||||
mockExecuteJavaScript.mockResolvedValue("");
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
controller.settings = defaultSettings();
|
||||
controller.realDebridWebFallbacks = new Map();
|
||||
controller.pendingRealDebridWebAccountIds = new Map();
|
||||
controller.realDebridWebGenerations = new Map();
|
||||
controller.realDebridWebAuthenticationTasks = new Map();
|
||||
controller.audit = vi.fn();
|
||||
|
||||
await controller.openRealDebridLoginWindow({ accountId: "rdw_closed", create: true });
|
||||
mockBrowserWindow.close();
|
||||
await vi.waitFor(() => expect(controller.realDebridWebFallbacks.size).toBe(0));
|
||||
|
||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_closed");
|
||||
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_closed");
|
||||
});
|
||||
|
||||
it("does not prime or notify while a browser window is cleared programmatically", async () => {
|
||||
mockExecuteJavaScript.mockResolvedValue("");
|
||||
const onAuthenticated = vi.fn();
|
||||
const onClosed = vi.fn();
|
||||
const fallback = new RealDebridWebFallback("persist:realdebrid-web-rdw_cleanup", () => true, onAuthenticated, onClosed);
|
||||
await fallback.openLoginWindow();
|
||||
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalled());
|
||||
mockExecuteJavaScript.mockReset();
|
||||
mockExecuteJavaScript.mockResolvedValue("stale-token");
|
||||
|
||||
await fallback.clearSessions();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(mockExecuteJavaScript).not.toHaveBeenCalled();
|
||||
expect(onAuthenticated).not.toHaveBeenCalled();
|
||||
expect(onClosed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets close-time authentication finish before cleaning a reserved account", async () => {
|
||||
let resolveClosingToken!: (token: string) => void;
|
||||
const closingToken = new Promise<string>((resolve) => {
|
||||
resolveClosingToken = resolve;
|
||||
});
|
||||
mockExecuteJavaScript.mockResolvedValueOnce("").mockReturnValueOnce(closingToken);
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
||||
username: "close-user",
|
||||
email: "close-user@example.test",
|
||||
type: "premium",
|
||||
expiration: "2030-01-02T03:04:05.000Z"
|
||||
}), { status: 200 })));
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
controller.settings = defaultSettings();
|
||||
controller.realDebridWebFallbacks = new Map();
|
||||
controller.pendingRealDebridWebAccountIds = new Map();
|
||||
controller.realDebridWebGenerations = new Map();
|
||||
controller.realDebridWebAuthenticationTasks = new Map();
|
||||
controller.manager = { applyDebridAccountStatuses: vi.fn() };
|
||||
controller.updateSettings = vi.fn((settings) => {
|
||||
controller.settings = settings;
|
||||
});
|
||||
controller.audit = vi.fn();
|
||||
|
||||
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true });
|
||||
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1));
|
||||
mockBrowserWindow.close();
|
||||
resolveClosingToken("close-time-token");
|
||||
await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"]));
|
||||
|
||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||
expect(mockFromPartition).not.toHaveBeenCalledWith("persist:realdebrid-web-rdw_close_auth");
|
||||
expect(controller.manager.applyDebridAccountStatuses).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ accountId: "rdw_close_auth", valid: true, username: "close-user" })
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["clear", "dispose"] as const)("ignores a session token that resolves after %s", async (mode) => {
|
||||
let resolveSessionFetch!: (response: Response) => void;
|
||||
const delayedResponse = new Promise<Response>((resolve) => {
|
||||
resolveSessionFetch = resolve;
|
||||
});
|
||||
mockSessionFetch.mockReturnValueOnce(delayedResponse);
|
||||
const onAuthenticated = vi.fn();
|
||||
const fallback = new RealDebridWebFallback(`persist:realdebrid-web-rdw_delayed_${mode}`, () => true, onAuthenticated);
|
||||
|
||||
const probing = fallback.probeLoginState();
|
||||
await vi.waitFor(() => expect(mockSessionFetch).toHaveBeenCalledTimes(1));
|
||||
if (mode === "clear") {
|
||||
await fallback.clearSessions();
|
||||
} else {
|
||||
fallback.dispose();
|
||||
}
|
||||
resolveSessionFetch(new Response("<input name=\"private_token\" value=\"late-token\">", { status: 200 }));
|
||||
await probing;
|
||||
await Promise.resolve();
|
||||
|
||||
expect((fallback as any).cachedToken).toBe("");
|
||||
expect(onAuthenticated).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user