Skip to content

Commit 5cafab8

Browse files
sirdeggenclaude
andauthored
test(wallet-toolbox): unit tests for entityValidationHelpers (#61) (#62)
Add Jest unit tests for the four exports of packages/wallet/wallet-toolbox/src/storage/remoting/entityValidationHelpers.ts to recover SonarCloud's new-coverage gate after PR #55. Covers validateDate (Date / ISO string / number / epoch / unparsable), validateEntity (string-to-Date coercion, null-to-undefined, Uint8Array and Buffer to number[] equivalence, custom dateFields, in-place mutation), validateEntities (non-array passthrough, empty / single / multi, dateFields propagation), and validateSyncChunkEntities (empty chunk, user-only, all-arrays-populated, undefined / empty array handling). 100% statement / branch / function / line coverage on the file. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b6f30b1 commit 5cafab8

1 file changed

Lines changed: 310 additions & 0 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import {
2+
validateDate,
3+
validateEntities,
4+
validateEntity,
5+
validateSyncChunkEntities
6+
} from '../entityValidationHelpers'
7+
import { EntityTimeStamp } from '../../../sdk/types'
8+
import { SyncChunk } from '../../../sdk/WalletStorage.interfaces'
9+
10+
interface TestEntity extends EntityTimeStamp {
11+
id?: number
12+
name?: string | null
13+
blob?: Uint8Array | number[] | null
14+
ts?: Date | string
15+
optional?: string | null
16+
[key: string]: unknown
17+
}
18+
19+
const isoFromMs = (ms: number): string => new Date(ms).toISOString()
20+
21+
const makeEntity = (overrides: Partial<TestEntity> = {}): TestEntity => ({
22+
created_at: new Date('2024-01-01T00:00:00.000Z'),
23+
updated_at: new Date('2024-01-02T00:00:00.000Z'),
24+
...overrides
25+
})
26+
27+
describe('entityValidationHelpers', () => {
28+
describe('validateDate', () => {
29+
test('returns the same Date instance when input is already a Date', () => {
30+
const d = new Date('2024-06-15T12:34:56.000Z')
31+
const result = validateDate(d)
32+
expect(result).toBe(d)
33+
expect(result).toBeInstanceOf(Date)
34+
})
35+
36+
test('parses an ISO date string into a Date', () => {
37+
const iso = '2023-03-04T05:06:07.000Z'
38+
const result = validateDate(iso)
39+
expect(result).toBeInstanceOf(Date)
40+
expect(result.toISOString()).toBe(iso)
41+
})
42+
43+
test('parses a numeric timestamp into a Date', () => {
44+
const ms = 1_700_000_000_000
45+
const result = validateDate(ms)
46+
expect(result).toBeInstanceOf(Date)
47+
expect(result.getTime()).toBe(ms)
48+
})
49+
50+
test('handles epoch (0) numeric input', () => {
51+
const result = validateDate(0)
52+
expect(result).toBeInstanceOf(Date)
53+
expect(result.getTime()).toBe(0)
54+
})
55+
56+
test('returns an Invalid Date when given an unparsable string', () => {
57+
const result = validateDate('not-a-real-date')
58+
expect(result).toBeInstanceOf(Date)
59+
expect(Number.isNaN(result.getTime())).toBe(true)
60+
})
61+
})
62+
63+
describe('validateEntity', () => {
64+
test('coerces created_at and updated_at strings to Date instances', () => {
65+
const e: TestEntity = {
66+
created_at: '2024-01-01T00:00:00.000Z' as unknown as Date,
67+
updated_at: '2024-01-02T00:00:00.000Z' as unknown as Date
68+
}
69+
const result = validateEntity(e)
70+
expect(result.created_at).toBeInstanceOf(Date)
71+
expect(result.updated_at).toBeInstanceOf(Date)
72+
expect(result.created_at.toISOString()).toBe('2024-01-01T00:00:00.000Z')
73+
expect(result.updated_at.toISOString()).toBe('2024-01-02T00:00:00.000Z')
74+
})
75+
76+
test('preserves existing Date instances unchanged', () => {
77+
const created = new Date('2024-01-01T00:00:00.000Z')
78+
const updated = new Date('2024-01-02T00:00:00.000Z')
79+
const e = makeEntity({ created_at: created, updated_at: updated })
80+
const result = validateEntity(e)
81+
expect(result.created_at).toBe(created)
82+
expect(result.updated_at).toBe(updated)
83+
})
84+
85+
test('replaces null fields with undefined', () => {
86+
const e = makeEntity({ name: null, optional: null })
87+
const result = validateEntity(e)
88+
expect(result.name).toBeUndefined()
89+
expect(result.optional).toBeUndefined()
90+
// The keys are still present; their values are now undefined.
91+
expect('name' in result).toBe(true)
92+
expect('optional' in result).toBe(true)
93+
})
94+
95+
test('converts Uint8Array fields to plain number[]', () => {
96+
const bytes = new Uint8Array([0, 1, 2, 250, 255])
97+
const e = makeEntity({ blob: bytes })
98+
const result = validateEntity(e)
99+
expect(Array.isArray(result.blob)).toBe(true)
100+
expect(result.blob).not.toBeInstanceOf(Uint8Array)
101+
expect(result.blob).toEqual([0, 1, 2, 250, 255])
102+
})
103+
104+
test('converts Buffer fields to plain number[] (Buffer is a Uint8Array)', () => {
105+
const buf = Buffer.from([10, 20, 30, 40])
106+
const e = makeEntity({ blob: buf as unknown as Uint8Array })
107+
const result = validateEntity(e)
108+
expect(Array.isArray(result.blob)).toBe(true)
109+
expect(Buffer.isBuffer(result.blob)).toBe(false)
110+
expect(result.blob).not.toBeInstanceOf(Uint8Array)
111+
expect(result.blob).toEqual([10, 20, 30, 40])
112+
})
113+
114+
test('coerces additional date fields supplied via dateFields argument', () => {
115+
const e = makeEntity({ ts: '2025-05-05T00:00:00.000Z' })
116+
const result = validateEntity(e, ['ts'])
117+
expect(result.ts).toBeInstanceOf(Date)
118+
expect((result.ts as Date).toISOString()).toBe('2025-05-05T00:00:00.000Z')
119+
})
120+
121+
test('skips falsy custom date fields without throwing', () => {
122+
const e = makeEntity({ ts: undefined })
123+
// ts is undefined (falsy) so the helper should not attempt to coerce it.
124+
const result = validateEntity(e, ['ts', 'missingField'])
125+
expect(result.ts).toBeUndefined()
126+
})
127+
128+
test('coerces a numeric custom date field', () => {
129+
const ms = 1_700_000_000_000
130+
const e = makeEntity({ ts: ms as unknown as Date })
131+
const result = validateEntity(e, ['ts'])
132+
expect(result.ts).toBeInstanceOf(Date)
133+
expect((result.ts as Date).getTime()).toBe(ms)
134+
})
135+
136+
test('returns the same object reference (mutates in place)', () => {
137+
const e = makeEntity({ name: null })
138+
const result = validateEntity(e)
139+
expect(result).toBe(e)
140+
})
141+
142+
test('handles an entity with no optional or nullable fields', () => {
143+
const e = makeEntity({ id: 7, name: 'alice' })
144+
const result = validateEntity(e)
145+
expect(result.id).toBe(7)
146+
expect(result.name).toBe('alice')
147+
expect(result.created_at).toBeInstanceOf(Date)
148+
expect(result.updated_at).toBeInstanceOf(Date)
149+
})
150+
151+
test('processes a mixed entity with nulls, Uint8Array, and string dates together', () => {
152+
const e: TestEntity = {
153+
created_at: isoFromMs(1_000_000_000_000) as unknown as Date,
154+
updated_at: isoFromMs(1_000_000_001_000) as unknown as Date,
155+
id: 1,
156+
name: null,
157+
blob: new Uint8Array([9, 8, 7])
158+
}
159+
const result = validateEntity(e)
160+
expect(result.created_at).toBeInstanceOf(Date)
161+
expect(result.updated_at).toBeInstanceOf(Date)
162+
expect(result.name).toBeUndefined()
163+
expect(result.blob).toEqual([9, 8, 7])
164+
expect(result.id).toBe(1)
165+
})
166+
})
167+
168+
describe('validateEntities', () => {
169+
test('returns the input unchanged when it is not an array', () => {
170+
const notArray = { foo: 'bar' } as unknown as TestEntity[]
171+
const result = validateEntities(notArray)
172+
expect(result).toBe(notArray)
173+
})
174+
175+
test('returns an empty array unchanged', () => {
176+
const arr: TestEntity[] = []
177+
const result = validateEntities(arr)
178+
expect(result).toBe(arr)
179+
expect(result).toEqual([])
180+
})
181+
182+
test('validates a single-entity array', () => {
183+
const arr: TestEntity[] = [makeEntity({ name: null })]
184+
const result = validateEntities(arr)
185+
expect(result).toBe(arr)
186+
expect(result[0].name).toBeUndefined()
187+
expect(result[0].created_at).toBeInstanceOf(Date)
188+
})
189+
190+
test('validates every entity in a multi-entity array', () => {
191+
const arr: TestEntity[] = [
192+
{ created_at: '2024-01-01T00:00:00.000Z' as unknown as Date, updated_at: '2024-01-01T00:00:00.000Z' as unknown as Date, name: null },
193+
makeEntity({ blob: new Uint8Array([1, 2, 3]) }),
194+
makeEntity({ blob: Buffer.from([4, 5, 6]) as unknown as Uint8Array })
195+
]
196+
const result = validateEntities(arr)
197+
expect(result).toHaveLength(3)
198+
expect(result[0].created_at).toBeInstanceOf(Date)
199+
expect(result[0].name).toBeUndefined()
200+
expect(result[1].blob).toEqual([1, 2, 3])
201+
expect(result[2].blob).toEqual([4, 5, 6])
202+
expect(result[2].blob).not.toBeInstanceOf(Uint8Array)
203+
})
204+
205+
test('passes dateFields through to each entity', () => {
206+
const arr: TestEntity[] = [
207+
makeEntity({ ts: '2024-06-01T00:00:00.000Z' }),
208+
makeEntity({ ts: '2024-06-02T00:00:00.000Z' })
209+
]
210+
const result = validateEntities(arr, ['ts'])
211+
expect(result[0].ts).toBeInstanceOf(Date)
212+
expect(result[1].ts).toBeInstanceOf(Date)
213+
})
214+
})
215+
216+
describe('validateSyncChunkEntities', () => {
217+
const baseChunk = (): SyncChunk => ({
218+
fromStorageIdentityKey: 'from-key',
219+
toStorageIdentityKey: 'to-key',
220+
userIdentityKey: 'user-key'
221+
})
222+
223+
test('returns a chunk with no entity arrays unchanged', () => {
224+
const chunk = baseChunk()
225+
const result = validateSyncChunkEntities(chunk)
226+
expect(result).toBe(chunk)
227+
expect(result.user).toBeUndefined()
228+
expect(result.provenTxs).toBeUndefined()
229+
})
230+
231+
test('validates the user entity when present', () => {
232+
const chunk: SyncChunk = {
233+
...baseChunk(),
234+
user: makeEntity({ name: null }) as never
235+
}
236+
const result = validateSyncChunkEntities(chunk)
237+
expect(result.user).toBeDefined()
238+
// The user object should have been mutated by validateEntity.
239+
expect((result.user as unknown as TestEntity).name).toBeUndefined()
240+
expect((result.user as unknown as TestEntity).created_at).toBeInstanceOf(Date)
241+
})
242+
243+
test('validates each populated entity-array property in place', () => {
244+
const chunk: SyncChunk = {
245+
...baseChunk(),
246+
provenTxs: [makeEntity({ blob: new Uint8Array([1, 2]) })] as never,
247+
provenTxReqs: [makeEntity({ blob: Buffer.from([3, 4]) as unknown as Uint8Array })] as never,
248+
outputBaskets: [makeEntity({ name: null })] as never,
249+
txLabels: [makeEntity()] as never,
250+
outputTags: [makeEntity()] as never,
251+
transactions: [makeEntity()] as never,
252+
txLabelMaps: [makeEntity()] as never,
253+
commissions: [makeEntity()] as never,
254+
outputs: [makeEntity()] as never,
255+
outputTagMaps: [makeEntity()] as never,
256+
certificates: [makeEntity()] as never,
257+
certificateFields: [makeEntity()] as never,
258+
user: makeEntity() as never
259+
}
260+
const result = validateSyncChunkEntities(chunk)
261+
expect(result).toBe(chunk)
262+
expect((result.provenTxs as unknown as TestEntity[])[0].blob).toEqual([1, 2])
263+
expect((result.provenTxReqs as unknown as TestEntity[])[0].blob).toEqual([3, 4])
264+
expect((result.outputBaskets as unknown as TestEntity[])[0].name).toBeUndefined()
265+
// Spot-check that all timestamp fields were processed.
266+
const everyArrayKey: Array<keyof SyncChunk> = [
267+
'provenTxs',
268+
'provenTxReqs',
269+
'outputBaskets',
270+
'txLabels',
271+
'outputTags',
272+
'transactions',
273+
'txLabelMaps',
274+
'commissions',
275+
'outputs',
276+
'outputTagMaps',
277+
'certificates',
278+
'certificateFields'
279+
]
280+
for (const k of everyArrayKey) {
281+
const arr = result[k] as unknown as TestEntity[]
282+
expect(Array.isArray(arr)).toBe(true)
283+
expect(arr[0].created_at).toBeInstanceOf(Date)
284+
expect(arr[0].updated_at).toBeInstanceOf(Date)
285+
}
286+
})
287+
288+
test('skips properties that are explicitly undefined', () => {
289+
const chunk: SyncChunk = {
290+
...baseChunk(),
291+
provenTxs: undefined,
292+
user: undefined
293+
}
294+
const result = validateSyncChunkEntities(chunk)
295+
expect(result.provenTxs).toBeUndefined()
296+
expect(result.user).toBeUndefined()
297+
})
298+
299+
test('handles empty entity arrays without error', () => {
300+
const chunk: SyncChunk = {
301+
...baseChunk(),
302+
provenTxs: [],
303+
certificates: []
304+
}
305+
const result = validateSyncChunkEntities(chunk)
306+
expect(result.provenTxs).toEqual([])
307+
expect(result.certificates).toEqual([])
308+
})
309+
})
310+
})

0 commit comments

Comments
 (0)