Skip to content

Commit c74d184

Browse files
[change] Extracted storage logic from Status component #918
This commit extracts the storage logic from the Status component to improve modularity and testability. Closes #918
1 parent 5eef317 commit c74d184

3 files changed

Lines changed: 133 additions & 60 deletions

File tree

client/components/status/status.js

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import {localStorage} from "../../utils/storage";
3737
import handleSession from "../../utils/session";
3838
import getPlanSelection from "../../utils/get-plan-selection";
3939
import getPlans from "../../utils/get-plans";
40+
import {
41+
storeValue,
42+
resolveStoredValue,
43+
} from "../../utils/captive-portal-storage";
4044

4145
export default class Status extends React.Component {
4246
constructor(props) {
@@ -153,13 +157,13 @@ export default class Status extends React.Component {
153157
mustLogout: userMustLogout,
154158
repeatLogin,
155159
} = userData;
156-
const mustLogin = this.resolveStoredValue(
160+
const mustLogin = resolveStoredValue(
157161
captivePortalSyncAuth,
158162
`${orgSlug}_mustLogin`,
159163
userMustLogin,
160164
cookies,
161165
);
162-
const mustLogout = this.resolveStoredValue(
166+
const mustLogout = resolveStoredValue(
163167
captivePortalSyncAuth,
164168
`${orgSlug}_mustLogout`,
165169
userMustLogout,
@@ -227,7 +231,7 @@ export default class Status extends React.Component {
227231
shouldLogin = shouldLogin && settings.payment_requires_internet;
228232
}
229233
if (this.loginFormRef && this.loginFormRef.current && shouldLogin) {
230-
this.storeValue(
234+
storeValue(
231235
captivePortalSyncAuth,
232236
`${orgSlug}_mustLogin`,
233237
false,
@@ -494,7 +498,7 @@ export default class Status extends React.Component {
494498
// After a successful payment, the user is redirected back to the status page.
495499
// If the user plan was previously exhausted, they need to be logged into the captive portal
496500
// to regain internet access. This ensures seamless browsing after upgrading their plan.
497-
this.storeValue(
501+
storeValue(
498502
captivePortalSyncAuth,
499503
`${orgSlug}_mustLogin`,
500504
true,
@@ -582,7 +586,7 @@ export default class Status extends React.Component {
582586
this.repeatLogin = true;
583587
}
584588
if (!internetMode) {
585-
this.storeValue(
589+
storeValue(
586590
captivePortalSyncAuth,
587591
`${orgSlug}_mustLogout`,
588592
true,
@@ -699,7 +703,7 @@ export default class Status extends React.Component {
699703
const userAutoLogin = localStorage.getItem("userAutoLogin") === "true";
700704
if (
701705
loggedOut ||
702-
this.resolveStoredValue(
706+
resolveStoredValue(
703707
captivePortalSyncAuth,
704708
`${orgSlug}_mustLogout`,
705709
false,
@@ -811,60 +815,6 @@ export default class Status extends React.Component {
811815
}
812816
};
813817

814-
// eslint-disable-next-line class-methods-use-this
815-
storeValue = (captivePortalSyncAuth, key, value, cookies) => {
816-
/**
817-
* Stores a value in both cookies and localStorage if synchronous
818-
* captive portal authentication is enabled.
819-
*
820-
* In synchronous authentication, submitting the captive portal form
821-
* triggers a page reload, which resets the component state.
822-
* Storing the value in cookies ensures it persists across reloads.
823-
*
824-
* The value is also saved in localStorage as a fallback in case the browser does not support cookies.
825-
*
826-
* @param {boolean} captivePortalSyncAuth - Whether synchronous authentication is enabled.
827-
* @param {string} key - The key under which the value is stored.
828-
* @param {boolean} value - The value to store.
829-
* @param {Cookies} cookies - The cookies instance used to set the cookie.
830-
*/
831-
if (!captivePortalSyncAuth) {
832-
return;
833-
}
834-
localStorage.setItem(key, value);
835-
cookies.set(key, value, {path: "/", maxAge: 60});
836-
};
837-
838-
// eslint-disable-next-line class-methods-use-this
839-
resolveStoredValue = (captivePortalSyncAuth, key, fallback, cookies) => {
840-
/**
841-
* Resolves the correct value by checking cookies, then localStorage,
842-
* falling back to a default value if neither is found.
843-
*
844-
* @param {boolean} captivePortalSyncAuth - Whether synchronization is enabled.
845-
* @param {string} cookieKey - The key to look for in cookies and localStorage.
846-
* @param {*} fallback - The fallback value if no valid stored value is found.
847-
* @returns {*} - The selected value based on storage or fallback.
848-
*/
849-
if (!captivePortalSyncAuth) {
850-
return fallback;
851-
}
852-
853-
const cookieValue = cookies.get(key);
854-
if (cookieValue !== undefined) {
855-
localStorage.removeItem(key);
856-
return cookieValue;
857-
}
858-
859-
const localStorageValue = localStorage.getItem(key);
860-
if (localStorageValue !== null) {
861-
localStorage.removeItem(key);
862-
return localStorageValue === "true";
863-
}
864-
865-
return fallback;
866-
};
867-
868818
updateScreenWidth = () => {
869819
this.setStateSafe({screenWidth: window.innerWidth});
870820
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {localStorage} from "./storage";
2+
3+
/**
4+
* Stores a value in both cookies and localStorage if synchronous
5+
* captive portal authentication is enabled.
6+
*
7+
* In synchronous authentication, submitting the captive portal form
8+
* triggers a page reload, which resets the component state.
9+
* Storing the value in cookies ensures it persists across reloads.
10+
*
11+
* The value is also saved in localStorage as a fallback in case the browser does not support cookies.
12+
*
13+
* @param {boolean} captivePortalSyncAuth - Whether synchronous authentication is enabled.
14+
* @param {string} key - The key under which the value is stored.
15+
* @param {boolean} value - The value to store.
16+
* @param {Cookies} cookies - The cookies instance used to set the cookie.
17+
*/
18+
export const storeValue = (captivePortalSyncAuth, key, value, cookies) => {
19+
if (!captivePortalSyncAuth) {
20+
return;
21+
}
22+
localStorage.setItem(key, value);
23+
cookies.set(key, value, {path: "/", maxAge: 60});
24+
};
25+
26+
/**
27+
* Resolves the correct value by checking cookies, then localStorage,
28+
* falling back to a default value if neither is found.
29+
*
30+
* @param {boolean} captivePortalSyncAuth - Whether synchronization is enabled.
31+
* @param {string} key - The key to look for in cookies and localStorage.
32+
* @param {*} fallback - The fallback value if no valid stored value is found.
33+
* @param {Cookies} cookies - The cookies instance used to get the cookie.
34+
* @returns {*} - The selected value based on storage or fallback.
35+
*/
36+
export const resolveStoredValue = (
37+
captivePortalSyncAuth,
38+
key,
39+
fallback,
40+
cookies,
41+
) => {
42+
if (!captivePortalSyncAuth) {
43+
return fallback;
44+
}
45+
46+
const cookieValue = cookies.get(key);
47+
if (cookieValue !== undefined) {
48+
localStorage.removeItem(key);
49+
return cookieValue === true || cookieValue === "true";
50+
}
51+
52+
const localStorageValue = localStorage.getItem(key);
53+
if (localStorageValue !== null) {
54+
localStorage.removeItem(key);
55+
return localStorageValue === "true";
56+
}
57+
58+
return fallback;
59+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {resolveStoredValue} from "./captive-portal-storage";
2+
import {localStorage} from "./storage";
3+
4+
// Mock the internal storage dependency
5+
jest.mock("./storage", () => ({
6+
localStorage: {
7+
getItem: jest.fn(),
8+
setItem: jest.fn(),
9+
removeItem: jest.fn(),
10+
},
11+
}));
12+
13+
describe("captive-portal-storage utility", () => {
14+
let mockCookies;
15+
16+
beforeEach(() => {
17+
// Clear mocks before each test
18+
jest.clearAllMocks();
19+
20+
mockCookies = {
21+
get: jest.fn(),
22+
set: jest.fn(),
23+
};
24+
});
25+
26+
describe("resolveStoredValue", () => {
27+
const key = "testKey";
28+
const fallback = "fallbackValue";
29+
30+
it("returns fallback when captivePortalSyncAuth is false", () => {
31+
const result = resolveStoredValue(false, key, fallback, mockCookies);
32+
expect(result).toBe(fallback);
33+
expect(mockCookies.get).not.toHaveBeenCalled();
34+
});
35+
36+
it("returns true and removes localStorage when cookie exists", () => {
37+
mockCookies.get.mockReturnValue("true"); // Cookie exists
38+
39+
const result = resolveStoredValue(true, key, fallback, mockCookies);
40+
41+
expect(result).toBe(true);
42+
expect(localStorage.removeItem).toHaveBeenCalledWith(key);
43+
});
44+
45+
it("returns true and removes localStorage when cookie is undefined but localStorage exists", () => {
46+
mockCookies.get.mockReturnValue(undefined); // No cookie
47+
localStorage.getItem.mockReturnValue("true"); // Has localStorage
48+
49+
const result = resolveStoredValue(true, key, fallback, mockCookies);
50+
51+
expect(result).toBe(true);
52+
expect(localStorage.removeItem).toHaveBeenCalledWith(key);
53+
});
54+
55+
it("returns fallback when neither cookie nor localStorage is present", () => {
56+
mockCookies.get.mockReturnValue(undefined);
57+
localStorage.getItem.mockReturnValue(null);
58+
59+
const result = resolveStoredValue(true, key, fallback, mockCookies);
60+
61+
expect(result).toBe(fallback);
62+
});
63+
});
64+
});

0 commit comments

Comments
 (0)