Skip to content

Commit e4bc511

Browse files
authored
test: add controller disconnect localStorage cleanup regression (#2414)
1 parent 22d7d23 commit e4bc511

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import ControllerProvider from "../controller";
2+
3+
type MockStorage = {
4+
[key: string]: string;
5+
};
6+
7+
const createMockLocalStorage = () => {
8+
const store: MockStorage = {};
9+
10+
return {
11+
get length() {
12+
return Object.keys(store).length;
13+
},
14+
clear: jest.fn(() => {
15+
Object.keys(store).forEach((key) => {
16+
delete store[key];
17+
});
18+
}),
19+
getItem: jest.fn((key: string) => (key in store ? store[key] : null)),
20+
setItem: jest.fn((key: string, value: string) => {
21+
store[key] = String(value);
22+
}),
23+
removeItem: jest.fn((key: string) => {
24+
delete store[key];
25+
}),
26+
key: jest.fn((index: number) => {
27+
const keys = Object.keys(store);
28+
return keys[index] || null;
29+
}),
30+
} as Storage;
31+
};
32+
33+
describe("ControllerProvider.disconnect", () => {
34+
const originalLocalStorage = (global as any).localStorage;
35+
36+
beforeEach(() => {
37+
(global as any).localStorage = createMockLocalStorage();
38+
});
39+
40+
afterEach(() => {
41+
(global as any).localStorage = originalLocalStorage;
42+
jest.restoreAllMocks();
43+
});
44+
45+
test("cleans persisted connector/session localStorage keys", async () => {
46+
const controller = new ControllerProvider({});
47+
const keychainDisconnect = jest.fn().mockResolvedValue(undefined);
48+
(controller as any).keychain = {
49+
disconnect: keychainDisconnect,
50+
};
51+
52+
localStorage.setItem("lastUsedConnector", "controller");
53+
localStorage.setItem(
54+
"@cartridge/account/0x40bda2fcd37963c0b8f951801c63a88132feb399dab0f5318245b2c59a553af/0x534e5f5345504f4c4941",
55+
JSON.stringify({ Controller: {} }),
56+
);
57+
localStorage.setItem("@cartridge/active", JSON.stringify({ Active: {} }));
58+
localStorage.setItem(
59+
"@cartridge/https://x.cartridge.gg/active",
60+
JSON.stringify({ Active: {} }),
61+
);
62+
localStorage.setItem(
63+
"@cartridge/policies/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
64+
JSON.stringify({ policies: [] }),
65+
);
66+
localStorage.setItem("@cartridge/features", JSON.stringify({}));
67+
localStorage.setItem(
68+
"@cartridge/session/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
69+
JSON.stringify({ Session: {} }),
70+
);
71+
localStorage.setItem("keepMe", "keep");
72+
73+
await controller.disconnect();
74+
75+
expect(localStorage.getItem("lastUsedConnector")).toBeNull();
76+
expect(
77+
localStorage.getItem(
78+
"@cartridge/account/0x40bda2fcd37963c0b8f951801c63a88132feb399dab0f5318245b2c59a553af/0x534e5f5345504f4c4941",
79+
),
80+
).toBeNull();
81+
expect(localStorage.getItem("@cartridge/active")).toBeNull();
82+
expect(
83+
localStorage.getItem("@cartridge/https://x.cartridge.gg/active"),
84+
).toBeNull();
85+
expect(
86+
localStorage.getItem(
87+
"@cartridge/policies/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
88+
),
89+
).toBeNull();
90+
expect(localStorage.getItem("@cartridge/features")).toBeNull();
91+
expect(
92+
localStorage.getItem(
93+
"@cartridge/session/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
94+
),
95+
).toBeNull();
96+
expect(localStorage.getItem("keepMe")).toBe("keep");
97+
expect(localStorage.removeItem).toHaveBeenCalledWith("lastUsedConnector");
98+
expect(keychainDisconnect).toHaveBeenCalledTimes(1);
99+
});
100+
101+
test("does not throw when localStorage is unavailable", async () => {
102+
delete (global as any).localStorage;
103+
const controller = new ControllerProvider({});
104+
const keychainDisconnect = jest.fn().mockResolvedValue(undefined);
105+
(controller as any).keychain = {
106+
disconnect: keychainDisconnect,
107+
};
108+
109+
await expect(controller.disconnect()).resolves.toBeUndefined();
110+
expect(keychainDisconnect).toHaveBeenCalledTimes(1);
111+
});
112+
});

packages/controller/src/controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ export default class ControllerProvider extends BaseProvider {
395395
try {
396396
if (typeof localStorage !== "undefined") {
397397
localStorage.removeItem("lastUsedConnector");
398+
399+
for (let i = localStorage.length - 1; i >= 0; i--) {
400+
const key = localStorage.key(i);
401+
if (key?.startsWith("@cartridge/")) {
402+
localStorage.removeItem(key);
403+
}
404+
}
398405
}
399406
} catch {
400407
// Ignore environments where localStorage is unavailable.

0 commit comments

Comments
 (0)