1 package webserver 2 3 import ( 4 "bytes" 5 "errors" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 11 "github.com/cybertec-postgresql/pgwatch/v3/internal/metrics" 12 "github.com/stretchr/testify/assert" 13 ) 14 15 type mockMetricsReaderWriter struct { 16 GetMetricsFunc func() (*metrics.Metrics, error) 17 UpdateMetricFunc func(name string, m metrics.Metric) error 18 DeleteMetricFunc func(name string) error 19 DeletePresetFunc func(name string) error 20 UpdatePresetFunc func(name string, preset metrics.Preset) error 21 WriteMetricsFunc func(metricDefs *metrics.Metrics) error 22 } 23 24 func (m *mockMetricsReaderWriter) GetMetrics() (*metrics.Metrics, error) { 25 return m.GetMetricsFunc() 26 } 27 func (m *mockMetricsReaderWriter) UpdateMetric(name string, metric metrics.Metric) error { 28 return m.UpdateMetricFunc(name, metric) 29 } 30 func (m *mockMetricsReaderWriter) DeleteMetric(name string) error { 31 return m.DeleteMetricFunc(name) 32 } 33 func (m *mockMetricsReaderWriter) DeletePreset(name string) error { 34 return m.DeletePresetFunc(name) 35 } 36 func (m *mockMetricsReaderWriter) UpdatePreset(name string, preset metrics.Preset) error { 37 return m.UpdatePresetFunc(name, preset) 38 } 39 func (m *mockMetricsReaderWriter) WriteMetrics(metricDefs *metrics.Metrics) error { 40 return m.WriteMetricsFunc(metricDefs) 41 } 42 43 func newTestMetricServer(mrw *mockMetricsReaderWriter) *WebUIServer { 44 return &WebUIServer{ 45 metricsReaderWriter: mrw, 46 } 47 } 48 49 func TestHandleMetrics_GET(t *testing.T) { 50 mock := &mockMetricsReaderWriter{ 51 GetMetricsFunc: func() (*metrics.Metrics, error) { 52 return &metrics.Metrics{MetricDefs: map[string]metrics.Metric{"foo": {Description: "foo"}}}, nil 53 }, 54 } 55 ts := newTestMetricServer(mock) 56 r := httptest.NewRequest(http.MethodGet, "/metric", nil) 57 w := httptest.NewRecorder() 58 ts.handleMetrics(w, r) 59 resp := w.Result() 60 defer resp.Body.Close() 61 assert.Equal(t, http.StatusOK, resp.StatusCode) 62 body, _ := io.ReadAll(resp.Body) 63 var got map[string]metrics.Metric 64 assert.NoError(t, json.Unmarshal(body, &got)) 65 assert.Contains(t, got, "foo") 66 } 67 68 func TestHandleMetrics_GET_Fail(t *testing.T) { 69 mock := &mockMetricsReaderWriter{ 70 GetMetricsFunc: func() (*metrics.Metrics, error) { 71 return nil, errors.New("fail") 72 }, 73 } 74 ts := newTestMetricServer(mock) 75 r := httptest.NewRequest(http.MethodGet, "/metric", nil) 76 w := httptest.NewRecorder() 77 ts.handleMetrics(w, r) 78 resp := w.Result() 79 defer resp.Body.Close() 80 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 81 body, _ := io.ReadAll(resp.Body) 82 assert.Contains(t, string(body), "fail") 83 } 84 85 func TestHandleMetrics_POST(t *testing.T) { 86 var updatedName string 87 var updatedMetric metrics.Metric 88 mock := &mockMetricsReaderWriter{ 89 UpdateMetricFunc: func(name string, m metrics.Metric) error { 90 updatedName = name 91 updatedMetric = m 92 return nil 93 }, 94 } 95 ts := newTestMetricServer(mock) 96 m := metrics.Metric{Description: "bar"} 97 b, _ := json.Marshal(m) 98 r := httptest.NewRequest(http.MethodPost, "/metric?name=bar", bytes.NewReader(b)) 99 w := httptest.NewRecorder() 100 ts.handleMetrics(w, r) 101 resp := w.Result() 102 defer resp.Body.Close() 103 assert.Equal(t, http.StatusOK, resp.StatusCode) 104 assert.Equal(t, "bar", updatedName) 105 assert.Equal(t, m, updatedMetric) 106 } 107 108 type errorReader struct{} 109 110 func (e *errorReader) Read([]byte) (n int, err error) { 111 return 0, errors.New("mock read error") 112 } 113 114 func TestHandleMetrics_POST_ReaderFail(t *testing.T) { 115 mock := &mockMetricsReaderWriter{ 116 UpdateMetricFunc: func(_ string, _ metrics.Metric) error { 117 return nil 118 }, 119 } 120 ts := newTestMetricServer(mock) 121 r := httptest.NewRequest(http.MethodPost, "/metric?name=bar", &errorReader{}) 122 w := httptest.NewRecorder() 123 ts.handleMetrics(w, r) 124 resp := w.Result() 125 defer resp.Body.Close() 126 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 127 body, _ := io.ReadAll(resp.Body) 128 assert.Contains(t, string(body), "mock read error") 129 } 130 131 func TestHandleMetrics_POST_Fail(t *testing.T) { 132 mock := &mockMetricsReaderWriter{ 133 UpdateMetricFunc: func(_ string, _ metrics.Metric) error { 134 return errors.New("fail") 135 }, 136 } 137 ts := newTestMetricServer(mock) 138 m := metrics.Metric{Description: "bar"} 139 b, _ := json.Marshal(m) 140 r := httptest.NewRequest(http.MethodPost, "/metric?name=bar", bytes.NewReader(b)) 141 w := httptest.NewRecorder() 142 ts.handleMetrics(w, r) 143 resp := w.Result() 144 defer resp.Body.Close() 145 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 146 body, _ := io.ReadAll(resp.Body) 147 assert.Contains(t, string(body), "fail") 148 } 149 150 func TestHandleMetrics_DELETE(t *testing.T) { 151 var deletedName string 152 mock := &mockMetricsReaderWriter{ 153 DeleteMetricFunc: func(name string) error { 154 deletedName = name 155 return nil 156 }, 157 } 158 ts := newTestMetricServer(mock) 159 r := httptest.NewRequest(http.MethodDelete, "/metric?name=foo", nil) 160 w := httptest.NewRecorder() 161 ts.handleMetrics(w, r) 162 resp := w.Result() 163 defer resp.Body.Close() 164 assert.Equal(t, http.StatusOK, resp.StatusCode) 165 assert.Equal(t, "foo", deletedName) 166 } 167 168 func TestHandleMetrics_Options(t *testing.T) { 169 mock := &mockMetricsReaderWriter{} 170 ts := newTestMetricServer(mock) 171 r := httptest.NewRequest(http.MethodOptions, "/metric", nil) 172 w := httptest.NewRecorder() 173 ts.handleMetrics(w, r) 174 resp := w.Result() 175 defer resp.Body.Close() 176 assert.Equal(t, http.StatusNoContent, resp.StatusCode) 177 assert.Equal(t, "GET, POST, DELETE, OPTIONS", resp.Header.Get("Allow")) 178 } 179 180 func TestHandleMetrics_MethodNotAllowed(t *testing.T) { 181 mock := &mockMetricsReaderWriter{} 182 ts := newTestMetricServer(mock) 183 r := httptest.NewRequest(http.MethodPut, "/metric", nil) 184 w := httptest.NewRecorder() 185 ts.handleMetrics(w, r) 186 resp := w.Result() 187 defer resp.Body.Close() 188 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) 189 assert.Equal(t, "GET, POST, DELETE, OPTIONS", resp.Header.Get("Allow")) 190 } 191 192 func TestGetMetrics_Error(t *testing.T) { 193 mock := &mockMetricsReaderWriter{ 194 GetMetricsFunc: func() (*metrics.Metrics, error) { 195 return nil, errors.New("fail") 196 }, 197 } 198 ts := newTestMetricServer(mock) 199 _, err := ts.GetMetrics() 200 assert.Error(t, err) 201 } 202 203 func TestUpdateMetric_Error(t *testing.T) { 204 mock := &mockMetricsReaderWriter{ 205 UpdateMetricFunc: func(_ string, _ metrics.Metric) error { 206 return errors.New("fail") 207 }, 208 } 209 ts := newTestMetricServer(mock) 210 err := ts.UpdateMetric("foo", []byte("notjson")) 211 assert.Error(t, err) 212 } 213 214 func TestDeleteMetric_Error(t *testing.T) { 215 mock := &mockMetricsReaderWriter{ 216 DeleteMetricFunc: func(_ string) error { 217 return errors.New("fail") 218 }, 219 } 220 ts := newTestMetricServer(mock) 221 err := ts.DeleteMetric("foo") 222 assert.Error(t, err) 223 } 224 225 func TestHandlePreset_GET(t *testing.T) { 226 mock := &mockMetricsReaderWriter{ 227 GetMetricsFunc: func() (*metrics.Metrics, error) { 228 return &metrics.Metrics{PresetDefs: map[string]metrics.Preset{"foo": {Description: "foo"}}}, nil 229 }, 230 } 231 ts := newTestMetricServer(mock) 232 r := httptest.NewRequest(http.MethodGet, "/preset", nil) 233 w := httptest.NewRecorder() 234 ts.handlePresets(w, r) 235 resp := w.Result() 236 defer resp.Body.Close() 237 assert.Equal(t, http.StatusOK, resp.StatusCode) 238 body, _ := io.ReadAll(resp.Body) 239 var got map[string]metrics.Preset 240 assert.NoError(t, json.Unmarshal(body, &got)) 241 assert.Contains(t, got, "foo") 242 } 243 244 func TestHandlePreset_GET_Fail(t *testing.T) { 245 mock := &mockMetricsReaderWriter{ 246 GetMetricsFunc: func() (*metrics.Metrics, error) { 247 return nil, errors.New("fail") 248 }, 249 } 250 ts := newTestMetricServer(mock) 251 r := httptest.NewRequest(http.MethodGet, "/preset", nil) 252 w := httptest.NewRecorder() 253 ts.handlePresets(w, r) 254 resp := w.Result() 255 defer resp.Body.Close() 256 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 257 body, _ := io.ReadAll(resp.Body) 258 assert.Contains(t, string(body), "fail") 259 } 260 261 func TestHandlePreset_POST(t *testing.T) { 262 var updatedName string 263 var updatedPreset metrics.Preset 264 mock := &mockMetricsReaderWriter{ 265 UpdatePresetFunc: func(name string, p metrics.Preset) error { 266 updatedName = name 267 updatedPreset = p 268 return nil 269 }, 270 } 271 ts := newTestMetricServer(mock) 272 p := metrics.Preset{Description: "bar"} 273 b, _ := json.Marshal(p) 274 r := httptest.NewRequest(http.MethodPost, "/preset?name=bar", bytes.NewReader(b)) 275 w := httptest.NewRecorder() 276 ts.handlePresets(w, r) 277 resp := w.Result() 278 defer resp.Body.Close() 279 assert.Equal(t, http.StatusOK, resp.StatusCode) 280 assert.Equal(t, "bar", updatedName) 281 assert.Equal(t, p, updatedPreset) 282 } 283 284 func TestHandlePreset_POST_ReaderFail(t *testing.T) { 285 mock := &mockMetricsReaderWriter{ 286 UpdatePresetFunc: func(string, metrics.Preset) error { 287 return nil 288 }, 289 } 290 ts := newTestMetricServer(mock) 291 r := httptest.NewRequest(http.MethodPost, "/preset?name=bar", &errorReader{}) 292 w := httptest.NewRecorder() 293 ts.handlePresets(w, r) 294 resp := w.Result() 295 defer resp.Body.Close() 296 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 297 body, _ := io.ReadAll(resp.Body) 298 assert.Contains(t, string(body), "mock read error") 299 } 300 301 func TestHandlePreset_POST_Fail(t *testing.T) { 302 mock := &mockMetricsReaderWriter{ 303 UpdatePresetFunc: func(string, metrics.Preset) error { 304 return errors.New("fail") 305 }, 306 } 307 ts := newTestMetricServer(mock) 308 p := metrics.Preset{Description: "bar"} 309 b, _ := json.Marshal(p) 310 r := httptest.NewRequest(http.MethodPost, "/preset?name=bar", bytes.NewReader(b)) 311 w := httptest.NewRecorder() 312 ts.handlePresets(w, r) 313 resp := w.Result() 314 defer resp.Body.Close() 315 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 316 body, _ := io.ReadAll(resp.Body) 317 assert.Contains(t, string(body), "fail") 318 } 319 320 func TestHandlePreset_DELETE(t *testing.T) { 321 var deletedName string 322 mock := &mockMetricsReaderWriter{ 323 DeletePresetFunc: func(name string) error { 324 deletedName = name 325 return nil 326 }, 327 } 328 ts := newTestMetricServer(mock) 329 r := httptest.NewRequest(http.MethodDelete, "/preset?name=foo", nil) 330 w := httptest.NewRecorder() 331 ts.handlePresets(w, r) 332 resp := w.Result() 333 defer resp.Body.Close() 334 assert.Equal(t, http.StatusOK, resp.StatusCode) 335 assert.Equal(t, "foo", deletedName) 336 } 337 338 func TestHandlePreset_Options(t *testing.T) { 339 mock := &mockMetricsReaderWriter{} 340 ts := newTestMetricServer(mock) 341 r := httptest.NewRequest(http.MethodOptions, "/preset", nil) 342 w := httptest.NewRecorder() 343 ts.handlePresets(w, r) 344 resp := w.Result() 345 defer resp.Body.Close() 346 assert.Equal(t, http.StatusNoContent, resp.StatusCode) 347 assert.Equal(t, "GET, POST, PATCH, DELETE, OPTIONS", resp.Header.Get("Allow")) 348 } 349 350 func TestHandlePreset_MethodNotAllowed(t *testing.T) { 351 mock := &mockMetricsReaderWriter{} 352 ts := newTestMetricServer(mock) 353 r := httptest.NewRequest(http.MethodPut, "/preset", nil) 354 w := httptest.NewRecorder() 355 ts.handlePresets(w, r) 356 resp := w.Result() 357 defer resp.Body.Close() 358 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) 359 assert.Equal(t, "GET, POST, PATCH, DELETE, OPTIONS", resp.Header.Get("Allow")) 360 } 361 362 func TestGetPresets_Error(t *testing.T) { 363 mock := &mockMetricsReaderWriter{ 364 GetMetricsFunc: func() (*metrics.Metrics, error) { 365 return nil, errors.New("fail") 366 }, 367 } 368 ts := newTestMetricServer(mock) 369 _, err := ts.GetPresets() 370 assert.Error(t, err) 371 } 372 373 func TestUpdatePreset_Error(t *testing.T) { 374 mock := &mockMetricsReaderWriter{ 375 UpdatePresetFunc: func(string, metrics.Preset) error { 376 return errors.New("fail") 377 }, 378 } 379 ts := newTestMetricServer(mock) 380 err := ts.UpdatePreset("foo", []byte("notjson")) 381 assert.Error(t, err) 382 } 383 384 func TestDeletePreset_Error(t *testing.T) { 385 mock := &mockMetricsReaderWriter{ 386 DeletePresetFunc: func(string) error { 387 return errors.New("fail") 388 }, 389 } 390 ts := newTestMetricServer(mock) 391 err := ts.DeletePreset("foo") 392 assert.Error(t, err) 393 } 394