|
1 | 1 | import { describe, it, expect } from 'bun:test'; |
2 | | -import { getSortDir } from '../../src/api/routes/v2-history/get_actions/functions.js'; |
| 2 | +import { getSortDir, applyTimeFilter, isBlockNumber } from '../../src/api/routes/v2-history/get_actions/functions.js'; |
3 | 3 |
|
4 | 4 | describe('getSortDir', () => { |
5 | 5 |
|
@@ -50,12 +50,11 @@ describe('getSortDir', () => { |
50 | 50 | }); |
51 | 51 |
|
52 | 52 | // sort=asc with invalid bounds |
53 | | - it('should throw for sort=asc with after="0" (neither valid date with T nor positive int)', () => { |
54 | | - // "0" is truthy, new Date("0") parses to 2000-01-01 (valid), Number("0") = 0 (not > 0) |
55 | | - // isValidBound: Date parse succeeds → passes bounds check |
56 | | - // But "0" doesn't contain 'T', so max window check is skipped → returns asc |
57 | | - // This is acceptable: "0" as a date will produce @timestamp range that ES handles |
58 | | - expect(getSortDir({ sort: 'asc', after: '0' })).toBe('asc'); |
| 53 | + it('should reject sort=asc with after="0" (not a positive int → treated as a date, year 2000, outside the window)', () => { |
| 54 | + // "0" is not a positive integer, so it is classified as a date. new Date("0") |
| 55 | + // resolves to year 2000 — far outside the recency window — so it is now rejected |
| 56 | + // instead of silently bypassing the guard. |
| 57 | + expect(() => getSortDir({ sort: 'asc', after: '0' })).toThrow('within the last'); |
59 | 58 | }); |
60 | 59 |
|
61 | 60 | it('should throw for sort=asc with after=0 (falsy)', () => { |
@@ -88,4 +87,94 @@ describe('getSortDir', () => { |
88 | 87 | it('should not apply max window check on block numbers', () => { |
89 | 88 | expect(getSortDir({ sort: 'asc', after: '100' })).toBe('asc'); |
90 | 89 | }); |
| 90 | + |
| 91 | + // Date strings WITHOUT a 'T' separator must still be window-checked (previously they |
| 92 | + // slipped past because the check only fired on strings containing 'T'). |
| 93 | + it('should reject sort=asc with an old date that has no T separator', () => { |
| 94 | + expect(() => getSortDir({ sort: 'asc', after: '2020-01-01' })).toThrow('within the last'); |
| 95 | + }); |
| 96 | + |
| 97 | + it('should allow sort=asc with a recent date that has no T separator', () => { |
| 98 | + const todayDateOnly = new Date(Date.now() - 3600000).toISOString().split('T')[0]; |
| 99 | + expect(getSortDir({ sort: 'asc', after: todayDateOnly })).toBe('asc'); |
| 100 | + }); |
| 101 | +}); |
| 102 | + |
| 103 | +describe('applyTimeFilter (mixed date / block-number bounds)', () => { |
| 104 | + |
| 105 | + const newStruct = () => ({ bool: { must: [], boost: 1.0 } as any }); |
| 106 | + |
| 107 | + // Regression: this exact combination used to return 400 "Invalid time value [after]" |
| 108 | + it('block-number "after" + ISO-date "before" produces both ranges', () => { |
| 109 | + const qs = newStruct(); |
| 110 | + applyTimeFilter({ after: '437506277', before: '2026-06-01T08:06:13' }, qs); |
| 111 | + const blockR = qs.bool.filter.find((f: any) => f.range.block_num); |
| 112 | + const tsR = qs.bool.filter.find((f: any) => f.range['@timestamp']); |
| 113 | + expect(blockR.range.block_num.gte).toBe('437506277'); |
| 114 | + expect(blockR.range.block_num.lte).toBeUndefined(); |
| 115 | + expect(tsR.range['@timestamp'].lte).toBe(new Date('2026-06-01T08:06:13').toISOString()); |
| 116 | + expect(tsR.range['@timestamp'].gte).toBeUndefined(); |
| 117 | + }); |
| 118 | + |
| 119 | + it('ISO-date "after" + block-number "before" produces both ranges', () => { |
| 120 | + const qs = newStruct(); |
| 121 | + applyTimeFilter({ after: '2025-01-01T00:00:00Z', before: '500000000' }, qs); |
| 122 | + const blockR = qs.bool.filter.find((f: any) => f.range.block_num); |
| 123 | + const tsR = qs.bool.filter.find((f: any) => f.range['@timestamp']); |
| 124 | + expect(tsR.range['@timestamp'].gte).toBe(new Date('2025-01-01T00:00:00Z').toISOString()); |
| 125 | + expect(blockR.range.block_num.lte).toBe('500000000'); |
| 126 | + }); |
| 127 | + |
| 128 | + it('two block numbers collapse into a single block_num range', () => { |
| 129 | + const qs = newStruct(); |
| 130 | + applyTimeFilter({ after: '100', before: '200' }, qs); |
| 131 | + expect(qs.bool.filter.length).toBe(1); |
| 132 | + expect(qs.bool.filter[0].range.block_num).toEqual({ gte: '100', lte: '200' }); |
| 133 | + }); |
| 134 | + |
| 135 | + it('two ISO dates collapse into a single @timestamp range', () => { |
| 136 | + const qs = newStruct(); |
| 137 | + applyTimeFilter({ after: '2026-01-01T00:00:00Z', before: '2026-02-01T00:00:00Z' }, qs); |
| 138 | + expect(qs.bool.filter.length).toBe(1); |
| 139 | + expect(qs.bool.filter[0].range['@timestamp']).toEqual({ |
| 140 | + gte: new Date('2026-01-01T00:00:00Z').toISOString(), |
| 141 | + lte: new Date('2026-02-01T00:00:00Z').toISOString() |
| 142 | + }); |
| 143 | + }); |
| 144 | + |
| 145 | + it('space-separated datetime is normalized to ISO and filters on @timestamp', () => { |
| 146 | + const qs = newStruct(); |
| 147 | + applyTimeFilter({ after: '2026-01-01 00:00:00' }, qs); |
| 148 | + expect(qs.bool.filter[0].range['@timestamp'].gte).toBe(new Date('2026-01-01T00:00:00').toISOString()); |
| 149 | + }); |
| 150 | + |
| 151 | + it('no bounds → no filter added', () => { |
| 152 | + const qs = newStruct(); |
| 153 | + applyTimeFilter({}, qs); |
| 154 | + expect(qs.bool.filter).toBeUndefined(); |
| 155 | + }); |
| 156 | + |
| 157 | + // Regression: a date string without 'T' must be a @timestamp bound, not block 2026 |
| 158 | + it('classifies a date-only string (no T) as a @timestamp bound, not a block number', () => { |
| 159 | + const qs = newStruct(); |
| 160 | + applyTimeFilter({ after: '2026-01-01' }, qs); |
| 161 | + expect(qs.bool.filter.find((f: any) => f.range.block_num)).toBeUndefined(); |
| 162 | + expect(qs.bool.filter[0].range['@timestamp'].gte).toBe(new Date('2026-01-01').toISOString()); |
| 163 | + }); |
| 164 | +}); |
| 165 | + |
| 166 | +describe('isBlockNumber', () => { |
| 167 | + it('treats bare positive integers (string or number) as block numbers', () => { |
| 168 | + expect(isBlockNumber('437506277')).toBe(true); |
| 169 | + expect(isBlockNumber(437506277)).toBe(true); |
| 170 | + expect(isBlockNumber('1')).toBe(true); |
| 171 | + }); |
| 172 | + |
| 173 | + it('treats dates, zero, and non-integers as NOT block numbers', () => { |
| 174 | + expect(isBlockNumber('2026-01-01')).toBe(false); |
| 175 | + expect(isBlockNumber('2026-06-01T08:06:13')).toBe(false); |
| 176 | + expect(isBlockNumber('0')).toBe(false); |
| 177 | + expect(isBlockNumber('')).toBe(false); |
| 178 | + expect(isBlockNumber('garbage')).toBe(false); |
| 179 | + }); |
91 | 180 | }); |
0 commit comments