diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 36b541a..a370c40 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -11,15 +11,23 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.19 - - - name: Build - run: go build -v ./... + - name: Setup gotestsum + uses: autero1/action-gotestsum@v2 + with: + gotestsum_version: 0.4.1 - name: Test - run: go test -v ./... \ No newline at end of file + run: make test-kong + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: | + test-results/**/*.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index eaa3f05..102c8a2 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,5 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/go,goland +*.env +test-results/ diff --git a/Dockerfile b/Dockerfile index b82e2d5..c3b573d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ ARG TC_KONG_IMAGE -FROM ${TC_KONG_IMAGE:-kong:2.8.1} +FROM ${TC_KONG_IMAGE:-kong/kong:3.4.0} RUN mkdir -p /usr/local/kong/go-plugins/bin diff --git a/LICENSE b/LICENSE index 0375514..7771f56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Viktor Gamov +Copyright (c) 2021-2023 Viktor Gamov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8fe12eb --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +GOFMT=gofmt + +.PHONY: test-% +test-%: + @echo "Running $* tests..." + gotestsum \ + --format standard-verbose \ + --rerun-fails=1 \ + --packages="./..." \ + --junitfile test-results/TEST-$*.xml + +fmt: + $(GOFMT) -w . diff --git a/fixtures/kong-mockbin.yaml b/fixtures/kong-mockbin.yaml new file mode 100644 index 0000000..d706195 --- /dev/null +++ b/fixtures/kong-mockbin.yaml @@ -0,0 +1,9 @@ +_format_version: "3.0" +services: + - name: mock + host: mockbin.org + port: 80 + protocol: http + routes: + - paths: + - /mock diff --git a/kong.yaml b/fixtures/kong-plugin.yaml similarity index 100% rename from kong.yaml rename to fixtures/kong-plugin.yaml diff --git a/jsonresponse.go b/jsonresponse.go new file mode 100644 index 0000000..25f3191 --- /dev/null +++ b/jsonresponse.go @@ -0,0 +1,43 @@ +package kong + +import "time" + +// JSONResponse representation of a response from mockbin service +type JSONResponse struct { + StartedDateTime time.Time `json:"startedDateTime"` + ClientIPAddress string `json:"clientIPAddress"` + Method string `json:"method"` + URL string `json:"url"` + HTTPVersion string `json:"httpVersion"` + Cookies struct { + } `json:"cookies"` + Headers struct { + Host string `json:"host"` + Connection string `json:"connection"` + AcceptEncoding string `json:"accept-encoding"` + XForwardedFor string `json:"x-forwarded-for"` + CfRay string `json:"cf-ray"` + XForwardedProto string `json:"x-forwarded-proto"` + CfVisitor string `json:"cf-visitor"` + XForwardedHost string `json:"x-forwarded-host"` + XForwardedPort string `json:"x-forwarded-port"` + XForwardedPath string `json:"x-forwarded-path"` + UserAgent string `json:"user-agent"` + CfConnectingIP string `json:"cf-connecting-ip"` + CdnLoop string `json:"cdn-loop"` + XRequestID string `json:"x-request-id"` + Via string `json:"via"` + ConnectTime string `json:"connect-time"` + XRequestStart string `json:"x-request-start"` + TotalRouteTime string `json:"total-route-time"` + } `json:"headers"` + QueryString struct { + } `json:"queryString"` + PostData struct { + MimeType string `json:"mimeType"` + Text string `json:"text"` + Params []interface{} `json:"params"` + } `json:"postData"` + HeadersSize int `json:"headersSize"` + BodySize int `json:"bodySize"` +} diff --git a/kong.go b/kong.go index 856b14b..2f9b152 100644 --- a/kong.go +++ b/kong.go @@ -4,16 +4,13 @@ import ( "context" "fmt" "github.com/docker/go-connections/nat" - "log" - "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" + "log" ) type kongContainer struct { testcontainers.Container - URI string - ProxyURI string } // TODO: mention all ports @@ -22,29 +19,29 @@ var ( defaultAdminAPIPort = "8001/tcp" ) -func SetupKong(ctx context.Context, - image string, - environment map[string]string, - files []testcontainers.ContainerFile, - opts ...testcontainers.ContainerCustomizer) (*kongContainer, error) { +// RunContainer is the entrypoint to the module +func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*kongContainer, error) { req := testcontainers.ContainerRequest{ - // needed because the official Docker image does not have the go-plugins/bin directory already created - FromDockerfile: testcontainers.FromDockerfile{ - Context: ".", - BuildArgs: map[string]*string{ - "TC_KONG_IMAGE": &image, - }, - PrintBuildLog: true, - }, + //Image: "kong/kong-gateway:3.3.0", ExposedPorts: []string{ defaultProxyPort, defaultAdminAPIPort}, WaitingFor: wait.ForListeningPort(nat.Port(defaultAdminAPIPort)), Cmd: []string{"kong", "start"}, - Env: environment, - Files: files, + Env: map[string]string{ + // default env variables, can be overwritten in test method + "KONG_DATABASE": "off", + "KONG_LOG_LEVEL": "debug", + "KONG_PROXY_ACCESS_LOG": "/dev/stdout", + "KONG_ADMIN_ACCESS_LOG": "/dev/stdout", + "KONG_PROXY_ERROR_LOG": "/dev/stderr", + "KONG_ADMIN_ERROR_LOG": "/dev/stderr", + "KONG_ADMIN_LISTEN": "0.0.0.0:8001", + "KONG_DECLARATIVE_CONFIG": "/usr/local/kong/kong.yaml", + }, } + genericContainerRequest := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, @@ -55,29 +52,41 @@ func SetupKong(ctx context.Context, } container, err := testcontainers.GenericContainer(ctx, genericContainerRequest) - if err != nil { log.Fatal(err) } - ip, err := container.Host(ctx) + return &kongContainer{Container: container}, nil +} + +// KongUrls returns admin url, proxy url, or error +func (c kongContainer) KongUrls(ctx context.Context, args ...string) (string, string, error) { + ip, err := c.Host(ctx) if err != nil { - return nil, err + return "", "", err } - - mappedPort, err := container.MappedPort(ctx, nat.Port(defaultAdminAPIPort)) + mappedPort, err := c.MappedPort(ctx, nat.Port(defaultAdminAPIPort)) if err != nil { - return nil, err + return "", "", err } - uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) - proxyMappedPort, err := container.MappedPort(ctx, nat.Port(defaultProxyPort)) + proxyMappedPort, err := c.MappedPort(ctx, nat.Port(defaultProxyPort)) if err != nil { - return nil, err + return "", "", err } pUri := fmt.Sprintf("http://%s:%s", ip, proxyMappedPort.Port()) - return &kongContainer{Container: container, URI: uri, ProxyURI: pUri}, nil + return uri, pUri, nil +} + +// KongCustomizer type represents a container customizer for transferring state from the options to the container +type KongCustomizer struct { +} + +// Customize method implementation +func (c KongCustomizer) Customize(req *testcontainers.GenericContainerRequest) testcontainers.ContainerRequest { + // req.ExposedPorts = append(req.ExposedPorts, "1234/tcp") + return req.ContainerRequest } diff --git a/kong_test.go b/kong_test.go index e4a2327..3eea2df 100644 --- a/kong_test.go +++ b/kong_test.go @@ -3,16 +3,14 @@ package kong import ( "context" "fmt" + "github.com/gavv/httpexpect/v2" "github.com/go-http-utils/headers" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "net/http" "strings" "testing" - "time" - - "github.com/gavv/httpexpect/v2" - "github.com/stretchr/testify/assert" - "github.com/testcontainers/testcontainers-go" ) type TestLogConsumer struct { @@ -41,15 +39,74 @@ func TestKongAdminAPI_ReturnVersion(t *testing.T) { ctx := context.Background() + tests := []struct { + name string + image string + version string + //wait wait.Strategy + }{ + { + name: "Kong OSS", + image: "kong/kong:3.4.0", + version: "kong/3.4.0", + }, + { + name: "Kong Gateway", + image: "kong/kong-gateway:3.4.0.0", + version: "kong/3.4.0.0-enterprise-edition", + }, + } + files := []testcontainers.ContainerFile{ + { + HostFilePath: "./fixtures/kong-mockbin.yaml", + ContainerFilePath: "/usr/local/kong/kong.yaml", + FileMode: 0644, // see https://github.com/supabase/cli/pull/132/files + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kong, err := RunContainer(ctx, testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: tt.image, + Files: files, + }})) + + require.NoError(t, err) + // Clean up the container after the test is complete + t.Cleanup(func() { + if err := kong.Terminate(ctx); err != nil { + t.Fatalf("failed to terminate container: %s", err) + } + }) + + adminUrl, proxy, err := kong.KongUrls(ctx) + + e := httpexpect.Default(t, adminUrl) + + e.GET("/"). + Expect(). + Status(http.StatusOK). + Header("Server").IsEqual(tt.version) + + e = httpexpect.Default(t, proxy) + e.GET("/mock/requests"). + Expect().Status(http.StatusOK). + JSON().Object().ContainsKey("url").HasValue("url", "http://localhost/requests") + }) + } +} + +func TestKongGoPlugin_ModifiesHeaders(t *testing.T) { + + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + env := map[string]string{ - "KONG_DATABASE": "off", - "KONG_LOG_LEVEL": "debug", - "KONG_PROXY_ACCESS_LOG": "/dev/stdout", - "KONG_ADMIN_ACCESS_LOG": "/dev/stdout", - "KONG_PROXY_ERROR_LOG": "/dev/stderr", - "KONG_ADMIN_ERROR_LOG": "/dev/stderr", - "KONG_ADMIN_LISTEN": "0.0.0.0:8001", - "KONG_DECLARATIVE_CONFIG": "/usr/local/kong/kong.yaml", + "KONG_LOG_LEVEL": "info", //------------ Kong Plugins ----------------- "KONG_PLUGINS": "goplug", "KONG_PLUGINSERVER_NAMES": "goplug", @@ -59,7 +116,7 @@ func TestKongAdminAPI_ReturnVersion(t *testing.T) { files := []testcontainers.ContainerFile{ { - HostFilePath: "./kong.yaml", + HostFilePath: "./fixtures/kong-plugin.yaml", ContainerFilePath: "/usr/local/kong/kong.yaml", FileMode: 0644, // see https://github.com/supabase/cli/pull/132/files }, @@ -69,13 +126,24 @@ func TestKongAdminAPI_ReturnVersion(t *testing.T) { FileMode: 0755, }, } - kong, err := SetupKong(ctx, - "kong/kong-gateway-dev:3.4.0.0-rc.1", - env, - files) + + image := "kong/kong-gateway-dev:3.4.0.0-rc.1" + kong, err := RunContainer(ctx, testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + // needed because the official Docker image does not have the go-plugins/bin directory already created + FromDockerfile: testcontainers.FromDockerfile{ + Context: ".", + BuildArgs: map[string]*string{ + "TC_KONG_IMAGE": &image, + }, + PrintBuildLog: true, + }, + Env: env, + Files: files, + }, + })) require.NoError(t, err) - // doesn't work 🤷‍♂️ consumer := TestLogConsumer{ Msgs: []string{}, Ack: make(chan bool), @@ -93,27 +161,10 @@ func TestKongAdminAPI_ReturnVersion(t *testing.T) { } }) - e := httpexpect.Default(t, kong.URI) - - //resp, err := http.Get(kong.URI) - //assert.Nil(t, err) - // - //// go get github.com/stretchr/testify - //assert.Equal(t, resp.StatusCode, http.StatusOK) - //assert.Equal(t, resp.Header.Get("Server"), "kong/2.8.1") - // this code is replaced with httpexpect - e.GET("/"). - Expect(). - Status(http.StatusOK). - Header("Server").IsEqual("kong/3.4.0.0-enterprise-edition") - - e = httpexpect.Default(t, kong.ProxyURI) - - //get, err := http.Get(kong.ProxyURI) - //assert.Nil(t, err) - // - //all, err := io.ReadAll(get.Body) - //assert.Nil(t, err) + _, proxyUrl, err := kong.KongUrls(ctx) + + e := httpexpect.Default(t, proxyUrl) + r := e.GET("/"). WithHeader(headers.UserAgent, "Kong Builders"). Expect() @@ -129,42 +180,3 @@ func TestKongAdminAPI_ReturnVersion(t *testing.T) { value := res.Headers.Host assert.True(t, strings.Contains(value, "mockbin")) } - -type JSONResponse struct { - StartedDateTime time.Time `json:"startedDateTime"` - ClientIPAddress string `json:"clientIPAddress"` - Method string `json:"method"` - URL string `json:"url"` - HTTPVersion string `json:"httpVersion"` - Cookies struct { - } `json:"cookies"` - Headers struct { - Host string `json:"host"` - Connection string `json:"connection"` - AcceptEncoding string `json:"accept-encoding"` - XForwardedFor string `json:"x-forwarded-for"` - CfRay string `json:"cf-ray"` - XForwardedProto string `json:"x-forwarded-proto"` - CfVisitor string `json:"cf-visitor"` - XForwardedHost string `json:"x-forwarded-host"` - XForwardedPort string `json:"x-forwarded-port"` - XForwardedPath string `json:"x-forwarded-path"` - UserAgent string `json:"user-agent"` - CfConnectingIP string `json:"cf-connecting-ip"` - CdnLoop string `json:"cdn-loop"` - XRequestID string `json:"x-request-id"` - Via string `json:"via"` - ConnectTime string `json:"connect-time"` - XRequestStart string `json:"x-request-start"` - TotalRouteTime string `json:"total-route-time"` - } `json:"headers"` - QueryString struct { - } `json:"queryString"` - PostData struct { - MimeType string `json:"mimeType"` - Text string `json:"text"` - Params []interface{} `json:"params"` - } `json:"postData"` - HeadersSize int `json:"headersSize"` - BodySize int `json:"bodySize"` -}