package api import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "crypto-miner-server/internal/db" ) // stubConfigProvider implements ConfigProvider for handler unit tests. type stubConfigProvider struct { configJSON json.RawMessage updateErr error updated json.RawMessage } func (s *stubConfigProvider) GetConfigJSON() json.RawMessage { if len(s.configJSON) == 0 { return json.RawMessage(`{"port":8989,"pool":{"host":"pool.example.com","port":3333,"use_tls":true}}`) } return s.configJSON } func (s *stubConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error { if s.updateErr != nil { return s.updateErr } s.updated = append(json.RawMessage(nil), data...) s.configJSON = append(json.RawMessage(nil), data...) return nil } func newTestConfigHandler(t *testing.T, cp ConfigProvider) *ConfigHandler { t.Helper() database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) return NewConfigHandler(database, cp) } func TestNewConfigHandler(t *testing.T) { h := newTestConfigHandler(t, &stubConfigProvider{}) if h == nil || h.config == nil || h.db == nil { t.Fatal("NewConfigHandler returned incomplete handler") } } func TestConfigHandlerServeHTTP_MethodNotAllowed(t *testing.T) { h := newTestConfigHandler(t, &stubConfigProvider{}) for _, method := range []string{http.MethodPost, http.MethodDelete, http.MethodPatch} { req := httptest.NewRequest(method, "/api/v1/config", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Fatalf("%s: expected 405, got %d", method, rec.Code) } } } func TestConfigHandlerGetConfig(t *testing.T) { stub := &stubConfigProvider{ configJSON: json.RawMessage(`{"port":9001,"wallet":{"address":"48abc"}}`), } h := newTestConfigHandler(t, stub) req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET status=%d body=%s", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") { t.Fatalf("expected JSON content-type, got %q", ct) } var body map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["port"].(float64) != 9001 { t.Fatalf("unexpected config body: %v", body) } } func TestConfigHandlerPutConfig_SuccessReturnsUpdated(t *testing.T) { stub := &stubConfigProvider{ configJSON: json.RawMessage(`{"port":8989}`), } h := newTestConfigHandler(t, stub) payload := `{"port":9100,"pool":{"host":"new.pool","port":4444,"use_tls":false}}` req := httptest.NewRequest(http.MethodPut, "/api/v1/config", strings.NewReader(payload)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("PUT status=%d body=%s", rec.Code, rec.Body.String()) } if string(stub.updated) != payload { t.Fatalf("provider did not receive payload: %q", stub.updated) } var body map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["port"].(float64) != 9100 { t.Fatalf("response not updated: %v", body) } } func TestConfigHandlerPutConfig_InvalidJSON(t *testing.T) { h := newTestConfigHandler(t, &stubConfigProvider{}) req := httptest.NewRequest(http.MethodPut, "/api/v1/config", strings.NewReader(`{not json`)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", rec.Code) } var errBody map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &errBody); err != nil { t.Fatalf("error body not valid JSON: %s", rec.Body.String()) } if errBody["error"] != "Invalid JSON" { t.Fatalf("unexpected error: %q", errBody["error"]) } } func TestConfigHandlerPutConfig_InvalidConfigBadRequest(t *testing.T) { stub := &stubConfigProvider{ updateErr: fmt.Errorf("invalid config: unexpected EOF"), } h := newTestConfigHandler(t, stub) req := httptest.NewRequest(http.MethodPut, "/api/v1/config", strings.NewReader(`{"port":1}`)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid config, got %d body=%s", rec.Code, rec.Body.String()) } } func TestConfigHandlerPutConfig_SaveErrorInternalServerError(t *testing.T) { stub := &stubConfigProvider{ updateErr: errors.New("failed to save config: disk full"), } h := newTestConfigHandler(t, stub) req := httptest.NewRequest(http.MethodPut, "/api/v1/config", strings.NewReader(`{"port":1}`)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rec.Code) } var errBody map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &errBody); err != nil { t.Fatal(err) } if !strings.Contains(errBody["error"], "disk full") { t.Fatalf("unexpected error message: %q", errBody["error"]) } } func TestConfigHandlerPutConfig_ErrorJSONEscapesQuotes(t *testing.T) { stub := &stubConfigProvider{ updateErr: errors.New(`failed: say "hello"`), } h := newTestConfigHandler(t, stub) req := httptest.NewRequest(http.MethodPut, "/api/v1/config", strings.NewReader(`{"port":1}`)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rec.Code) } var errBody map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &errBody); err != nil { t.Fatalf("malformed JSON error response: %s", rec.Body.String()) } if errBody["error"] != `failed: say "hello"` { t.Fatalf("unexpected escaped error: %q", errBody["error"]) } } func TestConfigHandlerPutConfig_EmptyBodyInvalidJSON(t *testing.T) { h := newTestConfigHandler(t, &stubConfigProvider{}) req := httptest.NewRequest(http.MethodPut, "/api/v1/config", nil) req.Body = io.NopCloser(bytes.NewReader(nil)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400 for empty body, got %d", rec.Code) } } func TestConfigProviderInterface(t *testing.T) { var _ ConfigProvider = (*stubConfigProvider)(nil) }