diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml index 4f29bda..f53ce34 100644 --- a/.github/workflows/bump-version.yaml +++ b/.github/workflows/bump-version.yaml @@ -14,6 +14,7 @@ jobs: permissions: issues: write pull-requests: write + contents: write steps: - uses: mheap/github-action-required-labels@v5 with: @@ -33,19 +34,23 @@ jobs: repo: speakeasy cli_name: speakeasy package_type: zip + - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} token: ${{ secrets.PAT }} + - name: Reset .speakeasy/gen.yaml to the version on main run: | git fetch origin main git checkout origin/main .speakeasy/gen.yaml .speakeasy/gen.lock + - name: Set bump (patch) run: | BUMP_TYPE=$(echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | jq -r '.[]' | grep -E 'patch|minor|major') speakeasy bump $BUMP_TYPE + - name: Commit SDK changes to the PR uses: EndBug/add-and-commit@v9 with: diff --git a/.github/workflows/generate_on_pr.yaml b/.github/workflows/generate_on_pr.yaml index bad6987..b371ec8 100644 --- a/.github/workflows/generate_on_pr.yaml +++ b/.github/workflows/generate_on_pr.yaml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.PAT }} - name: Configure speakeasy CLI run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..481602a --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,53 @@ +name: tests +run-name: tests, branch:${{ github.ref_name }}, triggered by @${{ github.actor }} + +concurrency: + # Run only for most recent commit in PRs but for all tags and commits on main + # Ref: https://docs.github.com/en/actions/using-jobs/using-concurrency + group: ${{ github.workflow }}-${{ github.head_ref || github.sha }} + cancel-in-progress: true + +on: + pull_request: + branches: + - '*' + push: + branches: + - 'main' + schedule: + - cron: '30 2 * * *' + workflow_dispatch: {} + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: run unit tests + run: make test.unit + + integration-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - konnect-api-url: https://us.api.konghq.tech + - konnect-api-url: https://eu.api.konghq.tech + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: run integration tests + run: make test.integration + env: + KONNECT_API_URL: ${{ matrix.konnect-api-url }} + KONNECT_API_PAT: ${{ secrets.KONNECT_API_PAT }} diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 9d81442..8fa0711 100755 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -13,7 +13,7 @@ generation: oAuth2ClientCredentialsEnabled: false oAuth2PasswordEnabled: false go: - version: 0.1.7 + version: 0.1.8 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/Makefile b/Makefile index 04e026a..22f736d 100644 --- a/Makefile +++ b/Makefile @@ -107,3 +107,15 @@ generate.sdk: speakeasy generate sdk --lang go --out . --schema ./$(OPENAPI_FILE) $(MAKE) _generate.omitempty go mod tidy + +.PHONY: test +test: test.unit test.integration + +.PHONY: test.unit +test.unit: + +.PHONY: test.integration +test.integration: + KONNECT_TEST_RUN_ID=$(shell openssl rand -hex 8) \ + go test -v -race $(GOTESTFLAGS) \ + ./test/integration/... diff --git a/go.mod b/go.mod index aa715cd..8442e3b 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,11 @@ go 1.20 require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f955779..7f63c8a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,14 @@ github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/hooks/globalurl.go b/internal/hooks/globalurl.go new file mode 100644 index 0000000..b5b5fb4 --- /dev/null +++ b/internal/hooks/globalurl.go @@ -0,0 +1,174 @@ +package hooks + +import ( + "net/http" + "strings" +) + +type GlobalAPIURLRequestHook struct{} + +var _ beforeRequestHook = (*GlobalAPIURLRequestHook)(nil) + +func (i *GlobalAPIURLRequestHook) BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) { + // NOTE: the list below was generated with + // for op in $(rg --no-line-number -o "operationId: (.*)" -r '$1' ../platform-api/src/konnect/definitions/identity/v3/openapi.yaml); do printf "case \"$op\":\n\tfallthrough\n"; done + switch hookCtx.OperationID { + case "post-auth0-register-internal": + fallthrough + case "get-auth0-organizations-internal": + fallthrough + case "get-users-internal": + fallthrough + case "post-oauth-device-authorize": + fallthrough + case "post-oauth-device-token": + fallthrough + case "post-oauth-device-authorize-user": + fallthrough + case "patch-oauth-device-confirm": + fallthrough + case "get-impersonation-settings": + fallthrough + case "update-impersonation-settings": + fallthrough + case "get-authentication-settings": + fallthrough + case "update-authentication-settings": + fallthrough + case "invite-user": + fallthrough + case "get-identity-providers": + fallthrough + case "create-identity-provider": + fallthrough + case "get-identity-provider": + fallthrough + case "update-identity-provider": + fallthrough + case "delete-identity-provider": + fallthrough + case "get-idp-configuration": + fallthrough + case "update-idp-configuration": + fallthrough + case "update-idp-team-mappings": + fallthrough + case "get-idp-team-mappings": + fallthrough + case "get-team-group-mappings": + fallthrough + case "patch-team-group-mappings": + fallthrough + case "get-predefined-roles": + fallthrough + case "list-teams": + fallthrough + case "create-team": + fallthrough + case "list-team-users": + fallthrough + case "add-user-to-team": + fallthrough + case "get-team": + fallthrough + case "update-team": + fallthrough + case "delete-team": + fallthrough + case "remove-user-from-team": + fallthrough + case "list-team-roles": + fallthrough + case "teams-assign-role": + fallthrough + case "teams-remove-role": + fallthrough + case "list-users": + fallthrough + case "get-user": + fallthrough + case "update-user": + fallthrough + case "delete-user": + fallthrough + case "list-user-teams": + fallthrough + case "list-user-roles": + fallthrough + case "users-assign-role": + fallthrough + case "users-remove-role": + fallthrough + case "get-system-accounts": + fallthrough + case "post-system-accounts": + fallthrough + case "get-system-accounts-id": + fallthrough + case "patch-system-accounts-id": + fallthrough + case "delete-system-accounts-id": + fallthrough + case "get-system-account-id-access-tokens": + fallthrough + case "post-system-accounts-id-access-tokens": + fallthrough + case "get-system-accounts-id-access-tokens-id": + fallthrough + case "patch-system-accounts-id-access-tokens-id": + fallthrough + case "delete-system-accounts-id-access-tokens-id": + fallthrough + case "get-system-accounts-assigned-roles-internal": + fallthrough + case "create-system-accounts-assigned-roles-internal": + fallthrough + case "get-system-accounts-accountId-assigned-roles": + fallthrough + case "post-system-accounts-accountId-assigned-roles": + fallthrough + case "delete-system-accounts-accountId-assigned-roles-roleId": + fallthrough + case "get-teams-teamId-system-accounts": + fallthrough + case "post-teams-teamId-system-accounts": + fallthrough + case "delete-teams-teamId-system-accounts-accountId": + fallthrough + case "get-system-accounts-accountId-teams": + fallthrough + case "get-users-me": + fallthrough + case "delete-users-me": + fallthrough + case "patch-users-me": + fallthrough + case "get-users-me-permissions": + fallthrough + case "get-organizations-me": + fallthrough + case "update-organizations-me": + fallthrough + case "refresh-token": + fallthrough + case "logout": + fallthrough + case "resolveCustomer": + fallthrough + case "authenticate-sso": + // NOTE(pmalek): This is because we merge OpenAPI specs and /organizations/me + // is only served by the global API. + // @mheap mentioned that we can add operation specific URLs to do away with this. + if strings.HasSuffix(req.URL.Host, "api.konghq.tech") { + req.URL.Host = "global.api.konghq.tech" + req.Host = "global.api.konghq.tech" + } else { + req.URL.Host = "global.api.konghq.com" + req.Host = "global.api.konghq.com" + } + + default: + + } + return req, nil +} diff --git a/internal/hooks/registration.go b/internal/hooks/registration.go index 38b7b5f..c75f176 100644 --- a/internal/hooks/registration.go +++ b/internal/hooks/registration.go @@ -11,6 +11,8 @@ import "os" func initHooks(h *Hooks) { h.registerBeforeRequestHook(&UserAgentPreRequestHook{}) + h.registerBeforeRequestHook(&GlobalAPIURLRequestHook{}) + h.registerBeforeRequestHook(&APIURLRequestHook{ CustomDomain: os.Getenv("KONG_CUSTOM_DOMAIN"), }) diff --git a/test/integration/controlplane_test.go b/test/integration/controlplane_test.go new file mode 100644 index 0000000..4e9ede5 --- /dev/null +++ b/test/integration/controlplane_test.go @@ -0,0 +1,88 @@ +package integration + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +func TestControlPlaneCreateGetDelete(t *testing.T) { + t.Parallel() + + sdk := SDK(t) + runID := KonnectTestRunID(t) + + ctx := context.Background() + req := sdkkonnectcomp.CreateControlPlaneRequest{ + Name: NamePrefix(t) + "-" + runID, + Labels: Labels(t), + } + respCreate, err := sdk.ControlPlanes.CreateControlPlane(ctx, req) + require.NoError(t, err) + t.Cleanup(func() { + _, err := sdk.ControlPlanes.DeleteControlPlane(ctx, respCreate.ControlPlane.ID) + require.NoError(t, err) + }) + + require.NotNil(t, respCreate) + require.NotEmpty(t, respCreate.ControlPlane.ID) + require.NotEmpty(t, respCreate.ControlPlane.Name) + require.NotNil(t, respCreate.ControlPlane.Labels) + require.EqualValues(t, Labels(t), respCreate.ControlPlane.Labels) + + respGet, err := sdk.ControlPlanes.GetControlPlane(ctx, respCreate.ControlPlane.ID) + require.NoError(t, err) + require.NotNil(t, respGet) + require.NotNil(t, respGet.ControlPlane) + require.Equal(t, respCreate.ControlPlane.Name, respGet.ControlPlane.Name) +} + +func TestControlPlaneList(t *testing.T) { + t.Parallel() + + sdk := SDK(t) + + ctx := context.Background() + reqList := sdkkonnectops.ListControlPlanesRequest{ + // TODO listing doesn't work with criteria yet. + } + respList, err := sdk.ControlPlanes.ListControlPlanes(ctx, reqList) + require.NoError(t, err) + require.NotNil(t, respList.ListControlPlanesResponse) + + // TODO listing doesn't work with criteria yet. + + // require.Empty(t, respList.ListControlPlanesResponse.Data) + + // req := sdkkonnectcomp.CreateControlPlaneRequest{ + // Name: cpName, + // Labels: Labels(t), + // } + // resp, err := sdk.ControlPlanes.CreateControlPlane(ctx, req) + // require.NoError(t, err) + // t.Cleanup(func() { + // _, err := sdk.ControlPlanes.DeleteControlPlane(ctx, resp.ControlPlane.ID) + // require.NoError(t, err) + // }) + + // require.NotNil(t, resp) + + // reqList = sdkkonnectops.ListControlPlanesRequest{ + // Filter: &sdkkonnectcomp.ControlPlaneFilterParameters{ + // ID: &sdkkonnectcomp.ID{ + // StringFieldOEQFilter: &sdkkonnectcomp.StringFieldOEQFilter{ + // Oeq: resp.ControlPlane.GetID(), + // }, + // }, + // }, + // } + // respList, err = sdk.ControlPlanes.ListControlPlanes(ctx, reqList) + // require.NoError(t, err) + // require.NotEmpty(t, respList.ListControlPlanesResponse.Data) + // require.Len(t, respList.ListControlPlanesResponse.Data, 1) + // require.Equal(t, respList.ListControlPlanesResponse.Data[0].ID, resp.ControlPlane.ID) +} diff --git a/test/integration/envs.go b/test/integration/envs.go new file mode 100644 index 0000000..a5d7ffa --- /dev/null +++ b/test/integration/envs.go @@ -0,0 +1,38 @@ +package integration + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + // KonnectPersonalAccessTokenEnv is the environment variable name for the Konnect PAT. + KonnectPersonalAccessTokenEnv = "KONNECT_API_PAT" + // KonnectURLEnv is the environment variable name for the Konnect URL. + KonnectURLEnv = "KONNECT_API_URL" + // KonnectTestRunIDEnv is the environment variable name for the Konnect test run ID. + KonnectTestRunIDEnv = "KONNECT_TEST_RUN_ID" +) + +// KonnectPersonalAccessToken returns the Konnect PAT from the environment. +func KonnectPersonalAccessToken(t *testing.T) string { + token := os.Getenv(KonnectPersonalAccessTokenEnv) + require.NotEmptyf(t, token, "%s is not set", KonnectPersonalAccessTokenEnv) + return token +} + +// KonnectURL returns the Konnect url from the environment. +func KonnectURL(t *testing.T) string { + url := os.Getenv(KonnectURLEnv) + require.NotEmptyf(t, url, "%s is not set", KonnectURLEnv) + return url +} + +// KonnectTestRunID returns the Konnect test run ID from the environment. +func KonnectTestRunID(t *testing.T) string { + id := os.Getenv(KonnectTestRunIDEnv) + require.NotEmptyf(t, id, "%s is not set", KonnectTestRunIDEnv) + return id +} diff --git a/test/integration/funcs.go b/test/integration/funcs.go new file mode 100644 index 0000000..ef5a18d --- /dev/null +++ b/test/integration/funcs.go @@ -0,0 +1,16 @@ +package integration + +import "testing" + +// NamePrefix returns a prefix for the test name. +func NamePrefix(t *testing.T) string { + return "sdk-konnect-go-test-integration-" + t.Name() +} + +func Labels(t *testing.T) map[string]string { + return map[string]string{ + "sdk-konnect-go": "true", + "test_name": NamePrefix(t), + "test_run_id": KonnectTestRunID(t), + } +} diff --git a/test/integration/me_test.go b/test/integration/me_test.go new file mode 100644 index 0000000..a7e48bc --- /dev/null +++ b/test/integration/me_test.go @@ -0,0 +1,51 @@ +package integration + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +func TestMeOrganizations(t *testing.T) { + t.Parallel() + + sdk := SDK(t) + url := KonnectURL(t) + + ctx := context.Background() + respOrg, err := sdk.Me.GetOrganizationsMe(ctx, + // NOTE: This is needed because currently the SDK only lists the prod global API as supported: + // https://github.com/Kong/sdk-konnect-go/blob/999d9a987e1aa7d2e09ac11b1450f4563adf21ea/models/operations/getorganizationsme.go#L10-L12 + sdkkonnectops.WithServerURL(url), + ) + require.NoError(t, err) + require.NotNil(t, respOrg) + require.NotEmpty(t, respOrg.MeOrganization.ID) + require.NotEmpty(t, respOrg.MeOrganization.Name) + require.NotNil(t, respOrg.MeOrganization.State) + require.EqualValues(t, "active", *respOrg.MeOrganization.State) +} + +func TestMeUsers(t *testing.T) { + t.Parallel() + + sdk := SDK(t) + url := KonnectURL(t) + + ctx := context.Background() + respOrg, err := sdk.Me.GetUsersMe(ctx, + // NOTE: This is needed because currently the SDK only lists the prod global API as supported: + // https://github.com/Kong/sdk-konnect-go/blob/999d9a987e1aa7d2e09ac11b1450f4563adf21ea/models/operations/getorganizationsme.go#L10-L12 + sdkkonnectops.WithServerURL(url), + ) + require.NoError(t, err) + require.NotNil(t, respOrg) + require.NotEmpty(t, respOrg.User.ID) + require.NotNil(t, respOrg.User.FullName) + require.NotEmpty(t, respOrg.User.FullName) + require.NotNil(t, respOrg.User.Active) + require.True(t, *respOrg.User.Active) +} diff --git a/test/integration/sdk.go b/test/integration/sdk.go new file mode 100644 index 0000000..3d2584d --- /dev/null +++ b/test/integration/sdk.go @@ -0,0 +1,27 @@ +package integration + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdkkonnectgo "github.com/Kong/sdk-konnect-go" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" +) + +// SDK returns a new SDK instance. It requires the KONNECT_API_PAT and KONNECT_API_URL +// environment variables to be set. +func SDK(t *testing.T) *sdkkonnectgo.SDK { + pat := KonnectPersonalAccessToken(t) + url := KonnectURL(t) + sdk := sdkkonnectgo.New( + sdkkonnectgo.WithSecurity( + sdkkonnectcomp.Security{ + PersonalAccessToken: sdkkonnectgo.String(pat), + }, + ), + sdkkonnectgo.WithServerURL(url), + ) + require.NotNil(t, sdk) + return sdk +}