Skip to content

Commit 73220e7

Browse files
committed
fix: Ensure arrays in state.hash are not allowed
1 parent f489240 commit 73220e7

File tree

2 files changed

+176
-1
lines changed

2 files changed

+176
-1
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { describe, expect, test } from "vitest";
2+
import { isConformantState } from "./isConformantState.js";
3+
4+
describe("isConformantState", () => {
5+
describe("Valid State objects", () => {
6+
test("Should return true for minimal valid state with empty hash object.", () => {
7+
const state = { hash: {} };
8+
expect(isConformantState(state)).toBe(true);
9+
});
10+
11+
test("Should return true for valid state with hash containing string values.", () => {
12+
const state = { hash: { path: "/home", id: "main" } };
13+
expect(isConformantState(state)).toBe(true);
14+
});
15+
16+
test("Should return true for valid state with hash containing mixed value types.", () => {
17+
const state = {
18+
hash: {
19+
path: "/home",
20+
count: 42,
21+
active: true,
22+
data: { nested: "value" }
23+
}
24+
};
25+
expect(isConformantState(state)).toBe(true);
26+
});
27+
28+
test("Should return true for valid state with additional properties beyond hash.", () => {
29+
const state = {
30+
hash: { path: "/home" },
31+
extraData: "allowed",
32+
metadata: { timestamp: Date.now() }
33+
};
34+
expect(isConformantState(state)).toBe(true);
35+
});
36+
37+
test("Should return true for valid state with nested hash object.", () => {
38+
const state = {
39+
hash: {
40+
main: "/home",
41+
sidebar: "/menu",
42+
modal: null,
43+
nested: { deep: { value: "test" } }
44+
}
45+
};
46+
expect(isConformantState(state)).toBe(true);
47+
});
48+
});
49+
50+
describe("Invalid State objects", () => {
51+
test("Should return false for null.", () => {
52+
expect(isConformantState(null)).toBe(false);
53+
});
54+
55+
test("Should return false for undefined.", () => {
56+
expect(isConformantState(undefined)).toBe(false);
57+
});
58+
59+
test("Should return false for primitive string.", () => {
60+
expect(isConformantState("not an object")).toBe(false);
61+
});
62+
63+
test("Should return false for primitive number.", () => {
64+
expect(isConformantState(42)).toBe(false);
65+
});
66+
67+
test("Should return false for primitive boolean.", () => {
68+
expect(isConformantState(true)).toBe(false);
69+
});
70+
71+
test("Should return false for array.", () => {
72+
expect(isConformantState([1, 2, 3])).toBe(false);
73+
});
74+
75+
test("Should return false for object without hash property.", () => {
76+
const state = { data: "value", path: "/home" };
77+
expect(isConformantState(state)).toBe(false);
78+
});
79+
80+
test("Should return false for object with null hash.", () => {
81+
const state = { hash: null };
82+
expect(isConformantState(state)).toBe(false);
83+
});
84+
85+
test("Should return false for object with undefined hash.", () => {
86+
const state = { hash: undefined };
87+
expect(isConformantState(state)).toBe(false);
88+
});
89+
90+
test("Should return false for object with string hash.", () => {
91+
const state = { hash: "string-value" };
92+
expect(isConformantState(state)).toBe(false);
93+
});
94+
95+
test("Should return false for object with number hash.", () => {
96+
const state = { hash: 42 };
97+
expect(isConformantState(state)).toBe(false);
98+
});
99+
100+
test("Should return false for object with boolean hash.", () => {
101+
const state = { hash: true };
102+
expect(isConformantState(state)).toBe(false);
103+
});
104+
105+
test("Should return false for object with function hash.", () => {
106+
const state = { hash: () => {} };
107+
expect(isConformantState(state)).toBe(false);
108+
});
109+
110+
test("Should return false for object with array hash.", () => {
111+
const state = { hash: ["path1", "path2"] };
112+
expect(isConformantState(state)).toBe(false);
113+
});
114+
});
115+
116+
describe("Edge cases", () => {
117+
test("Should return true for empty object with empty hash.", () => {
118+
const state = { hash: {} };
119+
expect(isConformantState(state)).toBe(true);
120+
});
121+
122+
test("Should return true for state with Date object in hash.", () => {
123+
const state = { hash: { timestamp: new Date() } };
124+
expect(isConformantState(state)).toBe(true);
125+
});
126+
127+
test("Should return true for state with RegExp object in hash.", () => {
128+
const state = { hash: { pattern: /test/ } };
129+
expect(isConformantState(state)).toBe(true);
130+
});
131+
132+
test("Should return true for state with Error object in hash.", () => {
133+
const state = { hash: { error: new Error("test") } };
134+
expect(isConformantState(state)).toBe(true);
135+
});
136+
137+
test("Should return false for object with Symbol as hash property key.", () => {
138+
const symbolKey = Symbol('hash');
139+
const state = { [symbolKey]: {} };
140+
expect(isConformantState(state)).toBe(false);
141+
});
142+
143+
test("Should handle circular references in hash object.", () => {
144+
const state: any = { hash: {} };
145+
state.hash.circular = state.hash;
146+
expect(isConformantState(state)).toBe(true);
147+
});
148+
});
149+
150+
describe("Type guard behavior", () => {
151+
test("Should work as type guard for TypeScript type narrowing.", () => {
152+
const unknownValue: unknown = { hash: { path: "/test" } };
153+
154+
if (isConformantState(unknownValue)) {
155+
// TypeScript should recognize this as State type
156+
expect(unknownValue.hash).toBeDefined();
157+
expect(typeof unknownValue.hash).toBe('object');
158+
} else {
159+
throw new Error("Should have passed type guard");
160+
}
161+
});
162+
163+
test("Should correctly reject invalid state in type guard context.", () => {
164+
const unknownValue: unknown = { data: "not a state" };
165+
166+
if (isConformantState(unknownValue)) {
167+
throw new Error("Should not have passed type guard");
168+
} else {
169+
// This path should be taken
170+
expect(unknownValue).toBeDefined();
171+
}
172+
});
173+
});
174+
});

src/lib/core/isConformantState.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export function isConformantState(state: unknown): state is State {
1313
&& state !== null
1414
&& 'hash' in state
1515
&& typeof state.hash === 'object'
16-
&& state.hash !== null;
16+
&& state.hash !== null
17+
&& !Array.isArray(state.hash);
1718
}

0 commit comments

Comments
 (0)