diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8074a8a --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ + +run: + PLANNER_PORT=8092 PLANNER_API_KEY=testKey go run . diff --git a/handler/handler.go b/handler/handler.go index dd77c35..c5391a0 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -16,32 +16,40 @@ import ( type Server struct { syncer storage.Syncer + apiKey string logger *slog.Logger } -func NewServer(syncer storage.Syncer, logger *slog.Logger) *Server { +func NewServer(syncer storage.Syncer, apiKey string, logger *slog.Logger) *Server { return &Server{ syncer: syncer, + apiKey: apiKey, logger: logger, } } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/" { Index(w, r) return } + if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", s.apiKey) { + http.Error(w, `{"error":"not authorized"}`, http.StatusUnauthorized) + return + } + head, tail := ShiftPath(r.URL.Path) switch { case head == "sync" && tail != "/": - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, `{"error":"not found"}`, http.StatusNotFound) case head == "sync" && r.Method == http.MethodGet: s.SyncGet(w, r) case head == "sync" && r.Method == http.MethodPost: s.SyncPost(w, r) default: - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, `{"error":"not found"}`, http.StatusNotFound) } } @@ -58,13 +66,13 @@ func (s *Server) SyncGet(w http.ResponseWriter, r *http.Request) { items, err := s.syncer.Updated(timestamp) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmtError(err), http.StatusInternalServerError) return } body, err := json.Marshal(items) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmtError(err), http.StatusInternalServerError) return } @@ -74,21 +82,21 @@ func (s *Server) SyncGet(w http.ResponseWriter, r *http.Request) { func (s *Server) SyncPost(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, fmtError(err), http.StatusBadRequest) return } defer r.Body.Close() var items []planner.Syncable if err := json.Unmarshal(body, &items); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, fmtError(err), http.StatusBadRequest) return } for _, item := range items { item.Updated = time.Now() if err := s.syncer.Update(item); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmtError(err), http.StatusInternalServerError) return } } @@ -112,3 +120,7 @@ func ShiftPath(p string) (head, tail string) { func Index(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"status":"ok"}`) } + +func fmtError(err error) string { + return fmt.Sprintf(`{"error":%q}`, err.Error()) +} diff --git a/handler/handler_test.go b/handler/handler_test.go index 278ff0f..e8fb2d3 100644 --- a/handler/handler_test.go +++ b/handler/handler_test.go @@ -19,6 +19,40 @@ import ( "code.ewintr.nl/planner/storage" ) +func TestServerServeHTTP(t *testing.T) { + t.Parallel() + + apiKey := "test" + srv := handler.NewServer(storage.NewMemory(), apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + for _, tc := range []struct { + name string + key string + url string + method string + expStatus int + }{ + { + name: "index always visible", + url: "/", + method: http.MethodGet, + expStatus: http.StatusOK, + }, + } { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(tc.method, tc.url, nil) + if err != nil { + t.Errorf("exp nil, got %v", err) + } + res := httptest.NewRecorder() + srv.ServeHTTP(res, req) + if res.Result().StatusCode != tc.expStatus { + t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode) + } + }) + } +} + func TestSyncGet(t *testing.T) { t.Parallel() @@ -37,7 +71,8 @@ func TestSyncGet(t *testing.T) { } } - srv := handler.NewServer(mem, slog.New(slog.NewJSONHandler(os.Stdout, nil))) + apiKey := "test" + srv := handler.NewServer(mem, apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil))) for _, tc := range []struct { name string @@ -63,6 +98,7 @@ func TestSyncGet(t *testing.T) { if err != nil { t.Errorf("exp nil, got %v", err) } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey)) res := httptest.NewRecorder() srv.ServeHTTP(res, req) @@ -98,6 +134,7 @@ func TestSyncGet(t *testing.T) { func TestSyncPost(t *testing.T) { t.Parallel() + apiKey := "test" for _, tc := range []struct { name string reqBody []byte @@ -128,11 +165,12 @@ func TestSyncPost(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { mem := storage.NewMemory() - srv := handler.NewServer(mem, slog.New(slog.NewJSONHandler(os.Stdout, nil))) + srv := handler.NewServer(mem, apiKey, slog.New(slog.NewJSONHandler(os.Stdout, nil))) req, err := http.NewRequest(http.MethodPost, "/sync", bytes.NewBuffer(tc.reqBody)) if err != nil { t.Errorf("exp nil, got %v", err) } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey)) res := httptest.NewRecorder() srv.ServeHTTP(res, req) diff --git a/main.go b/main.go index 731699d..eed3eae 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,12 @@ package main import ( + "fmt" "log/slog" "net/http" "os" "os/signal" + "strconv" "syscall" "code.ewintr.nl/planner/handler" @@ -12,10 +14,21 @@ import ( ) func main() { + port, err := strconv.Atoi(os.Getenv("PLANNER_PORT")) + if err != nil { + fmt.Println("PLANNER_PORT env is not an integer") + os.Exit(1) + } + apiKey := os.Getenv("PLANNER_API_KEY") + if apiKey == "" { + fmt.Println("PLANNER_API_KEY is empty") + os.Exit(1) + } + mem := storage.NewMemory() logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - go http.ListenAndServe(":8092", handler.NewServer(mem, logger)) + go http.ListenAndServe(fmt.Sprintf(":%d", port), handler.NewServer(mem, apiKey, logger)) logger.Info("service started")