package httpapi import ( "bytes" "context" "encoding/json" "errors" "io" "log/slog" "net/http" "net/http/httptest" "testing" devicecontrol "git.cactoz.su/cacto/home_automatization/proto/device_control" ) type fakeDispatcher struct { turnOnReq *devicecontrol.TurnOnRequest turnOffReq *devicecontrol.TurnOffRequest setLevelReq *devicecontrol.SetLevelRequest result *devicecontrol.CommandResult err error } func (f *fakeDispatcher) TurnOn(_ context.Context, req *devicecontrol.TurnOnRequest) (*devicecontrol.CommandResult, error) { f.turnOnReq = req return f.result, f.err } func (f *fakeDispatcher) TurnOff(_ context.Context, req *devicecontrol.TurnOffRequest) (*devicecontrol.CommandResult, error) { f.turnOffReq = req return f.result, f.err } func (f *fakeDispatcher) SetLevel(_ context.Context, req *devicecontrol.SetLevelRequest) (*devicecontrol.CommandResult, error) { f.setLevelReq = req return f.result, f.err } func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func decodeResult(t *testing.T, rec *httptest.ResponseRecorder) commandResult { t.Helper() var res commandResult if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil { t.Fatalf("decode response body %q: %v", rec.Body.String(), err) } return res } func TestTurnOn_Success(t *testing.T) { fake := &fakeDispatcher{result: &devicecontrol.CommandResult{Success: true}} router := NewRouter(fake, discardLogger()) req := httptest.NewRequest(http.MethodPost, "/devices/fan-1/turn-on", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want 200", rec.Code) } if fake.turnOnReq == nil || fake.turnOnReq.DeviceId != "fan-1" { t.Fatalf("got TurnOn request %+v, want device_id=fan-1", fake.turnOnReq) } if res := decodeResult(t, rec); !res.Success { t.Fatalf("got %+v, want success", res) } } func TestTurnOff_Success(t *testing.T) { fake := &fakeDispatcher{result: &devicecontrol.CommandResult{Success: true}} router := NewRouter(fake, discardLogger()) req := httptest.NewRequest(http.MethodPost, "/devices/fan-1/turn-off", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want 200", rec.Code) } if fake.turnOffReq == nil || fake.turnOffReq.DeviceId != "fan-1" { t.Fatalf("got TurnOff request %+v, want device_id=fan-1", fake.turnOffReq) } } func TestSetLevel_Success(t *testing.T) { fake := &fakeDispatcher{result: &devicecontrol.CommandResult{Success: true}} router := NewRouter(fake, discardLogger()) body := bytes.NewBufferString(`{"level": 42.5}`) req := httptest.NewRequest(http.MethodPost, "/devices/light-1/set-level", body) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want 200", rec.Code) } if fake.setLevelReq == nil || fake.setLevelReq.Level != 42.5 { t.Fatalf("got SetLevel request %+v, want level=42.5", fake.setLevelReq) } } func TestSetLevel_InvalidBody(t *testing.T) { fake := &fakeDispatcher{} router := NewRouter(fake, discardLogger()) req := httptest.NewRequest(http.MethodPost, "/devices/light-1/set-level", bytes.NewBufferString(`not json`)) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("got status %d, want 400", rec.Code) } if fake.setLevelReq != nil { t.Fatal("dispatcher should not have been called with an invalid body") } } func TestBusinessFailure_PassesThroughAs200(t *testing.T) { fake := &fakeDispatcher{result: &devicecontrol.CommandResult{Success: false, Error: "device offline"}} router := NewRouter(fake, discardLogger()) req := httptest.NewRequest(http.MethodPost, "/devices/fan-1/turn-on", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got status %d, want 200 (business failures aren't transport errors)", rec.Code) } res := decodeResult(t, rec) if res.Success || res.Error != "device offline" { t.Fatalf("got %+v, want business failure passed through", res) } } func TestDispatcherError_Returns500(t *testing.T) { fake := &fakeDispatcher{err: errors.New("boom")} router := NewRouter(fake, discardLogger()) req := httptest.NewRequest(http.MethodPost, "/devices/fan-1/turn-on", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("got status %d, want 500", rec.Code) } } func TestUnknownRoute_404(t *testing.T) { router := NewRouter(&fakeDispatcher{}, discardLogger()) req := httptest.NewRequest(http.MethodGet, "/devices/fan-1/turn-on", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed && rec.Code != http.StatusNotFound { t.Fatalf("got status %d for GET on a POST-only route, want 404 or 405", rec.Code) } }