summaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorPhilipp Tanlak <philipp.tanlak@gmail.com>2023-07-27 19:03:41 +0200
committerPhilipp Tanlak <philipp.tanlak@gmail.com>2023-07-27 19:03:41 +0200
commita9b61f84070cc7ca0d6e26f187c745619a91422a (patch)
treed69b67142b6de860d7da23bd5ff8c62af0aaca1e /api
init
Diffstat (limited to 'api')
-rw-r--r--api/api.go84
-rw-r--r--api/api_service_mock_test.go80
-rw-r--r--api/api_test.go67
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")
+}