1package filtering
2
3import (
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
14func TestFetchRemoteVersion(t *testing.T) {
15 tests := []struct {
16 name string
17 responseStatus int
18 etag string
19 lastModified string
20 wantVersion string
21 wantErr bool
22 }{
23 {
24 name: "returns ETag when present",
25 responseStatus: http.StatusOK,
26 etag: `"abc123"`,
27 wantVersion: `"abc123"`,
28 wantErr: false,
29 },
30 {
31 name: "returns Last-Modified when no ETag",
32 responseStatus: http.StatusOK,
33 lastModified: "Sat, 29 Nov 2025 12:48:00 GMT",
34 wantVersion: "Sat, 29 Nov 2025 12:48:00 GMT",
35 wantErr: false,
36 },
37 {
38 name: "prefers ETag over Last-Modified",
39 responseStatus: http.StatusOK,
40 etag: `"xyz789"`,
41 lastModified: "Sat, 29 Nov 2025 12:48:00 GMT",
42 wantVersion: `"xyz789"`,
43 wantErr: false,
44 },
45 {
46 name: "error when no version headers",
47 responseStatus: http.StatusOK,
48 wantVersion: "",
49 wantErr: true,
50 },
51 {
52 name: "server error",
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 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62 // Verify HEAD request
63 assert.Equal(t, http.MethodHead, r.Method)
64
65 if tt.etag != "" {
66 w.Header().Set("ETag", tt.etag)
67 }
68 if tt.lastModified != "" {
69 w.Header().Set("Last-Modified", tt.lastModified)
70 }
71 w.WriteHeader(tt.responseStatus)
72 }))
73 defer server.Close()
74
75 fu := &FilterUpdater{
76 httpClient: &http.Client{Timeout: 5 * time.Second},
77 }
78
79 version, err := fu.fetchRemoteVersion(context.Background(), server.URL)
80
81 if tt.wantErr {
82 assert.Error(t, err)
83 } else {
84 require.NoError(t, err)
85 assert.Equal(t, tt.wantVersion, version)
86 }
87 })
88 }
89}
90
91func TestNeedsUpdate(t *testing.T) {
92 tests := []struct {
93 name string
94 storedVersion string
95 remoteETag string
96 serverError bool
97 expectedResult bool
98 }{
99 {
100 name: "etags differ - needs update",
101 storedVersion: `"old-etag"`,
102 remoteETag: `"new-etag"`,
103 expectedResult: true,
104 },
105 {
106 name: "etags same - no update",
107 storedVersion: `"same-etag"`,
108 remoteETag: `"same-etag"`,
109 expectedResult: false,
110 },
111 {
112 name: "no stored version - needs update",
113 storedVersion: "",
114 remoteETag: `"any-etag"`,
115 expectedResult: true,
116 },
117 {
118 name: "server error - assume update needed",
119 storedVersion: `"old-etag"`,
120 serverError: true,
121 expectedResult: true,
122 },
123 }
124
125 for _, tt := range tests {
126 t.Run(tt.name, func(t *testing.T) {
127 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128 if tt.serverError {
129 w.WriteHeader(http.StatusInternalServerError)
130 return
131 }
132 w.Header().Set("ETag", tt.remoteETag)
133 w.WriteHeader(http.StatusOK)
134 }))
135 defer server.Close()
136
137 mockStore := &mockFilterStore{
138 sourceVersions: map[string]string{
139 server.URL: tt.storedVersion,
140 },
141 }
142
143 fm := &FilterManager{store: mockStore}
144 fu := &FilterUpdater{
145 manager: fm,
146 httpClient: &http.Client{Timeout: 5 * time.Second},
147 }
148
149 result := fu.needsUpdate(context.Background(), server.URL)
150 assert.Equal(t, tt.expectedResult, result)
151 })
152 }
153}
154
155// mockFilterStore implements FilterStore interface for testing
156type mockFilterStore struct {
157 sourceVersions map[string]string
158 cachedFilters *CompiledFilters
159 cacheExists bool
160 cacheModTime time.Time
161 lastCheckTime time.Time
162}
163
164func (m *mockFilterStore) LoadCached() (*CompiledFilters, error) {
165 return m.cachedFilters, nil
166}
167
168func (m *mockFilterStore) SaveCache(filters *CompiledFilters) error {
169 m.cachedFilters = filters
170 return nil
171}
172
173func (m *mockFilterStore) GetCacheInfo() (bool, time.Time, error) {
174 return m.cacheExists, m.cacheModTime, nil
175}
176
177func (m *mockFilterStore) GetSourceVersion(url string) string {
178 if m.sourceVersions == nil {
179 return ""
180 }
181 return m.sourceVersions[url]
182}
183
184func (m *mockFilterStore) SetSourceVersion(url string, version string) error {
185 if m.sourceVersions == nil {
186 m.sourceVersions = make(map[string]string)
187 }
188 m.sourceVersions[url] = version
189 return nil
190}
191
192func (m *mockFilterStore) GetLastCheckTime() time.Time {
193 return m.lastCheckTime
194}