Skip to content

Commit

Permalink
Add localStorage fallback class
Browse files Browse the repository at this point in the history
  • Loading branch information
wendevlin committed Dec 6, 2024
1 parent c419043 commit 2e6bf85
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 81 deletions.
7 changes: 5 additions & 2 deletions src/common/auth/token_storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
import { FallbackStorage } from "../../../test/test_helper/local-storage-fallback";

const storage = window.localStorage || new FallbackStorage();

declare global {
interface Window {
Expand Down Expand Up @@ -36,7 +39,7 @@ export function saveTokens(tokens: AuthData | null) {

if (tokenCache.writeEnabled) {
try {
window.localStorage.setItem("hassTokens", JSON.stringify(tokens));
storage.setItem("hassTokens", JSON.stringify(tokens));
} catch (err: any) {
// write failed, ignore it. Happens if storage is full or private mode.
// eslint-disable-next-line no-console
Expand All @@ -59,7 +62,7 @@ export function enableWrite() {
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
const tokens = window.localStorage.getItem("hassTokens");
const tokens = storage.getItem("hassTokens");
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;
Expand Down
12 changes: 6 additions & 6 deletions src/util/ha-pref-storage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { FallbackStorage } from "../../test/test_helper/local-storage-fallback";
import type { HomeAssistant } from "../types";

const storage = window.localStorage || new FallbackStorage();

const STORED_STATE = [
"dockedSidebar",
"selectedTheme",
Expand All @@ -15,10 +18,7 @@ export function storeState(hass: HomeAssistant) {
try {
STORED_STATE.forEach((key) => {
const value = hass[key];
window.localStorage.setItem(
key,
JSON.stringify(value === undefined ? null : value)
);
storage.setItem(key, JSON.stringify(value === undefined ? null : value));
});
} catch (err: any) {
// Safari throws exception in private mode
Expand All @@ -35,7 +35,7 @@ export function getState() {
const state = {};

STORED_STATE.forEach((key) => {
const storageItem = window.localStorage.getItem(key);
const storageItem = storage.getItem(key);
if (storageItem !== null) {
let value = JSON.parse(storageItem);
// selectedTheme went from string to object on 20200718
Expand All @@ -53,5 +53,5 @@ export function getState() {
}

export function clearState() {
window.localStorage.clear();
storage.clear();
}
28 changes: 16 additions & 12 deletions test/common/auth/token_storage/saveTokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ describe("token_storage.saveTokens", () => {
extractSearchParam: extractSearchParamSpy,
}));

window.localStorage = {
setItem: vi.fn(),
} as unknown as Storage;
const { FallbackStorage: fallbackStorage } = await import(
"../../../test_helper/local-storage-fallback"
);
const setItemSpy = vi.fn();
fallbackStorage.prototype.setItem = setItemSpy;

({ saveTokens } = await import(
"../../../../src/common/auth/token_storage"
Expand All @@ -70,8 +72,8 @@ describe("token_storage.saveTokens", () => {
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(extractSearchParamSpy).toHaveBeenCalledOnce();
expect(extractSearchParamSpy).toHaveBeenCalledWith("storeToken");
expect(window.localStorage.setItem).toHaveBeenCalledOnce();
expect(window.localStorage.setItem).toHaveBeenCalledWith(
expect(setItemSpy).toHaveBeenCalledOnce();
expect(setItemSpy).toHaveBeenCalledWith(
"hassTokens",
JSON.stringify(tokens)
);
Expand Down Expand Up @@ -101,11 +103,13 @@ describe("token_storage.saveTokens", () => {
extractSearchParam: extractSearchParamSpy,
}));

window.localStorage = {
setItem: vi.fn(() => {
throw new Error("Full storage");
}),
} as unknown as Storage;
const { FallbackStorage: fallbackStorage } = await import(
"../../../test_helper/local-storage-fallback"
);
const setItemSpy = vi.fn(() => {
throw new Error("Full storage");
});
fallbackStorage.prototype.setItem = setItemSpy;

// eslint-disable-next-line no-global-assign
console = {
Expand All @@ -120,8 +124,8 @@ describe("token_storage.saveTokens", () => {
expect(window.__tokenCache.tokens).toEqual(tokens);
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(extractSearchParamSpy).toBeCalledTimes(0);
expect(window.localStorage.setItem).toHaveBeenCalledOnce();
expect(window.localStorage.setItem).toHaveBeenCalledWith(
expect(setItemSpy).toHaveBeenCalledOnce();
expect(setItemSpy).toHaveBeenCalledWith(
"hassTokens",
JSON.stringify(tokens)
);
Expand Down
37 changes: 22 additions & 15 deletions test/common/auth/token_storage/token_storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("token_storage", () => {
});

afterEach(() => {
vi.resetAllMocks();
vi.resetModules();
});

Expand Down Expand Up @@ -40,9 +41,11 @@ describe("token_storage", () => {
clientId: "client",
};

window.localStorage = {
getItem: vi.fn(() => JSON.stringify(tokens)),
} as unknown as Storage;
const { FallbackStorage: fallbackStorage } = await import(
"../../../test_helper/local-storage-fallback"
);
const getItemSpy = vi.fn(() => JSON.stringify(tokens));
fallbackStorage.prototype.getItem = getItemSpy;

const { loadTokens } = await import(
"../../../../src/common/auth/token_storage"
Expand All @@ -53,14 +56,16 @@ describe("token_storage", () => {

expect(window.__tokenCache.tokens).toEqual(tokens);
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(window.localStorage.getItem).toHaveBeenCalledOnce();
expect(window.localStorage.getItem).toHaveBeenCalledWith("hassTokens");
expect(getItemSpy).toHaveBeenCalledOnce();
expect(getItemSpy).toHaveBeenCalledWith("hassTokens");
});

test("should load null tokens", async () => {
window.localStorage = {
getItem: vi.fn(() => null),
} as unknown as Storage;
const { FallbackStorage: fallbackStorage } = await import(
"../../../test_helper/local-storage-fallback"
);
const getItemSpy = vi.fn(() => "hello");
fallbackStorage.prototype.getItem = getItemSpy;

const { loadTokens } = await import(
"../../../../src/common/auth/token_storage"
Expand All @@ -71,8 +76,8 @@ describe("token_storage", () => {

expect(window.__tokenCache.tokens).toEqual(null);
expect(window.__tokenCache.writeEnabled).toBe(undefined);
expect(window.localStorage.getItem).toHaveBeenCalledOnce();
expect(window.localStorage.getItem).toHaveBeenCalledWith("hassTokens");
expect(getItemSpy).toHaveBeenCalledOnce();
expect(getItemSpy).toHaveBeenCalledWith("hassTokens");
});

it("should enable write", async () => {
Expand All @@ -91,18 +96,20 @@ describe("token_storage", () => {
})
);

window.localStorage = {
setItem: vi.fn(),
} as unknown as Storage;
const { FallbackStorage: fallbackStorage } = await import(
"../../../test_helper/local-storage-fallback"
);
const setItemSpy = vi.fn();
fallbackStorage.prototype.setItem = setItemSpy;

const { enableWrite } = await import(
"../../../../src/common/auth/token_storage"
);

enableWrite();
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(window.localStorage.setItem).toHaveBeenCalledOnce();
expect(window.localStorage.setItem).toHaveBeenCalledWith(
expect(setItemSpy).toHaveBeenCalledOnce();
expect(setItemSpy).toHaveBeenCalledWith(
"hassTokens",
JSON.stringify("testToken")
);
Expand Down
38 changes: 38 additions & 0 deletions test/test_helper/local-storage-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export class FallbackStorage implements Storage {
private valuesMap = new Map();

getItem(key) {
const stringKey = String(key);
if (this.valuesMap.has(key)) {
return String(this.valuesMap.get(stringKey));
}
return null;
}

setItem(key, val) {
this.valuesMap.set(String(key), String(val));
}

removeItem(key) {
this.valuesMap.delete(key);
}

clear() {
this.valuesMap.clear();
}

key(i) {
if (arguments.length === 0) {
// this is a TypeError implemented on Chrome, Firefox throws Not enough arguments to Storage.key.
throw new TypeError(
"Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present."
);
}
const arr = Array.from(this.valuesMap.keys());
return arr[i];
}

get length() {
return this.valuesMap.size;
}
}
Loading

0 comments on commit 2e6bf85

Please sign in to comment.