Skip to content

Commit ee9c833

Browse files
committed
more tests
1 parent 740c286 commit ee9c833

3 files changed

Lines changed: 104 additions & 7 deletions

File tree

redisinsight/api/test/api/array/POST-databases-id-array-get_next_index.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,44 @@ describe('POST /databases/:instanceId/array/get-next-index', () => {
107107
});
108108
});
109109

110+
it('Should return null when the cursor is exhausted', async () => {
111+
const keyName = constants.getRandomString();
112+
// Reach exhaustion: ARSEEK to the max valid index (2^64-2) then
113+
// ARINSERT one element. The next slot would be 2^64-1, which Redis
114+
// reserves as the no-index sentinel — ARNEXT reports the cursor as
115+
// exhausted via a nil reply, which toIndexString must surface as
116+
// JSON null (NOT the literal string "null").
117+
await rte.client.call('ARSEEK', keyName, '18446744073709551614');
118+
await rte.client.call('ARINSERT', keyName, 'last');
119+
120+
await validateApiCall({
121+
endpoint,
122+
data: { keyName },
123+
responseSchema,
124+
responseBody: { keyName, index: null },
125+
});
126+
});
127+
128+
it('Should round-trip a cursor above MAX_SAFE_INTEGER', async () => {
129+
const keyName = constants.getRandomString();
130+
// ARSEEK to a u64 cursor that exceeds Number.MAX_SAFE_INTEGER (2^53-1).
131+
// If the wire path ever decodes the integer reply as a JS number,
132+
// precision is silently lost — this assertion catches that regression.
133+
const hugeCursor = '9223372036854775818';
134+
await rte.client.call('ARSEEK', keyName, hugeCursor);
135+
136+
await validateApiCall({
137+
endpoint,
138+
data: { keyName },
139+
responseSchema,
140+
responseBody: { keyName, index: hugeCursor },
141+
checkFn: ({ body }: any) => {
142+
expect(typeof body.index).to.eql('string');
143+
expect(body.index).to.eql(hugeCursor);
144+
},
145+
});
146+
});
147+
110148
[
111149
{
112150
name: 'Should return BadRequest if key holds a non-array type',

redisinsight/api/test/api/array/POST-databases-id-array-scan.test.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ describe('POST /databases/:instanceId/array/scan', () => {
118118
},
119119
statusCode: 400,
120120
},
121+
{
122+
// DTO contract: @IsOptional + @IsInt + @Min(1). limit:0 must fail
123+
// validation before it can become a no-op `LIMIT 0` at Redis.
124+
name: 'Should reject limit: 0 (below @Min(1))',
125+
data: {
126+
keyName: constants.getRandomString(),
127+
start: '0',
128+
end: '5',
129+
limit: 0,
130+
},
131+
statusCode: 400,
132+
},
121133
].map(mainCheckFn);
122134
});
123135

@@ -162,16 +174,14 @@ describe('POST /databases/:instanceId/array/scan', () => {
162174
});
163175
});
164176

165-
// Skipped: ARSCAN's response `index` is returned by Redis as a RESP
166-
// integer, which ioredis decodes to a JS number — values above 2^53
167-
// round before they reach the API. Round-tripping the request side at
168-
// the upper edge is covered by /get-range and /get-element (neither
169-
// echoes the index in the response). Remove .skip once the integer-
170-
// reply precision path is fixed (see PR #6064 discussion).
171-
it.skip('Should round-trip the index when scanning at the maximum valid index (2^64-2)', async () => {
177+
it('Should round-trip the index when scanning at the maximum valid index (2^64-2)', async () => {
172178
const keyName = constants.getRandomString();
173179
const maxIndex = '18446744073709551614';
174180

181+
// Redis 8.8 returns u64 values ≥ 2^63 as RESP bulk strings (not RESP
182+
// integers), so ARSCAN's `index` field survives the wire intact for
183+
// values in this range. Locks in the contract that the API surfaces
184+
// the index as a decimal string even at the upper edge of u64.
175185
await rte.client.call('ARSET', keyName, maxIndex, 'edge');
176186

177187
await validateApiCall({
@@ -220,6 +230,24 @@ describe('POST /databases/:instanceId/array/scan', () => {
220230
});
221231
});
222232

233+
it('Should accept a span of exactly 1,000,000 elements', async () => {
234+
const keyName = constants.getRandomString();
235+
await rte.client.call('ARSET', keyName, '0', 'x');
236+
237+
// Boundary case: span = end - start + 1 = 1_000_000 → just within cap.
238+
// Mirror of the get-range boundary test — both endpoints share the
239+
// span validator.
240+
await validateApiCall({
241+
endpoint,
242+
data: { keyName, start: '0', end: '999999' },
243+
responseSchema,
244+
responseBody: {
245+
keyName,
246+
elements: [{ index: '0', value: 'x' }],
247+
},
248+
});
249+
});
250+
223251
it('Should return an empty list when the range covers only empty slots', async () => {
224252
const keyName = constants.getRandomString();
225253
await seedSparse(keyName);

redisinsight/api/test/api/array/POST-databases-id-array.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ describe('POST /databases/:id/array', () => {
120120
describe('Sparse (ARMSET)', () => {
121121
const sparseKey = constants.getRandomString();
122122
const sparseTtlKey = constants.getRandomString();
123+
const dupIndexKey = constants.getRandomString();
123124

124125
[
125126
{
@@ -157,6 +158,25 @@ describe('POST /databases/:id/array', () => {
157158
expect(await rte.client.ttl(sparseTtlKey)).to.gte(95);
158159
},
159160
},
161+
{
162+
// Pin "last write wins" for duplicate indexes — ARMSET accepts the
163+
// duplicate server-side and the trailing value overwrites the earlier
164+
// one. Count stays 1 because only one slot ends up populated.
165+
name: 'Should accept a duplicate index and keep the last value (sparse, last wins)',
166+
data: {
167+
keyName: dupIndexKey,
168+
mode: ArrayCreationMode.Sparse,
169+
elements: [
170+
{ index: '5', value: 'first' },
171+
{ index: '5', value: 'second' },
172+
],
173+
},
174+
statusCode: 201,
175+
after: async () => {
176+
expect(await arget(dupIndexKey, '5')).to.eql('second');
177+
expect(await arcount(dupIndexKey)).to.eql(1);
178+
},
179+
},
160180
].map(createCheckFn);
161181
});
162182

@@ -202,6 +222,17 @@ describe('POST /databases/:id/array', () => {
202222
},
203223
statusCode: 400,
204224
},
225+
{
226+
// Symmetric @ArrayMinSize(1) guard for the sparse path — without it,
227+
// we'd issue ARMSET with no pairs and surface the server error.
228+
name: 'Should reject sparse mode with an empty elements array',
229+
data: {
230+
keyName: constants.getRandomString(),
231+
mode: ArrayCreationMode.Sparse,
232+
elements: [],
233+
},
234+
statusCode: 400,
235+
},
205236
{
206237
name: 'Should reject an unknown mode',
207238
data: {

0 commit comments

Comments
 (0)