...

Source file src/github.com/cybertec-postgresql/pgwatch/v5/internal/webserver/metric_test.go

Documentation: github.com/cybertec-postgresql/pgwatch/v5/internal/webserver

     1  package webserver
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/cybertec-postgresql/pgwatch/v5/internal/metrics"
    13  	"github.com/cybertec-postgresql/pgwatch/v5/internal/testutil"
    14  	jsoniter "github.com/json-iterator/go"
    15  	"github.com/stretchr/testify/assert"
    16  )
    17  
    18  func newTestMetricServer(mrw *testutil.MockMetricsReaderWriter) *WebUIServer {
    19  	return &WebUIServer{
    20  		metricsReaderWriter: mrw,
    21  	}
    22  }
    23  
    24  func TestHandleMetrics(t *testing.T) {
    25  	t.Run("GET", func(t *testing.T) {
    26  		t.Run("Success", func(t *testing.T) {
    27  			mock := &testutil.MockMetricsReaderWriter{
    28  				GetMetricsFunc: func() (*metrics.Metrics, error) {
    29  					return &metrics.Metrics{MetricDefs: map[string]metrics.Metric{"foo": {Description: "foo"}}}, nil
    30  				},
    31  			}
    32  			ts := newTestMetricServer(mock)
    33  			r := httptest.NewRequest(http.MethodGet, "/metric", nil)
    34  			w := httptest.NewRecorder()
    35  			ts.handleMetrics(w, r)
    36  			resp := w.Result()
    37  			defer resp.Body.Close()
    38  			assert.Equal(t, http.StatusOK, resp.StatusCode)
    39  			body, _ := io.ReadAll(resp.Body)
    40  			var got map[string]metrics.Metric
    41  			assert.NoError(t, jsoniter.ConfigFastest.Unmarshal(body, &got))
    42  			assert.Contains(t, got, "foo")
    43  		})
    44  
    45  		t.Run("Failure", func(t *testing.T) {
    46  			mock := &testutil.MockMetricsReaderWriter{
    47  				GetMetricsFunc: func() (*metrics.Metrics, error) {
    48  					return nil, errors.New("fail")
    49  				},
    50  			}
    51  			ts := newTestMetricServer(mock)
    52  			r := httptest.NewRequest(http.MethodGet, "/metric", nil)
    53  			w := httptest.NewRecorder()
    54  			ts.handleMetrics(w, r)
    55  			resp := w.Result()
    56  			defer resp.Body.Close()
    57  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
    58  			body, _ := io.ReadAll(resp.Body)
    59  			assert.Contains(t, string(body), "fail")
    60  		})
    61  	})
    62  
    63  	t.Run("POST", func(t *testing.T) {
    64  		t.Run("Success", func(t *testing.T) {
    65  			var createdName string
    66  			var createdMetric metrics.Metric
    67  			mock := &testutil.MockMetricsReaderWriter{
    68  				CreateMetricFunc: func(name string, m metrics.Metric) error {
    69  					createdName = name
    70  					createdMetric = m
    71  					return nil
    72  				},
    73  			}
    74  			ts := newTestMetricServer(mock)
    75  
    76  			// Test the map-based JSON format expected by collection endpoint
    77  			metricData := map[string]metrics.Metric{
    78  				"bar": {Description: "bar"},
    79  			}
    80  			b, _ := jsoniter.ConfigFastest.Marshal(metricData)
    81  			r := httptest.NewRequest(http.MethodPost, "/metric", bytes.NewReader(b))
    82  			w := httptest.NewRecorder()
    83  			ts.handleMetrics(w, r)
    84  			resp := w.Result()
    85  			defer resp.Body.Close()
    86  			assert.Equal(t, http.StatusCreated, resp.StatusCode)
    87  			assert.Equal(t, "bar", createdName)
    88  			assert.Equal(t, metrics.Metric{Description: "bar"}, createdMetric)
    89  		})
    90  
    91  		t.Run("ReaderFailure", func(t *testing.T) {
    92  			mock := &testutil.MockMetricsReaderWriter{
    93  				CreateMetricFunc: func(_ string, _ metrics.Metric) error {
    94  					return nil
    95  				},
    96  			}
    97  			ts := newTestMetricServer(mock)
    98  			r := httptest.NewRequest(http.MethodPost, "/metric", &errorReader{})
    99  			w := httptest.NewRecorder()
   100  			ts.handleMetrics(w, r)
   101  			resp := w.Result()
   102  			defer resp.Body.Close()
   103  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   104  			body, _ := io.ReadAll(resp.Body)
   105  			assert.Contains(t, string(body), "mock read error")
   106  		})
   107  
   108  		t.Run("CreateFailure", func(t *testing.T) {
   109  			mock := &testutil.MockMetricsReaderWriter{
   110  				CreateMetricFunc: func(_ string, _ metrics.Metric) error {
   111  					return errors.New("fail")
   112  				},
   113  			}
   114  			ts := newTestMetricServer(mock)
   115  			metricData := map[string]metrics.Metric{
   116  				"bar": {Description: "bar"},
   117  			}
   118  			b, _ := jsoniter.ConfigFastest.Marshal(metricData)
   119  			r := httptest.NewRequest(http.MethodPost, "/metric", bytes.NewReader(b))
   120  			w := httptest.NewRecorder()
   121  			ts.handleMetrics(w, r)
   122  			resp := w.Result()
   123  			defer resp.Body.Close()
   124  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   125  			body, _ := io.ReadAll(resp.Body)
   126  			assert.Contains(t, string(body), "fail")
   127  		})
   128  
   129  		t.Run("Conflict", func(t *testing.T) {
   130  			mock := &testutil.MockMetricsReaderWriter{
   131  				CreateMetricFunc: func(_ string, _ metrics.Metric) error {
   132  					return metrics.ErrMetricExists
   133  				},
   134  			}
   135  			ts := newTestMetricServer(mock)
   136  			metricData := map[string]metrics.Metric{
   137  				"bar": {Description: "bar"},
   138  			}
   139  			b, _ := jsoniter.ConfigFastest.Marshal(metricData)
   140  			r := httptest.NewRequest(http.MethodPost, "/metric", 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.StatusConflict, resp.StatusCode)
   146  			body, _ := io.ReadAll(resp.Body)
   147  			assert.Contains(t, string(body), "metric already exists")
   148  		})
   149  	})
   150  
   151  	t.Run("OPTIONS", func(t *testing.T) {
   152  		mock := &testutil.MockMetricsReaderWriter{}
   153  		ts := newTestMetricServer(mock)
   154  		r := httptest.NewRequest(http.MethodOptions, "/metric", nil)
   155  		w := httptest.NewRecorder()
   156  		ts.handleMetrics(w, r)
   157  		resp := w.Result()
   158  		defer resp.Body.Close()
   159  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   160  		assert.Equal(t, "GET, POST, OPTIONS", resp.Header.Get("Allow"))
   161  	})
   162  
   163  	t.Run("MethodNotAllowed", func(t *testing.T) {
   164  		mock := &testutil.MockMetricsReaderWriter{}
   165  		ts := newTestMetricServer(mock)
   166  		r := httptest.NewRequest(http.MethodPut, "/metric", nil)
   167  		w := httptest.NewRecorder()
   168  		ts.handleMetrics(w, r)
   169  		resp := w.Result()
   170  		defer resp.Body.Close()
   171  		assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
   172  		assert.Equal(t, "GET, POST, OPTIONS", resp.Header.Get("Allow"))
   173  	})
   174  }
   175  
   176  type errorReader struct{}
   177  
   178  func (e *errorReader) Read([]byte) (n int, err error) {
   179  	return 0, errors.New("mock read error")
   180  }
   181  
   182  func TestGetMetrics_Error(t *testing.T) {
   183  	mock := &testutil.MockMetricsReaderWriter{
   184  		GetMetricsFunc: func() (*metrics.Metrics, error) {
   185  			return nil, errors.New("fail")
   186  		},
   187  	}
   188  	ts := newTestMetricServer(mock)
   189  	_, err := ts.GetMetrics()
   190  	assert.Error(t, err)
   191  }
   192  
   193  func TestUpdateMetric_Error(t *testing.T) {
   194  	mock := &testutil.MockMetricsReaderWriter{
   195  		UpdateMetricFunc: func(_ string, _ metrics.Metric) error {
   196  			return errors.New("fail")
   197  		},
   198  	}
   199  	ts := newTestMetricServer(mock)
   200  	err := ts.UpdateMetric("foo", []byte("notjson"))
   201  	assert.Error(t, err)
   202  }
   203  
   204  func TestDeleteMetric_Error(t *testing.T) {
   205  	mock := &testutil.MockMetricsReaderWriter{
   206  		DeleteMetricFunc: func(_ string) error {
   207  			return errors.New("fail")
   208  		},
   209  	}
   210  	ts := newTestMetricServer(mock)
   211  	err := ts.DeleteMetric("foo")
   212  	assert.Error(t, err)
   213  }
   214  
   215  func TestHandlePresets(t *testing.T) {
   216  	t.Run("GET", func(t *testing.T) {
   217  		t.Run("Success", func(t *testing.T) {
   218  			mock := &testutil.MockMetricsReaderWriter{
   219  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   220  					return &metrics.Metrics{PresetDefs: map[string]metrics.Preset{"foo": {Description: "foo"}}}, nil
   221  				},
   222  			}
   223  			ts := newTestMetricServer(mock)
   224  			r := httptest.NewRequest(http.MethodGet, "/preset", nil)
   225  			w := httptest.NewRecorder()
   226  			ts.handlePresets(w, r)
   227  			resp := w.Result()
   228  			defer resp.Body.Close()
   229  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   230  			body, _ := io.ReadAll(resp.Body)
   231  			var got map[string]metrics.Preset
   232  			assert.NoError(t, jsoniter.ConfigFastest.Unmarshal(body, &got))
   233  			assert.Contains(t, got, "foo")
   234  		})
   235  
   236  		t.Run("Failure", func(t *testing.T) {
   237  			mock := &testutil.MockMetricsReaderWriter{
   238  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   239  					return nil, errors.New("fail")
   240  				},
   241  			}
   242  			ts := newTestMetricServer(mock)
   243  			r := httptest.NewRequest(http.MethodGet, "/preset", nil)
   244  			w := httptest.NewRecorder()
   245  			ts.handlePresets(w, r)
   246  			resp := w.Result()
   247  			defer resp.Body.Close()
   248  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   249  			body, _ := io.ReadAll(resp.Body)
   250  			assert.Contains(t, string(body), "fail")
   251  		})
   252  	})
   253  
   254  	t.Run("POST", func(t *testing.T) {
   255  		t.Run("Success", func(t *testing.T) {
   256  			var createdName string
   257  			var createdPreset metrics.Preset
   258  			mock := &testutil.MockMetricsReaderWriter{
   259  				CreatePresetFunc: func(name string, p metrics.Preset) error {
   260  					createdName = name
   261  					createdPreset = p
   262  					return nil
   263  				},
   264  			}
   265  			ts := newTestMetricServer(mock)
   266  			p := metrics.Preset{Description: "bar"}
   267  			// Use map format for collection endpoint
   268  			presetMap := map[string]metrics.Preset{"bar": p}
   269  			b, _ := jsoniter.ConfigFastest.Marshal(presetMap)
   270  			r := httptest.NewRequest(http.MethodPost, "/preset", bytes.NewReader(b))
   271  			w := httptest.NewRecorder()
   272  			ts.handlePresets(w, r)
   273  			resp := w.Result()
   274  			defer resp.Body.Close()
   275  			assert.Equal(t, http.StatusCreated, resp.StatusCode)
   276  			assert.Equal(t, "bar", createdName)
   277  			assert.Equal(t, p, createdPreset)
   278  		})
   279  
   280  		t.Run("ReaderFailure", func(t *testing.T) {
   281  			mock := &testutil.MockMetricsReaderWriter{
   282  				UpdatePresetFunc: func(string, metrics.Preset) error {
   283  					return nil
   284  				},
   285  			}
   286  			ts := newTestMetricServer(mock)
   287  			r := httptest.NewRequest(http.MethodPost, "/preset?name=bar", &errorReader{})
   288  			w := httptest.NewRecorder()
   289  			ts.handlePresets(w, r)
   290  			resp := w.Result()
   291  			defer resp.Body.Close()
   292  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   293  			body, _ := io.ReadAll(resp.Body)
   294  			assert.Contains(t, string(body), "mock read error")
   295  		})
   296  
   297  		t.Run("CreateFailure", func(t *testing.T) {
   298  			mock := &testutil.MockMetricsReaderWriter{
   299  				CreatePresetFunc: func(string, metrics.Preset) error {
   300  					return errors.New("fail")
   301  				},
   302  			}
   303  			ts := newTestMetricServer(mock)
   304  			p := metrics.Preset{Description: "bar"}
   305  			// Use map format for collection endpoint
   306  			presetMap := map[string]metrics.Preset{"bar": p}
   307  			b, _ := jsoniter.ConfigFastest.Marshal(presetMap)
   308  			r := httptest.NewRequest(http.MethodPost, "/preset", bytes.NewReader(b))
   309  			w := httptest.NewRecorder()
   310  			ts.handlePresets(w, r)
   311  			resp := w.Result()
   312  			defer resp.Body.Close()
   313  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   314  			body, _ := io.ReadAll(resp.Body)
   315  			assert.Contains(t, string(body), "fail")
   316  		})
   317  	})
   318  
   319  	t.Run("OPTIONS", func(t *testing.T) {
   320  		mock := &testutil.MockMetricsReaderWriter{}
   321  		ts := newTestMetricServer(mock)
   322  		r := httptest.NewRequest(http.MethodOptions, "/preset", nil)
   323  		w := httptest.NewRecorder()
   324  		ts.handlePresets(w, r)
   325  		resp := w.Result()
   326  		defer resp.Body.Close()
   327  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   328  		assert.Equal(t, "GET, POST, OPTIONS", resp.Header.Get("Allow"))
   329  	})
   330  
   331  	t.Run("MethodNotAllowed", func(t *testing.T) {
   332  		mock := &testutil.MockMetricsReaderWriter{}
   333  		ts := newTestMetricServer(mock)
   334  		r := httptest.NewRequest(http.MethodPut, "/preset", nil)
   335  		w := httptest.NewRecorder()
   336  		ts.handlePresets(w, r)
   337  		resp := w.Result()
   338  		defer resp.Body.Close()
   339  		assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
   340  		assert.Equal(t, "GET, POST, OPTIONS", resp.Header.Get("Allow"))
   341  	})
   342  }
   343  
   344  func TestGetPresets_Error(t *testing.T) {
   345  	mock := &testutil.MockMetricsReaderWriter{
   346  		GetMetricsFunc: func() (*metrics.Metrics, error) {
   347  			return nil, errors.New("fail")
   348  		},
   349  	}
   350  	ts := newTestMetricServer(mock)
   351  	_, err := ts.GetPresets()
   352  	assert.Error(t, err)
   353  }
   354  
   355  func TestUpdatePreset_Error(t *testing.T) {
   356  	mock := &testutil.MockMetricsReaderWriter{
   357  		UpdatePresetFunc: func(string, metrics.Preset) error {
   358  			return errors.New("fail")
   359  		},
   360  	}
   361  	ts := newTestMetricServer(mock)
   362  	err := ts.UpdatePreset("foo", []byte("notjson"))
   363  	assert.Error(t, err)
   364  }
   365  
   366  func TestDeletePreset_Error(t *testing.T) {
   367  	mock := &testutil.MockMetricsReaderWriter{
   368  		DeletePresetFunc: func(string) error {
   369  			return errors.New("fail")
   370  		},
   371  	}
   372  	ts := newTestMetricServer(mock)
   373  	err := ts.DeletePreset("foo")
   374  	assert.Error(t, err)
   375  }
   376  
   377  // Helper function to create HTTP requests with path values for testing individual metric endpoints
   378  func newMetricItemRequest(method, name string, body io.Reader) *http.Request {
   379  	url := "/metric/" + name
   380  	r := httptest.NewRequest(method, url, body)
   381  	r.SetPathValue("name", name)
   382  	return r
   383  }
   384  
   385  func TestHandleMetricItem(t *testing.T) {
   386  	t.Run("GET", func(t *testing.T) {
   387  		t.Run("Success", func(t *testing.T) {
   388  			metric := metrics.Metric{Description: "test metric", SQLs: map[int]string{130000: "SELECT 1"}}
   389  			mock := &testutil.MockMetricsReaderWriter{
   390  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   391  					return &metrics.Metrics{
   392  						MetricDefs: map[string]metrics.Metric{"test-metric": metric},
   393  					}, nil
   394  				},
   395  			}
   396  			ts := newTestMetricServer(mock)
   397  			r := newMetricItemRequest(http.MethodGet, "test-metric", nil)
   398  			w := httptest.NewRecorder()
   399  			ts.handleMetricItem(w, r)
   400  			resp := w.Result()
   401  			defer resp.Body.Close()
   402  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   403  			assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
   404  
   405  			var returnedMetric metrics.Metric
   406  			body, _ := io.ReadAll(resp.Body)
   407  			assert.NoError(t, jsoniter.ConfigFastest.Unmarshal(body, &returnedMetric))
   408  			assert.Equal(t, metric.Description, returnedMetric.Description)
   409  		})
   410  
   411  		t.Run("NotFound", func(t *testing.T) {
   412  			mock := &testutil.MockMetricsReaderWriter{
   413  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   414  					return &metrics.Metrics{MetricDefs: map[string]metrics.Metric{}}, nil
   415  				},
   416  			}
   417  			ts := newTestMetricServer(mock)
   418  			r := newMetricItemRequest(http.MethodGet, "nonexistent", nil)
   419  			w := httptest.NewRecorder()
   420  			ts.handleMetricItem(w, r)
   421  			resp := w.Result()
   422  			defer resp.Body.Close()
   423  			assert.Equal(t, http.StatusNotFound, resp.StatusCode)
   424  			body, _ := io.ReadAll(resp.Body)
   425  			assert.Contains(t, string(body), "metric not found")
   426  		})
   427  
   428  		t.Run("GetMetricsError", func(t *testing.T) {
   429  			mock := &testutil.MockMetricsReaderWriter{
   430  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   431  					return nil, errors.New("database connection failed")
   432  				},
   433  			}
   434  			ts := newTestMetricServer(mock)
   435  			r := newMetricItemRequest(http.MethodGet, "test-metric", nil)
   436  			w := httptest.NewRecorder()
   437  			ts.handleMetricItem(w, r)
   438  			resp := w.Result()
   439  			defer resp.Body.Close()
   440  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   441  			body, _ := io.ReadAll(resp.Body)
   442  			assert.Contains(t, string(body), "database connection failed")
   443  		})
   444  	})
   445  
   446  	t.Run("PUT", func(t *testing.T) {
   447  		t.Run("Success", func(t *testing.T) {
   448  			var updatedName string
   449  			var updatedMetric metrics.Metric
   450  			mock := &testutil.MockMetricsReaderWriter{
   451  				UpdateMetricFunc: func(name string, m metrics.Metric) error {
   452  					updatedName = name
   453  					updatedMetric = m
   454  					return nil
   455  				},
   456  			}
   457  			ts := newTestMetricServer(mock)
   458  
   459  			metric := metrics.Metric{Description: "updated metric", SQLs: map[int]string{130000: "SELECT 2"}}
   460  			b, _ := jsoniter.ConfigFastest.Marshal(metric)
   461  			r := newMetricItemRequest(http.MethodPut, "test-metric", bytes.NewReader(b))
   462  			w := httptest.NewRecorder()
   463  			ts.handleMetricItem(w, r)
   464  			resp := w.Result()
   465  			defer resp.Body.Close()
   466  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   467  			assert.Equal(t, "test-metric", updatedName)
   468  			assert.Equal(t, metric.Description, updatedMetric.Description)
   469  		})
   470  
   471  		t.Run("InvalidRequestBody", func(t *testing.T) {
   472  			mock := &testutil.MockMetricsReaderWriter{}
   473  			ts := newTestMetricServer(mock)
   474  			r := newMetricItemRequest(http.MethodPut, "test-metric", &errorReader{})
   475  			w := httptest.NewRecorder()
   476  			ts.handleMetricItem(w, r)
   477  			resp := w.Result()
   478  			defer resp.Body.Close()
   479  			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   480  			body, _ := io.ReadAll(resp.Body)
   481  			assert.Contains(t, string(body), "mock read error")
   482  		})
   483  
   484  		t.Run("InvalidJSON", func(t *testing.T) {
   485  			mock := &testutil.MockMetricsReaderWriter{}
   486  			ts := newTestMetricServer(mock)
   487  			r := newMetricItemRequest(http.MethodPut, "test-metric", strings.NewReader("invalid json"))
   488  			w := httptest.NewRecorder()
   489  			ts.handleMetricItem(w, r)
   490  			resp := w.Result()
   491  			defer resp.Body.Close()
   492  			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   493  			body, _ := io.ReadAll(resp.Body)
   494  			assert.Contains(t, string(body), "invalid json")
   495  		})
   496  
   497  		t.Run("UpdateError", func(t *testing.T) {
   498  			mock := &testutil.MockMetricsReaderWriter{
   499  				UpdateMetricFunc: func(string, metrics.Metric) error {
   500  					return errors.New("update operation failed")
   501  				},
   502  			}
   503  			ts := newTestMetricServer(mock)
   504  
   505  			metric := metrics.Metric{Description: "test metric"}
   506  			b, _ := jsoniter.ConfigFastest.Marshal(metric)
   507  			r := newMetricItemRequest(http.MethodPut, "test-metric", bytes.NewReader(b))
   508  			w := httptest.NewRecorder()
   509  			ts.handleMetricItem(w, r)
   510  			resp := w.Result()
   511  			defer resp.Body.Close()
   512  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   513  			body, _ := io.ReadAll(resp.Body)
   514  			assert.Contains(t, string(body), "update operation failed")
   515  		})
   516  	})
   517  
   518  	t.Run("DELETE", func(t *testing.T) {
   519  		t.Run("Success", func(t *testing.T) {
   520  			var deletedName string
   521  			mock := &testutil.MockMetricsReaderWriter{
   522  				DeleteMetricFunc: func(name string) error {
   523  					deletedName = name
   524  					return nil
   525  				},
   526  			}
   527  			ts := newTestMetricServer(mock)
   528  			r := newMetricItemRequest(http.MethodDelete, "test-metric", nil)
   529  			w := httptest.NewRecorder()
   530  			ts.handleMetricItem(w, r)
   531  			resp := w.Result()
   532  			defer resp.Body.Close()
   533  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   534  			assert.Equal(t, "test-metric", deletedName)
   535  		})
   536  
   537  		t.Run("DeleteError", func(t *testing.T) {
   538  			mock := &testutil.MockMetricsReaderWriter{
   539  				DeleteMetricFunc: func(string) error {
   540  					return errors.New("delete operation failed")
   541  				},
   542  			}
   543  			ts := newTestMetricServer(mock)
   544  			r := newMetricItemRequest(http.MethodDelete, "test-metric", nil)
   545  			w := httptest.NewRecorder()
   546  			ts.handleMetricItem(w, r)
   547  			resp := w.Result()
   548  			defer resp.Body.Close()
   549  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   550  			body, _ := io.ReadAll(resp.Body)
   551  			assert.Contains(t, string(body), "delete operation failed")
   552  		})
   553  	})
   554  
   555  	t.Run("EmptyName", func(t *testing.T) {
   556  		mock := &testutil.MockMetricsReaderWriter{}
   557  		ts := newTestMetricServer(mock)
   558  		r := newMetricItemRequest(http.MethodGet, "", nil)
   559  		w := httptest.NewRecorder()
   560  		ts.handleMetricItem(w, r)
   561  		resp := w.Result()
   562  		defer resp.Body.Close()
   563  		assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   564  		body, _ := io.ReadAll(resp.Body)
   565  		assert.Contains(t, string(body), "metric name is required")
   566  	})
   567  
   568  	t.Run("OPTIONS", func(t *testing.T) {
   569  		mock := &testutil.MockMetricsReaderWriter{}
   570  		ts := newTestMetricServer(mock)
   571  		r := newMetricItemRequest(http.MethodOptions, "test", nil)
   572  		w := httptest.NewRecorder()
   573  		ts.handleMetricItem(w, r)
   574  		resp := w.Result()
   575  		defer resp.Body.Close()
   576  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   577  		assert.Equal(t, "GET, PUT, DELETE, OPTIONS", resp.Header.Get("Allow"))
   578  	})
   579  
   580  	t.Run("MethodNotAllowed", func(t *testing.T) {
   581  		mock := &testutil.MockMetricsReaderWriter{}
   582  		ts := newTestMetricServer(mock)
   583  		r := newMetricItemRequest(http.MethodPost, "test", nil)
   584  		w := httptest.NewRecorder()
   585  		ts.handleMetricItem(w, r)
   586  		resp := w.Result()
   587  		defer resp.Body.Close()
   588  		assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
   589  		assert.Equal(t, "GET, PUT, DELETE, OPTIONS", resp.Header.Get("Allow"))
   590  	})
   591  }
   592  
   593  // Helper function to create HTTP requests with path values for testing individual preset endpoints
   594  func newPresetItemRequest(method, name string, body io.Reader) *http.Request {
   595  	url := "/preset/" + name
   596  	r := httptest.NewRequest(method, url, body)
   597  	r.SetPathValue("name", name)
   598  	return r
   599  }
   600  
   601  // Tests for new REST-compliant preset endpoints
   602  
   603  func TestHandlePresetItem(t *testing.T) {
   604  	t.Run("GET", func(t *testing.T) {
   605  		t.Run("Success", func(t *testing.T) {
   606  			preset := metrics.Preset{Description: "test preset", Metrics: map[string]float64{"cpu": 1.0}}
   607  			mock := &testutil.MockMetricsReaderWriter{
   608  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   609  					return &metrics.Metrics{
   610  						PresetDefs: map[string]metrics.Preset{"test-preset": preset},
   611  					}, nil
   612  				},
   613  			}
   614  			ts := newTestMetricServer(mock)
   615  			r := newPresetItemRequest(http.MethodGet, "test-preset", nil)
   616  			w := httptest.NewRecorder()
   617  			ts.handlePresetItem(w, r)
   618  			resp := w.Result()
   619  			defer resp.Body.Close()
   620  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   621  			assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
   622  
   623  			var returnedPreset metrics.Preset
   624  			body, _ := io.ReadAll(resp.Body)
   625  			assert.NoError(t, jsoniter.ConfigFastest.Unmarshal(body, &returnedPreset))
   626  			assert.Equal(t, preset.Description, returnedPreset.Description)
   627  		})
   628  
   629  		t.Run("NotFound", func(t *testing.T) {
   630  			mock := &testutil.MockMetricsReaderWriter{
   631  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   632  					return &metrics.Metrics{PresetDefs: map[string]metrics.Preset{}}, nil
   633  				},
   634  			}
   635  			ts := newTestMetricServer(mock)
   636  			r := newPresetItemRequest(http.MethodGet, "nonexistent", nil)
   637  			w := httptest.NewRecorder()
   638  			ts.handlePresetItem(w, r)
   639  			resp := w.Result()
   640  			defer resp.Body.Close()
   641  			assert.Equal(t, http.StatusNotFound, resp.StatusCode)
   642  			body, _ := io.ReadAll(resp.Body)
   643  			assert.Contains(t, string(body), "preset not found")
   644  		})
   645  
   646  		t.Run("GetMetricsError", func(t *testing.T) {
   647  			mock := &testutil.MockMetricsReaderWriter{
   648  				GetMetricsFunc: func() (*metrics.Metrics, error) {
   649  					return nil, errors.New("database connection failed")
   650  				},
   651  			}
   652  			ts := newTestMetricServer(mock)
   653  			r := newPresetItemRequest(http.MethodGet, "test-preset", nil)
   654  			w := httptest.NewRecorder()
   655  			ts.handlePresetItem(w, r)
   656  			resp := w.Result()
   657  			defer resp.Body.Close()
   658  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   659  			body, _ := io.ReadAll(resp.Body)
   660  			assert.Contains(t, string(body), "database connection failed")
   661  		})
   662  	})
   663  
   664  	t.Run("PUT", func(t *testing.T) {
   665  		t.Run("Success", func(t *testing.T) {
   666  			var updatedName string
   667  			var updatedPreset metrics.Preset
   668  			mock := &testutil.MockMetricsReaderWriter{
   669  				UpdatePresetFunc: func(name string, p metrics.Preset) error {
   670  					updatedName = name
   671  					updatedPreset = p
   672  					return nil
   673  				},
   674  			}
   675  			ts := newTestMetricServer(mock)
   676  
   677  			preset := metrics.Preset{Description: "updated preset", Metrics: map[string]float64{"memory": 2.0}}
   678  			b, _ := jsoniter.ConfigFastest.Marshal(preset)
   679  			r := newPresetItemRequest(http.MethodPut, "test-preset", bytes.NewReader(b))
   680  			w := httptest.NewRecorder()
   681  			ts.handlePresetItem(w, r)
   682  			resp := w.Result()
   683  			defer resp.Body.Close()
   684  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   685  			assert.Equal(t, "test-preset", updatedName)
   686  			assert.Equal(t, preset.Description, updatedPreset.Description)
   687  		})
   688  
   689  		t.Run("InvalidRequestBody", func(t *testing.T) {
   690  			mock := &testutil.MockMetricsReaderWriter{}
   691  			ts := newTestMetricServer(mock)
   692  			r := newPresetItemRequest(http.MethodPut, "test-preset", &errorReader{})
   693  			w := httptest.NewRecorder()
   694  			ts.handlePresetItem(w, r)
   695  			resp := w.Result()
   696  			defer resp.Body.Close()
   697  			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   698  			body, _ := io.ReadAll(resp.Body)
   699  			assert.Contains(t, string(body), "invalid request body")
   700  		})
   701  
   702  		t.Run("InvalidJSON", func(t *testing.T) {
   703  			mock := &testutil.MockMetricsReaderWriter{}
   704  			ts := newTestMetricServer(mock)
   705  			r := newPresetItemRequest(http.MethodPut, "test-preset", strings.NewReader("invalid json"))
   706  			w := httptest.NewRecorder()
   707  			ts.handlePresetItem(w, r)
   708  			resp := w.Result()
   709  			defer resp.Body.Close()
   710  			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   711  			body, _ := io.ReadAll(resp.Body)
   712  			assert.Contains(t, string(body), "invalid JSON format")
   713  		})
   714  
   715  		t.Run("UpdateError", func(t *testing.T) {
   716  			mock := &testutil.MockMetricsReaderWriter{
   717  				UpdatePresetFunc: func(string, metrics.Preset) error {
   718  					return errors.New("update operation failed")
   719  				},
   720  			}
   721  			ts := newTestMetricServer(mock)
   722  
   723  			preset := metrics.Preset{Description: "test preset"}
   724  			b, _ := jsoniter.ConfigFastest.Marshal(preset)
   725  			r := newPresetItemRequest(http.MethodPut, "test-preset", bytes.NewReader(b))
   726  			w := httptest.NewRecorder()
   727  			ts.handlePresetItem(w, r)
   728  			resp := w.Result()
   729  			defer resp.Body.Close()
   730  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   731  			body, _ := io.ReadAll(resp.Body)
   732  			assert.Contains(t, string(body), "update operation failed")
   733  		})
   734  	})
   735  
   736  	t.Run("DELETE", func(t *testing.T) {
   737  		t.Run("Success", func(t *testing.T) {
   738  			var deletedName string
   739  			mock := &testutil.MockMetricsReaderWriter{
   740  				DeletePresetFunc: func(name string) error {
   741  					deletedName = name
   742  					return nil
   743  				},
   744  			}
   745  			ts := newTestMetricServer(mock)
   746  			r := newPresetItemRequest(http.MethodDelete, "test-preset", nil)
   747  			w := httptest.NewRecorder()
   748  			ts.handlePresetItem(w, r)
   749  			resp := w.Result()
   750  			defer resp.Body.Close()
   751  			assert.Equal(t, http.StatusOK, resp.StatusCode)
   752  			assert.Equal(t, "test-preset", deletedName)
   753  		})
   754  
   755  		t.Run("DeleteError", func(t *testing.T) {
   756  			mock := &testutil.MockMetricsReaderWriter{
   757  				DeletePresetFunc: func(string) error {
   758  					return errors.New("delete operation failed")
   759  				},
   760  			}
   761  			ts := newTestMetricServer(mock)
   762  			r := newPresetItemRequest(http.MethodDelete, "test-preset", nil)
   763  			w := httptest.NewRecorder()
   764  			ts.handlePresetItem(w, r)
   765  			resp := w.Result()
   766  			defer resp.Body.Close()
   767  			assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
   768  			body, _ := io.ReadAll(resp.Body)
   769  			assert.Contains(t, string(body), "delete operation failed")
   770  		})
   771  	})
   772  
   773  	t.Run("EmptyName", func(t *testing.T) {
   774  		mock := &testutil.MockMetricsReaderWriter{}
   775  		ts := newTestMetricServer(mock)
   776  		r := newPresetItemRequest(http.MethodGet, "", nil)
   777  		w := httptest.NewRecorder()
   778  		ts.handlePresetItem(w, r)
   779  		resp := w.Result()
   780  		defer resp.Body.Close()
   781  		assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
   782  		body, _ := io.ReadAll(resp.Body)
   783  		assert.Contains(t, string(body), "preset name is required")
   784  	})
   785  
   786  	t.Run("OPTIONS", func(t *testing.T) {
   787  		mock := &testutil.MockMetricsReaderWriter{}
   788  		ts := newTestMetricServer(mock)
   789  		r := newPresetItemRequest(http.MethodOptions, "test", nil)
   790  		w := httptest.NewRecorder()
   791  		ts.handlePresetItem(w, r)
   792  		resp := w.Result()
   793  		defer resp.Body.Close()
   794  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   795  		assert.Equal(t, "GET, PUT, DELETE, OPTIONS", resp.Header.Get("Allow"))
   796  	})
   797  
   798  	t.Run("MethodNotAllowed", func(t *testing.T) {
   799  		mock := &testutil.MockMetricsReaderWriter{}
   800  		ts := newTestMetricServer(mock)
   801  		r := newPresetItemRequest(http.MethodPost, "test", nil)
   802  		w := httptest.NewRecorder()
   803  		ts.handlePresetItem(w, r)
   804  		resp := w.Result()
   805  		defer resp.Body.Close()
   806  		assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
   807  		assert.Equal(t, "GET, PUT, DELETE, OPTIONS", resp.Header.Get("Allow"))
   808  	})
   809  }
   810