Skip to content

Commit 396cf83

Browse files
committed
test: cursor
1 parent 7d9ea5b commit 396cf83

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package cursor
2+
3+
import (
4+
"errors"
5+
"math/big"
6+
"testing"
7+
8+
"github.com/hookdeck/outpost/internal/logstore/driver"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestCursor_IsEmpty(t *testing.T) {
14+
t.Run("empty cursor", func(t *testing.T) {
15+
c := Cursor{}
16+
assert.True(t, c.IsEmpty())
17+
})
18+
19+
t.Run("cursor with position", func(t *testing.T) {
20+
c := Cursor{Position: "abc123"}
21+
assert.False(t, c.IsEmpty())
22+
})
23+
24+
t.Run("cursor with only sort params", func(t *testing.T) {
25+
c := Cursor{SortBy: "event_time", SortOrder: "desc"}
26+
assert.True(t, c.IsEmpty(), "cursor without position is empty")
27+
})
28+
}
29+
30+
func TestEncode(t *testing.T) {
31+
t.Run("encodes cursor to base62", func(t *testing.T) {
32+
c := Cursor{
33+
SortBy: "delivery_time",
34+
SortOrder: "desc",
35+
Position: "1234567890_del_abc",
36+
}
37+
encoded := Encode(c)
38+
assert.NotEmpty(t, encoded)
39+
assert.NotContains(t, encoded, ":", "encoded cursor should not contain raw separators")
40+
})
41+
42+
t.Run("different cursors produce different encodings", func(t *testing.T) {
43+
c1 := Cursor{SortBy: "delivery_time", SortOrder: "desc", Position: "pos1"}
44+
c2 := Cursor{SortBy: "delivery_time", SortOrder: "desc", Position: "pos2"}
45+
assert.NotEqual(t, Encode(c1), Encode(c2))
46+
})
47+
48+
t.Run("same cursor produces same encoding", func(t *testing.T) {
49+
c := Cursor{SortBy: "event_time", SortOrder: "asc", Position: "pos"}
50+
assert.Equal(t, Encode(c), Encode(c))
51+
})
52+
}
53+
54+
func TestDecode(t *testing.T) {
55+
t.Run("empty string returns empty cursor", func(t *testing.T) {
56+
c, err := Decode("")
57+
require.NoError(t, err)
58+
assert.True(t, c.IsEmpty())
59+
})
60+
61+
t.Run("decodes valid cursor", func(t *testing.T) {
62+
original := Cursor{
63+
SortBy: "delivery_time",
64+
SortOrder: "desc",
65+
Position: "1234567890_del_abc",
66+
}
67+
encoded := Encode(original)
68+
69+
decoded, err := Decode(encoded)
70+
require.NoError(t, err)
71+
assert.Equal(t, original.SortBy, decoded.SortBy)
72+
assert.Equal(t, original.SortOrder, decoded.SortOrder)
73+
assert.Equal(t, original.Position, decoded.Position)
74+
})
75+
76+
t.Run("decodes cursor with colons in position", func(t *testing.T) {
77+
original := Cursor{
78+
SortBy: "event_time",
79+
SortOrder: "asc",
80+
Position: "time:with:colons:in:it",
81+
}
82+
encoded := Encode(original)
83+
84+
decoded, err := Decode(encoded)
85+
require.NoError(t, err)
86+
assert.Equal(t, original.Position, decoded.Position)
87+
})
88+
89+
t.Run("invalid base62 returns error", func(t *testing.T) {
90+
_, err := Decode("!!!invalid!!!")
91+
require.Error(t, err)
92+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
93+
})
94+
95+
t.Run("invalid format returns error", func(t *testing.T) {
96+
// Encode something that's not in the right format
97+
_, err := Decode("abc123")
98+
require.Error(t, err)
99+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
100+
})
101+
102+
t.Run("invalid sortBy returns error", func(t *testing.T) {
103+
// Manually create a cursor with invalid sortBy by encoding raw bytes
104+
raw := "v1:invalid_sort:desc:position"
105+
encoded := encodeRaw(raw)
106+
107+
_, err := Decode(encoded)
108+
require.Error(t, err)
109+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
110+
})
111+
112+
t.Run("invalid sortOrder returns error", func(t *testing.T) {
113+
raw := "v1:event_time:invalid_order:position"
114+
encoded := encodeRaw(raw)
115+
116+
_, err := Decode(encoded)
117+
require.Error(t, err)
118+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
119+
})
120+
121+
t.Run("unsupported version returns error", func(t *testing.T) {
122+
raw := "v99:event_time:desc:position"
123+
encoded := encodeRaw(raw)
124+
125+
_, err := Decode(encoded)
126+
require.Error(t, err)
127+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
128+
assert.Contains(t, err.Error(), "unsupported cursor version")
129+
})
130+
}
131+
132+
func TestValidate(t *testing.T) {
133+
t.Run("empty cursor is always valid", func(t *testing.T) {
134+
c := Cursor{}
135+
err := Validate(c, "event_time", "desc")
136+
assert.NoError(t, err)
137+
})
138+
139+
t.Run("matching params is valid", func(t *testing.T) {
140+
c := Cursor{SortBy: "event_time", SortOrder: "desc", Position: "pos"}
141+
err := Validate(c, "event_time", "desc")
142+
assert.NoError(t, err)
143+
})
144+
145+
t.Run("mismatched sortBy returns error", func(t *testing.T) {
146+
c := Cursor{SortBy: "event_time", SortOrder: "desc", Position: "pos"}
147+
err := Validate(c, "delivery_time", "desc")
148+
require.Error(t, err)
149+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
150+
assert.Contains(t, err.Error(), "sortBy")
151+
})
152+
153+
t.Run("mismatched sortOrder returns error", func(t *testing.T) {
154+
c := Cursor{SortBy: "event_time", SortOrder: "desc", Position: "pos"}
155+
err := Validate(c, "event_time", "asc")
156+
require.Error(t, err)
157+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
158+
assert.Contains(t, err.Error(), "sortOrder")
159+
})
160+
}
161+
162+
func TestDecodeAndValidate(t *testing.T) {
163+
t.Run("empty cursors return empty results", func(t *testing.T) {
164+
next, prev, err := DecodeAndValidate("", "", "delivery_time", "desc")
165+
require.NoError(t, err)
166+
assert.True(t, next.IsEmpty())
167+
assert.True(t, prev.IsEmpty())
168+
})
169+
170+
t.Run("valid next cursor", func(t *testing.T) {
171+
original := Cursor{SortBy: "delivery_time", SortOrder: "desc", Position: "pos"}
172+
encoded := Encode(original)
173+
174+
next, prev, err := DecodeAndValidate(encoded, "", "delivery_time", "desc")
175+
require.NoError(t, err)
176+
assert.Equal(t, "pos", next.Position)
177+
assert.True(t, prev.IsEmpty())
178+
})
179+
180+
t.Run("valid prev cursor", func(t *testing.T) {
181+
original := Cursor{SortBy: "event_time", SortOrder: "asc", Position: "pos"}
182+
encoded := Encode(original)
183+
184+
next, prev, err := DecodeAndValidate("", encoded, "event_time", "asc")
185+
require.NoError(t, err)
186+
assert.True(t, next.IsEmpty())
187+
assert.Equal(t, "pos", prev.Position)
188+
})
189+
190+
t.Run("invalid next cursor returns error", func(t *testing.T) {
191+
_, _, err := DecodeAndValidate("!!!invalid!!!", "", "delivery_time", "desc")
192+
require.Error(t, err)
193+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
194+
})
195+
196+
t.Run("invalid prev cursor returns error", func(t *testing.T) {
197+
_, _, err := DecodeAndValidate("", "!!!invalid!!!", "delivery_time", "desc")
198+
require.Error(t, err)
199+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
200+
})
201+
202+
t.Run("mismatched next cursor returns error", func(t *testing.T) {
203+
original := Cursor{SortBy: "delivery_time", SortOrder: "desc", Position: "pos"}
204+
encoded := Encode(original)
205+
206+
_, _, err := DecodeAndValidate(encoded, "", "event_time", "desc")
207+
require.Error(t, err)
208+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
209+
})
210+
211+
t.Run("mismatched prev cursor returns error", func(t *testing.T) {
212+
original := Cursor{SortBy: "delivery_time", SortOrder: "desc", Position: "pos"}
213+
encoded := Encode(original)
214+
215+
_, _, err := DecodeAndValidate("", encoded, "delivery_time", "asc")
216+
require.Error(t, err)
217+
assert.True(t, errors.Is(err, driver.ErrInvalidCursor))
218+
})
219+
}
220+
221+
func TestRoundTrip(t *testing.T) {
222+
testCases := []Cursor{
223+
{SortBy: "delivery_time", SortOrder: "desc", Position: "simple"},
224+
{SortBy: "delivery_time", SortOrder: "asc", Position: "1234567890_del_abc123"},
225+
{SortBy: "event_time", SortOrder: "desc", Position: "1234567890_evt_abc_1234567891_del_xyz"},
226+
{SortBy: "event_time", SortOrder: "asc", Position: "with:colons:and_underscores"},
227+
{SortBy: "delivery_time", SortOrder: "desc", Position: "unicode-émoji-🎉"},
228+
}
229+
230+
for _, tc := range testCases {
231+
t.Run(tc.Position, func(t *testing.T) {
232+
encoded := Encode(tc)
233+
decoded, err := Decode(encoded)
234+
require.NoError(t, err)
235+
236+
assert.Equal(t, tc.SortBy, decoded.SortBy)
237+
assert.Equal(t, tc.SortOrder, decoded.SortOrder)
238+
assert.Equal(t, tc.Position, decoded.Position)
239+
})
240+
}
241+
}
242+
243+
// encodeRaw is a helper to encode raw strings for testing invalid formats
244+
func encodeRaw(raw string) string {
245+
num := new(big.Int)
246+
num.SetBytes([]byte(raw))
247+
return num.Text(62)
248+
}

0 commit comments

Comments
 (0)