From 23b98f507c37a6f7ff37cd0564be40cd802b1561 Mon Sep 17 00:00:00 2001 From: scanash00 Date: Wed, 12 Aug 2026 09:29:17 -0800 Subject: [PATCH] More deactivated fixes --- frontend/src/lib/flows/migration-shared.ts | 28 +++++++++++++++---- frontend/src/lib/migration/atproto-client.ts | 1 + .../tests/migration/atproto-client.test.ts | 22 +++++++++++++++ .../tests/migration/migration-shared.test.ts | 27 ++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/flows/migration-shared.ts b/frontend/src/lib/flows/migration-shared.ts index beca67c..f42addc 100644 --- a/frontend/src/lib/flows/migration-shared.ts +++ b/frontend/src/lib/flows/migration-shared.ts @@ -62,14 +62,32 @@ export async function finalizeAccountMigration( sourceAlreadyDeactivated?: boolean; onSourceDeactivated?: () => void; onSourceDeactivationFailed?: (error: unknown) => void; + sourceDeactivationRetryDelayMs?: number; } = {}, ): Promise { if (!options.sourceAlreadyDeactivated) { - try { - await sourceClient.deactivateAccount(); - options.onSourceDeactivated?.(); - } catch (error) { - options.onSourceDeactivationFailed?.(error); + const maxAttempts = 3; + const retryDelayMs = options.sourceDeactivationRetryDelayMs ?? 500; + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + await sourceClient.deactivateAccount(); + options.onSourceDeactivated?.(); + lastError = undefined; + break; + } catch (error) { + lastError = error; + if (attempt < maxAttempts && retryDelayMs > 0) { + await new Promise((resolve) => + setTimeout(resolve, retryDelayMs * attempt) + ); + } + } + } + + if (lastError !== undefined) { + options.onSourceDeactivationFailed?.(lastError); } } await destinationClient.activateAccount(); diff --git a/frontend/src/lib/migration/atproto-client.ts b/frontend/src/lib/migration/atproto-client.ts index fd6e9a0..bd0c8fe 100644 --- a/frontend/src/lib/migration/atproto-client.ts +++ b/frontend/src/lib/migration/atproto-client.ts @@ -562,6 +562,7 @@ export class AtprotoClient { try { await this.xrpc("com.atproto.server.deactivateAccount", { httpMethod: "POST", + body: {}, }); apiLog( "POST", diff --git a/frontend/src/tests/migration/atproto-client.test.ts b/frontend/src/tests/migration/atproto-client.test.ts index 9582915..b051c7e 100644 --- a/frontend/src/tests/migration/atproto-client.test.ts +++ b/frontend/src/tests/migration/atproto-client.test.ts @@ -349,4 +349,26 @@ describe("migration/atproto-client", () => { ); }); }); + + describe("AtprotoClient.deactivateAccount", () => { + it("sends the required empty JSON object", async () => { + globalThis.fetch = vi.fn().mockResolvedValue( + new Response("{}", { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AtprotoClient("https://old-pds.example.com"); + await client.deactivateAccount(); + + expect(fetch).toHaveBeenCalledWith( + "https://old-pds.example.com/xrpc/com.atproto.server.deactivateAccount", + expect.objectContaining({ + method: "POST", + body: "{}", + }), + ); + }); + }); }); diff --git a/frontend/src/tests/migration/migration-shared.test.ts b/frontend/src/tests/migration/migration-shared.test.ts index a3719f7..f194800 100644 --- a/frontend/src/tests/migration/migration-shared.test.ts +++ b/frontend/src/tests/migration/migration-shared.test.ts @@ -34,13 +34,40 @@ describe("migration/finalizeAccountMigration", () => { await finalizeAccountMigration(sourceClient, destinationClient, { onSourceDeactivated, onSourceDeactivationFailed, + sourceDeactivationRetryDelayMs: 0, }); expect(onSourceDeactivated).not.toHaveBeenCalled(); expect(onSourceDeactivationFailed).toHaveBeenCalledWith(sourceError); + expect(sourceClient.deactivateAccount).toHaveBeenCalledTimes(3); expect(destinationClient.activateAccount).toHaveBeenCalledOnce(); }); + it("retries source deactivation before activating the destination", async () => { + const calls: string[] = []; + const sourceClient = { + deactivateAccount: vi + .fn() + .mockRejectedValueOnce(new Error("source unavailable")) + .mockRejectedValueOnce(new Error("source unavailable")) + .mockImplementation(async () => { + calls.push("deactivate-source"); + }), + }; + const destinationClient = { + activateAccount: vi.fn(async () => { + calls.push("activate-destination"); + }), + }; + + await finalizeAccountMigration(sourceClient, destinationClient, { + sourceDeactivationRetryDelayMs: 0, + }); + + expect(sourceClient.deactivateAccount).toHaveBeenCalledTimes(3); + expect(calls).toEqual(["deactivate-source", "activate-destination"]); + }); + it("retries activation without deactivating the source again", async () => { const sourceClient = { deactivateAccount: vi.fn(), -- 2.51.2