diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml index 6544651..236e38b 100644 --- a/.github/workflows/qa.yaml +++ b/.github/workflows/qa.yaml @@ -20,6 +20,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 + - name: Start Postgresql + run: make db - name: Run tests run: go test -v ./... swagger: diff --git a/Makefile b/Makefile index a43b797..e7ecf35 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,6 @@ docs: swagger: docker run --detach --name eryth-swagger -p 4000:8080 -e API_URL=/doc/swagger.yaml --mount 'type=bind,src=$(shell pwd)/docs,dst=/usr/share/nginx/html/doc' swaggerapi/swagger-ui + +db: + docker compose up --wait db \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f03e43f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +name: eryth +services: + db: + image: postgres:15-alpine + ports: + - '54321:5432' + environment: + - POSTGRES_USER=go + - POSTGRES_PASSWORD=go + - POSTGRES_DB=eryth + volumes: + - ./db/migrations/:/docker-entrypoint-initdb.d + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U go" ] + interval: 1s + timeout: 1s + retries: 10 \ No newline at end of file diff --git a/internal/handler/endpoint.go b/internal/handler/endpoint.go index ec69491..93dc753 100644 --- a/internal/handler/endpoint.go +++ b/internal/handler/endpoint.go @@ -112,7 +112,6 @@ func (e *Endpoint) list(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("Failed to marshall endpoint list", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusOK) } // @Summary Create a new endpoint. @@ -149,12 +148,13 @@ func (e *Endpoint) create(w http.ResponseWriter, r *http.Request) { return } + w.WriteHeader(http.StatusCreated) w.Header().Set("Content-Type", "application/json") _, err = w.Write(content) if err != nil { slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusCreated) + } // @Summary Delete an endpoint and its associated resources. @@ -225,5 +225,4 @@ func (e *Endpoint) update(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) } - w.WriteHeader(http.StatusOK) } diff --git a/internal/handler/endpoint_test.go b/internal/handler/endpoint_test.go index abeebd1..b4f11f8 100644 --- a/internal/handler/endpoint_test.go +++ b/internal/handler/endpoint_test.go @@ -1 +1,219 @@ package handler + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/crazybolillo/eryth/internal/model" + "github.com/crazybolillo/eryth/internal/service" + "github.com/jackc/pgx/v5" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestEndpointAPI(t *testing.T) { + cases := []struct { + name string + test func(handler http.Handler) func(*testing.T) + }{ + {"Create", MustCreate}, + {"Delete", MustDelete}, + {"Read", MustRead}, + {"Update", MustUpdate}, + } + + conn, err := pgx.Connect(context.Background(), "postgres://go:go@127.0.0.1:54321/eryth") + if err != nil { + t.Fatalf( + "Connection to test database failed: %s. Try running 'make db' and run the tests again", + err, + ) + } + defer func(conn *pgx.Conn, ctx context.Context) { + err := conn.Close(ctx) + if err != nil { + t.Error("Failed to close db connection") + } + }(conn, context.Background()) + + for _, tt := range cases { + tx, err := conn.Begin(context.Background()) + if err != nil { + t.Fatalf("Transaction start failed: %s", err) + } + + handler := Endpoint{Service: &service.EndpointService{Cursor: tx}} + t.Run(tt.name, tt.test(handler.Router())) + + err = tx.Rollback(context.Background()) + if err != nil { + t.Fatalf("Failed to rollback transaction: %s", err) + } + } +} + +func createEndpoint(t *testing.T, handler http.Handler, endpoint model.NewEndpoint) *httptest.ResponseRecorder { + payload, err := json.Marshal(endpoint) + if err != nil { + t.Errorf("failed to marshal new endpoint: %s", err) + } + + req := httptest.NewRequest("POST", "/", bytes.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func readEndpoint(handler http.Handler, sid int32) *httptest.ResponseRecorder { + req := httptest.NewRequest("GET", fmt.Sprintf("/%d", sid), nil) + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func updateEndpoint(t *testing.T, handler http.Handler, sid int32, endpoint model.PatchedEndpoint) *httptest.ResponseRecorder { + payload, err := json.Marshal(endpoint) + if err != nil { + t.Errorf("failed to marshal new endpoint: %s", err) + } + req := httptest.NewRequest("PATCH", fmt.Sprintf("/%d", sid), bytes.NewReader(payload)) + res := httptest.NewRecorder() + handler.ServeHTTP(res, req) + + return res +} + +func parseEndpoint(t *testing.T, content *bytes.Buffer) model.Endpoint { + var createdEndpoint model.Endpoint + decoder := json.NewDecoder(content) + err := decoder.Decode(&createdEndpoint) + if err != nil { + t.Errorf("failed to parse endpoint: %s", err) + } + + return createdEndpoint +} + +func MustCreate(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "zinniaelegans", + Password: "verylongandsafepassword", + Context: "flowers", + Codecs: []string{"ulaw", "g722"}, + Extension: "1234", + DisplayName: "Zinnia Elegans", + MaxContacts: 10, + } + res := createEndpoint(t, handler, endpoint) + if res.Code != http.StatusCreated { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusCreated) + } + got := parseEndpoint(t, res.Body) + + want := model.Endpoint{ + Sid: got.Sid, + ID: endpoint.ID, + DisplayName: endpoint.DisplayName, + Transport: endpoint.Transport, + Context: endpoint.Context, + Codecs: endpoint.Codecs, + MaxContacts: 10, + Extension: "1234", + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Created endpoint does not match request, got %v, want %v", got, want) + } + } +} + +func MustRead(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "kiwi", + Password: "kiwipassword123", + Context: "fruits", + Codecs: nil, + Extension: "9000", + DisplayName: "Blue Kiwi", + } + res := createEndpoint(t, handler, endpoint) + want := parseEndpoint(t, res.Body) + + res = readEndpoint(handler, want.Sid) + if res.Code != http.StatusOK { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusOK) + } + got := parseEndpoint(t, res.Body) + + if !reflect.DeepEqual(want, got) { + t.Errorf("read endpoint does not match want, got %v, want %v", got, want) + } + } +} + +func MustDelete(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "testuser", + Password: "testpassword123$", + Context: "internal", + Codecs: nil, + Extension: "4000", + DisplayName: "Mr. Test User", + } + + res := createEndpoint(t, handler, endpoint) + createdEndpoint := parseEndpoint(t, res.Body) + + req := httptest.NewRequest("DELETE", fmt.Sprintf("/%d", createdEndpoint.Sid), nil) + res = httptest.NewRecorder() + handler.ServeHTTP(res, req) + + if res.Code != http.StatusNoContent { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusNoContent) + } + + res = readEndpoint(handler, createdEndpoint.Sid) + if res.Code != http.StatusNotFound { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusNotFound) + } + } +} + +func MustUpdate(handler http.Handler) func(*testing.T) { + return func(t *testing.T) { + endpoint := model.NewEndpoint{ + ID: "big_chungus", + Password: "big_chungus_password", + Context: "memes", + Codecs: []string{"ulaw", "opus"}, + Extension: "5061", + DisplayName: "Big Chungus", + } + res := createEndpoint(t, handler, endpoint) + want := parseEndpoint(t, res.Body) + want.MaxContacts = 5 + want.Extension = "6072" + + res = updateEndpoint(t, handler, want.Sid, model.PatchedEndpoint{ + MaxContacts: &want.MaxContacts, + Extension: &want.Extension, + }) + if res.Code != http.StatusOK { + t.Errorf("invalid http code, got %d, want %d", res.Code, http.StatusOK) + } + got := parseEndpoint(t, res.Body) + + if !reflect.DeepEqual(got, want) { + t.Errorf("inconsistent update result, got %v, want %v", got, want) + } + } +}