detached cac6aa4

test(filtering): add unit tests for version checking and store

commit
cac6aa4adc6bf4ca5f8a110ee385c9d4629de7ef
author
bnema · · 2025-11-29 13:24 UTC
committer
bnema · · 2025-11-29 13:24 UTC
parent
61444b7
signature
GPG Unverified2B82207D370127A0 The key is unknown
2 files · +359 -0
A internal/filtering/store_test.go +100 -0
@@ -0,0 +1,100 @@
1+package filtering
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+ "time"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func TestFileFilterStore_SourceVersions(t *testing.T) {
14+ // Create temp directory for test
15+ tmpDir, err := os.MkdirTemp("", "filter-store-test")
16+ require.NoError(t, err)
17+ defer os.RemoveAll(tmpDir)
18+
19+ // Create store with temp paths
20+ store := &FileFilterStore{
21+ cacheFile: filepath.Join(tmpDir, "cache.json"),
22+ metaFile: filepath.Join(tmpDir, "metadata.json"),
23+ }
24+
25+ testURL := "https://easylist.to/easylist/easylist.txt"
26+ testVersion := "202511291248"
27+
28+ t.Run("get version when no metadata exists", func(t *testing.T) {
29+ version := store.GetSourceVersion(testURL)
30+ assert.Empty(t, version)
31+ })
32+
33+ t.Run("set and get version", func(t *testing.T) {
34+ err := store.SetSourceVersion(testURL, testVersion)
35+ require.NoError(t, err)
36+
37+ version := store.GetSourceVersion(testURL)
38+ assert.Equal(t, testVersion, version)
39+ })
40+
41+ t.Run("update existing version", func(t *testing.T) {
42+ newVersion := "202511301000"
43+ err := store.SetSourceVersion(testURL, newVersion)
44+ require.NoError(t, err)
45+
46+ version := store.GetSourceVersion(testURL)
47+ assert.Equal(t, newVersion, version)
48+ })
49+
50+ t.Run("multiple URLs", func(t *testing.T) {
51+ url1 := "https://example.com/list1.txt"
52+ url2 := "https://example.com/list2.txt"
53+ version1 := "111111111111"
54+ version2 := "222222222222"
55+
56+ err := store.SetSourceVersion(url1, version1)
57+ require.NoError(t, err)
58+ err = store.SetSourceVersion(url2, version2)
59+ require.NoError(t, err)
60+
61+ assert.Equal(t, version1, store.GetSourceVersion(url1))
62+ assert.Equal(t, version2, store.GetSourceVersion(url2))
63+ })
64+
65+ t.Run("last check time is updated", func(t *testing.T) {
66+ beforeSet := time.Now().Add(-time.Second)
67+
68+ err := store.SetSourceVersion(testURL, testVersion)
69+ require.NoError(t, err)
70+
71+ lastCheck := store.GetLastCheckTime()
72+ assert.True(t, lastCheck.After(beforeSet))
73+ })
74+}
75+
76+func TestCacheMetadata_SourceVersions(t *testing.T) {
77+ t.Run("nil map handling", func(t *testing.T) {
78+ metadata := &CacheMetadata{
79+ Version: cacheVersion,
80+ SourceVersions: nil,
81+ }
82+
83+ // Should handle nil map gracefully
84+ assert.Nil(t, metadata.SourceVersions)
85+ })
86+
87+ t.Run("initialized map", func(t *testing.T) {
88+ metadata := &CacheMetadata{
89+ Version: cacheVersion,
90+ SourceVersions: map[string]string{
91+ "url1": "v1",
92+ "url2": "v2",
93+ },
94+ }
95+
96+ assert.Equal(t, "v1", metadata.SourceVersions["url1"])
97+ assert.Equal(t, "v2", metadata.SourceVersions["url2"])
98+ assert.Empty(t, metadata.SourceVersions["nonexistent"])
99+ })
100+}
@@ -0,0 +1,100 @@
1+package filtering
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+ "time"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func TestFileFilterStore_SourceVersions(t *testing.T) {
14+ // Create temp directory for test
15+ tmpDir, err := os.MkdirTemp("", "filter-store-test")
16+ require.NoError(t, err)
17+ defer os.RemoveAll(tmpDir)
18+
19+ // Create store with temp paths
20+ store := &FileFilterStore{
21+ cacheFile: filepath.Join(tmpDir, "cache.json"),
22+ metaFile: filepath.Join(tmpDir, "metadata.json"),
23+ }
24+
25+ testURL := "https://easylist.to/easylist/easylist.txt"
26+ testVersion := "202511291248"
27+
28+ t.Run("get version when no metadata exists", func(t *testing.T) {
29+ version := store.GetSourceVersion(testURL)
30+ assert.Empty(t, version)
31+ })
32+
33+ t.Run("set and get version", func(t *testing.T) {
34+ err := store.SetSourceVersion(testURL, testVersion)
35+ require.NoError(t, err)
36+
37+ version := store.GetSourceVersion(testURL)
38+ assert.Equal(t, testVersion, version)
39+ })
40+
41+ t.Run("update existing version", func(t *testing.T) {
42+ newVersion := "202511301000"
43+ err := store.SetSourceVersion(testURL, newVersion)
44+ require.NoError(t, err)
45+
46+ version := store.GetSourceVersion(testURL)
47+ assert.Equal(t, newVersion, version)
48+ })
49+
50+ t.Run("multiple URLs", func(t *testing.T) {
51+ url1 := "https://example.com/list1.txt"
52+ url2 := "https://example.com/list2.txt"
53+ version1 := "111111111111"
54+ version2 := "222222222222"
55+
56+ err := store.SetSourceVersion(url1, version1)
57+ require.NoError(t, err)
58+ err = store.SetSourceVersion(url2, version2)
59+ require.NoError(t, err)
60+
61+ assert.Equal(t, version1, store.GetSourceVersion(url1))
62+ assert.Equal(t, version2, store.GetSourceVersion(url2))
63+ })
64+
65+ t.Run("last check time is updated", func(t *testing.T) {
66+ beforeSet := time.Now().Add(-time.Second)
67+
68+ err := store.SetSourceVersion(testURL, testVersion)
69+ require.NoError(t, err)
70+
71+ lastCheck := store.GetLastCheckTime()
72+ assert.True(t, lastCheck.After(beforeSet))
73+ })
74+}
75+
76+func TestCacheMetadata_SourceVersions(t *testing.T) {
77+ t.Run("nil map handling", func(t *testing.T) {
78+ metadata := &CacheMetadata{
79+ Version: cacheVersion,
80+ SourceVersions: nil,
81+ }
82+
83+ // Should handle nil map gracefully
84+ assert.Nil(t, metadata.SourceVersions)
85+ })
86+
87+ t.Run("initialized map", func(t *testing.T) {
88+ metadata := &CacheMetadata{
89+ Version: cacheVersion,
90+ SourceVersions: map[string]string{
91+ "url1": "v1",
92+ "url2": "v2",
93+ },
94+ }
95+
96+ assert.Equal(t, "v1", metadata.SourceVersions["url1"])
97+ assert.Equal(t, "v2", metadata.SourceVersions["url2"])
98+ assert.Empty(t, metadata.SourceVersions["nonexistent"])
99+ })
100+}
A internal/filtering/updater_test.go +259 -0
@@ -0,0 +1,259 @@
1+package filtering
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestFetchRemoteVersion(t *testing.T) {
15+ tests := []struct {
16+ name string
17+ responseBody string
18+ responseStatus int
19+ wantVersion string
20+ wantErr bool
21+ }{
22+ {
23+ name: "valid version header",
24+ responseBody: `[Adblock Plus 2.0]
25+! Version: 202511291248
26+! Title: EasyList
27+! Last modified: 29 Nov 2025 12:48 UTC
28+`,
29+ responseStatus: http.StatusOK,
30+ wantVersion: "202511291248",
31+ wantErr: false,
32+ },
33+ {
34+ name: "partial content response (206)",
35+ responseBody: `[Adblock Plus 2.0]
36+! Version: 202501011200
37+! Title: Test
38+`,
39+ responseStatus: http.StatusPartialContent,
40+ wantVersion: "202501011200",
41+ wantErr: false,
42+ },
43+ {
44+ name: "no version header",
45+ responseBody: `[Adblock Plus 2.0]\n! Title: No Version\n`,
46+ responseStatus: http.StatusOK,
47+ wantVersion: "",
48+ wantErr: true,
49+ },
50+ {
51+ name: "server error",
52+ responseBody: "",
53+ responseStatus: http.StatusInternalServerError,
54+ wantVersion: "",
55+ wantErr: true,
56+ },
57+ }
58+
59+ for _, tt := range tests {
60+ t.Run(tt.name, func(t *testing.T) {
61+ // Create test server
62+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+ // Verify Range header is set
64+ rangeHeader := r.Header.Get("Range")
65+ assert.Contains(t, rangeHeader, "bytes=0-")
66+
67+ w.WriteHeader(tt.responseStatus)
68+ _, _ = w.Write([]byte(tt.responseBody))
69+ }))
70+ defer server.Close()
71+
72+ // Create updater with test server
73+ fu := &FilterUpdater{
74+ httpClient: &http.Client{Timeout: 5 * time.Second},
75+ }
76+
77+ // Test fetchRemoteVersion
78+ version, err := fu.fetchRemoteVersion(context.Background(), server.URL)
79+
80+ if tt.wantErr {
81+ assert.Error(t, err)
82+ } else {
83+ require.NoError(t, err)
84+ assert.Equal(t, tt.wantVersion, version)
85+ }
86+ })
87+ }
88+}
89+
90+func TestExtractVersion(t *testing.T) {
91+ tests := []struct {
92+ name string
93+ content []byte
94+ want string
95+ }{
96+ {
97+ name: "standard format",
98+ content: []byte(`[Adblock Plus 2.0]
99+! Version: 202511291248
100+! Title: EasyList`),
101+ want: "202511291248",
102+ },
103+ {
104+ name: "version with spaces",
105+ content: []byte(`! Version: 20251129
106+! Title: Test`),
107+ want: "20251129",
108+ },
109+ {
110+ name: "no version",
111+ content: []byte(`! Title: No Version\n! Author: Test`),
112+ want: "",
113+ },
114+ {
115+ name: "empty content",
116+ content: []byte{},
117+ want: "",
118+ },
119+ {
120+ name: "version after 20 lines (not found)",
121+ content: []byte(`line1
122+line2
123+line3
124+line4
125+line5
126+line6
127+line7
128+line8
129+line9
130+line10
131+line11
132+line12
133+line13
134+line14
135+line15
136+line16
137+line17
138+line18
139+line19
140+line20
141+line21
142+! Version: 202511291248`),
143+ want: "",
144+ },
145+ }
146+
147+ fu := &FilterUpdater{}
148+
149+ for _, tt := range tests {
150+ t.Run(tt.name, func(t *testing.T) {
151+ got := fu.extractVersion(tt.content)
152+ assert.Equal(t, tt.want, got)
153+ })
154+ }
155+}
156+
157+func TestNeedsUpdate(t *testing.T) {
158+ tests := []struct {
159+ name string
160+ storedVersion string
161+ remoteVersion string
162+ serverError bool
163+ expectedResult bool
164+ }{
165+ {
166+ name: "versions differ - needs update",
167+ storedVersion: "202511280000",
168+ remoteVersion: "202511291248",
169+ expectedResult: true,
170+ },
171+ {
172+ name: "versions same - no update",
173+ storedVersion: "202511291248",
174+ remoteVersion: "202511291248",
175+ expectedResult: false,
176+ },
177+ {
178+ name: "no stored version - needs update",
179+ storedVersion: "",
180+ remoteVersion: "202511291248",
181+ expectedResult: true,
182+ },
183+ {
184+ name: "server error - assume update needed",
185+ storedVersion: "202511280000",
186+ serverError: true,
187+ expectedResult: true,
188+ },
189+ }
190+
191+ for _, tt := range tests {
192+ t.Run(tt.name, func(t *testing.T) {
193+ // Create test server
194+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
195+ if tt.serverError {
196+ w.WriteHeader(http.StatusInternalServerError)
197+ return
198+ }
199+ w.WriteHeader(http.StatusOK)
200+ _, _ = w.Write([]byte("! Version: " + tt.remoteVersion + "\n"))
201+ }))
202+ defer server.Close()
203+
204+ // Create mock store
205+ mockStore := &mockFilterStore{
206+ sourceVersions: map[string]string{
207+ server.URL: tt.storedVersion,
208+ },
209+ }
210+
211+ // Create manager and updater
212+ fm := &FilterManager{store: mockStore}
213+ fu := &FilterUpdater{
214+ manager: fm,
215+ httpClient: &http.Client{Timeout: 5 * time.Second},
216+ }
217+
218+ // Test needsUpdate
219+ result := fu.needsUpdate(context.Background(), server.URL)
220+ assert.Equal(t, tt.expectedResult, result)
221+ })
222+ }
223+}
224+
225+// mockFilterStore implements FilterStore interface for testing
226+type mockFilterStore struct {
227+ sourceVersions map[string]string
228+ cachedFilters *CompiledFilters
229+ cacheExists bool
230+ cacheModTime time.Time
231+}
232+
233+func (m *mockFilterStore) LoadCached() (*CompiledFilters, error) {
234+ return m.cachedFilters, nil
235+}
236+
237+func (m *mockFilterStore) SaveCache(filters *CompiledFilters) error {
238+ m.cachedFilters = filters
239+ return nil
240+}
241+
242+func (m *mockFilterStore) GetCacheInfo() (bool, time.Time, error) {
243+ return m.cacheExists, m.cacheModTime, nil
244+}
245+
246+func (m *mockFilterStore) GetSourceVersion(url string) string {
247+ if m.sourceVersions == nil {
248+ return ""
249+ }
250+ return m.sourceVersions[url]
251+}
252+
253+func (m *mockFilterStore) SetSourceVersion(url string, version string) error {
254+ if m.sourceVersions == nil {
255+ m.sourceVersions = make(map[string]string)
256+ }
257+ m.sourceVersions[url] = version
258+ return nil
259+}
@@ -0,0 +1,259 @@
1+package filtering
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestFetchRemoteVersion(t *testing.T) {
15+ tests := []struct {
16+ name string
17+ responseBody string
18+ responseStatus int
19+ wantVersion string
20+ wantErr bool
21+ }{
22+ {
23+ name: "valid version header",
24+ responseBody: `[Adblock Plus 2.0]
25+! Version: 202511291248
26+! Title: EasyList
27+! Last modified: 29 Nov 2025 12:48 UTC
28+`,
29+ responseStatus: http.StatusOK,
30+ wantVersion: "202511291248",
31+ wantErr: false,
32+ },
33+ {
34+ name: "partial content response (206)",
35+ responseBody: `[Adblock Plus 2.0]
36+! Version: 202501011200
37+! Title: Test
38+`,
39+ responseStatus: http.StatusPartialContent,
40+ wantVersion: "202501011200",
41+ wantErr: false,
42+ },
43+ {
44+ name: "no version header",
45+ responseBody: `[Adblock Plus 2.0]\n! Title: No Version\n`,
46+ responseStatus: http.StatusOK,
47+ wantVersion: "",
48+ wantErr: true,
49+ },
50+ {
51+ name: "server error",
52+ responseBody: "",
53+ responseStatus: http.StatusInternalServerError,
54+ wantVersion: "",
55+ wantErr: true,
56+ },
57+ }
58+
59+ for _, tt := range tests {
60+ t.Run(tt.name, func(t *testing.T) {
61+ // Create test server
62+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+ // Verify Range header is set
64+ rangeHeader := r.Header.Get("Range")
65+ assert.Contains(t, rangeHeader, "bytes=0-")
66+
67+ w.WriteHeader(tt.responseStatus)
68+ _, _ = w.Write([]byte(tt.responseBody))
69+ }))
70+ defer server.Close()
71+
72+ // Create updater with test server
73+ fu := &FilterUpdater{
74+ httpClient: &http.Client{Timeout: 5 * time.Second},
75+ }
76+
77+ // Test fetchRemoteVersion
78+ version, err := fu.fetchRemoteVersion(context.Background(), server.URL)
79+
80+ if tt.wantErr {
81+ assert.Error(t, err)
82+ } else {
83+ require.NoError(t, err)
84+ assert.Equal(t, tt.wantVersion, version)
85+ }
86+ })
87+ }
88+}
89+
90+func TestExtractVersion(t *testing.T) {
91+ tests := []struct {
92+ name string
93+ content []byte
94+ want string
95+ }{
96+ {
97+ name: "standard format",
98+ content: []byte(`[Adblock Plus 2.0]
99+! Version: 202511291248
100+! Title: EasyList`),
101+ want: "202511291248",
102+ },
103+ {
104+ name: "version with spaces",
105+ content: []byte(`! Version: 20251129
106+! Title: Test`),
107+ want: "20251129",
108+ },
109+ {
110+ name: "no version",
111+ content: []byte(`! Title: No Version\n! Author: Test`),
112+ want: "",
113+ },
114+ {
115+ name: "empty content",
116+ content: []byte{},
117+ want: "",
118+ },
119+ {
120+ name: "version after 20 lines (not found)",
121+ content: []byte(`line1
122+line2
123+line3
124+line4
125+line5
126+line6
127+line7
128+line8
129+line9
130+line10
131+line11
132+line12
133+line13
134+line14
135+line15
136+line16
137+line17
138+line18
139+line19
140+line20
141+line21
142+! Version: 202511291248`),
143+ want: "",
144+ },
145+ }
146+
147+ fu := &FilterUpdater{}
148+
149+ for _, tt := range tests {
150+ t.Run(tt.name, func(t *testing.T) {
151+ got := fu.extractVersion(tt.content)
152+ assert.Equal(t, tt.want, got)
153+ })
154+ }
155+}
156+
157+func TestNeedsUpdate(t *testing.T) {
158+ tests := []struct {
159+ name string
160+ storedVersion string
161+ remoteVersion string
162+ serverError bool
163+ expectedResult bool
164+ }{
165+ {
166+ name: "versions differ - needs update",
167+ storedVersion: "202511280000",
168+ remoteVersion: "202511291248",
169+ expectedResult: true,
170+ },
171+ {
172+ name: "versions same - no update",
173+ storedVersion: "202511291248",
174+ remoteVersion: "202511291248",
175+ expectedResult: false,
176+ },
177+ {
178+ name: "no stored version - needs update",
179+ storedVersion: "",
180+ remoteVersion: "202511291248",
181+ expectedResult: true,
182+ },
183+ {
184+ name: "server error - assume update needed",
185+ storedVersion: "202511280000",
186+ serverError: true,
187+ expectedResult: true,
188+ },
189+ }
190+
191+ for _, tt := range tests {
192+ t.Run(tt.name, func(t *testing.T) {
193+ // Create test server
194+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
195+ if tt.serverError {
196+ w.WriteHeader(http.StatusInternalServerError)
197+ return
198+ }
199+ w.WriteHeader(http.StatusOK)
200+ _, _ = w.Write([]byte("! Version: " + tt.remoteVersion + "\n"))
201+ }))
202+ defer server.Close()
203+
204+ // Create mock store
205+ mockStore := &mockFilterStore{
206+ sourceVersions: map[string]string{
207+ server.URL: tt.storedVersion,
208+ },
209+ }
210+
211+ // Create manager and updater
212+ fm := &FilterManager{store: mockStore}
213+ fu := &FilterUpdater{
214+ manager: fm,
215+ httpClient: &http.Client{Timeout: 5 * time.Second},
216+ }
217+
218+ // Test needsUpdate
219+ result := fu.needsUpdate(context.Background(), server.URL)
220+ assert.Equal(t, tt.expectedResult, result)
221+ })
222+ }
223+}
224+
225+// mockFilterStore implements FilterStore interface for testing
226+type mockFilterStore struct {
227+ sourceVersions map[string]string
228+ cachedFilters *CompiledFilters
229+ cacheExists bool
230+ cacheModTime time.Time
231+}
232+
233+func (m *mockFilterStore) LoadCached() (*CompiledFilters, error) {
234+ return m.cachedFilters, nil
235+}
236+
237+func (m *mockFilterStore) SaveCache(filters *CompiledFilters) error {
238+ m.cachedFilters = filters
239+ return nil
240+}
241+
242+func (m *mockFilterStore) GetCacheInfo() (bool, time.Time, error) {
243+ return m.cacheExists, m.cacheModTime, nil
244+}
245+
246+func (m *mockFilterStore) GetSourceVersion(url string) string {
247+ if m.sourceVersions == nil {
248+ return ""
249+ }
250+ return m.sourceVersions[url]
251+}
252+
253+func (m *mockFilterStore) SetSourceVersion(url string, version string) error {
254+ if m.sourceVersions == nil {
255+ m.sourceVersions = make(map[string]string)
256+ }
257+ m.sourceVersions[url] = version
258+ return nil
259+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close