diff options
Diffstat (limited to 'api')
| -rw-r--r-- | api/api.go | 84 | ||||
| -rw-r--r-- | api/api_service_mock_test.go | 80 | ||||
| -rw-r--r-- | api/api_test.go | 67 |
3 files changed, 231 insertions, 0 deletions
diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..893dd00 --- /dev/null +++ b/api/api.go @@ -0,0 +1,84 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/alexedwards/flow" +) + +type ScrapeRequest struct { + URL string `json:"url"` + Data map[string]any `json:"data"` +} + +type ScrapeResponse struct { + URL string `json:"url"` + Data any `json:"data"` +} + +//go:generate moq -out api_service_mock_test.go . Service +type Service interface { + ScrapeURL(url string, params map[string]any) (any, error) +} + +func NewHandler(svc Service) http.Handler { + h := &Handler{ + router: flow.New(), + svc: svc, + } + h.routes() + return h +} + +type Handler struct { + router *flow.Mux + svc Service +} + +func (h *Handler) routes() { + h.router.HandleFunc("/scrape", h.handleScrape, "POST") +} + +func (h *Handler) handleScrape(w http.ResponseWriter, r *http.Request) { + var req ScrapeRequest + if err := decodeRequest(r, &req); err != nil { + respondErr(w, http.StatusBadRequest, err) + return + } + + result, err := h.svc.ScrapeURL(req.URL, req.Data) + if err != nil { + respondErr(w, http.StatusInternalServerError, err) + return + } + + respond(w, ScrapeResponse{ + URL: req.URL, + Data: result, + }) +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.router.ServeHTTP(w, r) +} + +func decodeRequest(r *http.Request, v any) error { + return json.NewDecoder(r.Body).Decode(v) +} + +func respond(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(v) +} + +func respondErr(w http.ResponseWriter, statusCode int, err error) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(struct { + Error string `json:"error"` + }{ + Error: err.Error(), + }) +} diff --git a/api/api_service_mock_test.go b/api/api_service_mock_test.go new file mode 100644 index 0000000..a7536be --- /dev/null +++ b/api/api_service_mock_test.go @@ -0,0 +1,80 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package api + +import ( + "sync" +) + +// Ensure, that ServiceMock does implement Service. +// If this is not the case, regenerate this file with moq. +var _ Service = &ServiceMock{} + +// ServiceMock is a mock implementation of Service. +// +// func TestSomethingThatUsesService(t *testing.T) { +// +// // make and configure a mocked Service +// mockedService := &ServiceMock{ +// ScrapeURLFunc: func(url string, params map[string]any) (any, error) { +// panic("mock out the ScrapeURL method") +// }, +// } +// +// // use mockedService in code that requires Service +// // and then make assertions. +// +// } +type ServiceMock struct { + // ScrapeURLFunc mocks the ScrapeURL method. + ScrapeURLFunc func(url string, params map[string]any) (any, error) + + // calls tracks calls to the methods. + calls struct { + // ScrapeURL holds details about calls to the ScrapeURL method. + ScrapeURL []struct { + // URL is the url argument value. + URL string + // Params is the params argument value. + Params map[string]any + } + } + lockScrapeURL sync.RWMutex +} + +// ScrapeURL calls ScrapeURLFunc. +func (mock *ServiceMock) ScrapeURL(url string, params map[string]any) (any, error) { + if mock.ScrapeURLFunc == nil { + panic("ServiceMock.ScrapeURLFunc: method is nil but Service.ScrapeURL was just called") + } + callInfo := struct { + URL string + Params map[string]any + }{ + URL: url, + Params: params, + } + mock.lockScrapeURL.Lock() + mock.calls.ScrapeURL = append(mock.calls.ScrapeURL, callInfo) + mock.lockScrapeURL.Unlock() + return mock.ScrapeURLFunc(url, params) +} + +// ScrapeURLCalls gets all the calls that were made to ScrapeURL. +// Check the length with: +// +// len(mockedService.ScrapeURLCalls()) +func (mock *ServiceMock) ScrapeURLCalls() []struct { + URL string + Params map[string]any +} { + var calls []struct { + URL string + Params map[string]any + } + mock.lockScrapeURL.RLock() + calls = mock.calls.ScrapeURL + mock.lockScrapeURL.RUnlock() + return calls +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 0000000..624b684 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,67 @@ +package api_test + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "flyscrape/api" + + "github.com/stretchr/testify/require" +) + +func TestScrapeURL(t *testing.T) { + svc := &api.ServiceMock{ + ScrapeURLFunc: func(url string, params map[string]any) (any, error) { + return map[string]any{"foo": "bar"}, nil + }, + } + h := api.NewHandler(svc) + + r := httptest.NewRequest("POST", "/scrape", strings.NewReader(`{"url": "https://example.com", "data": {"foo":".foo"}}`)) + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + + require.Equal(t, w.Result().StatusCode, http.StatusOK) + require.Equal(t, w.Result().Header.Get("Content-Type"), "application/json") + + result := map[string]any{} + require.NoError(t, json.NewDecoder(w.Result().Body).Decode(&result)) + require.Equal(t, result["url"].(string), "https://example.com") + require.Equal(t, result["data"].(map[string]any)["foo"], "bar") +} + +func TestScrapeURLInternalServerError(t *testing.T) { + svc := &api.ServiceMock{ + ScrapeURLFunc: func(url string, params map[string]any) (any, error) { + return nil, errors.New("whoops") + }, + } + h := api.NewHandler(svc) + + r := httptest.NewRequest("POST", "/scrape", strings.NewReader(`{"url": "https://example.com", "data": {"foo":".foo"}}`)) + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + + require.Equal(t, w.Result().StatusCode, http.StatusInternalServerError) + require.Equal(t, w.Result().Header.Get("Content-Type"), "application/json") +} + +func TestScrapeURLBadRequest(t *testing.T) { + svc := &api.ServiceMock{ + ScrapeURLFunc: func(url string, params map[string]any) (any, error) { + return nil, errors.New("whoops") + }, + } + h := api.NewHandler(svc) + + r := httptest.NewRequest("POST", "/scrape", strings.NewReader(`{"}`)) + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + + require.Equal(t, w.Result().StatusCode, http.StatusBadRequest) + require.Equal(t, w.Result().Header.Get("Content-Type"), "application/json") +} |