diff --git a/README.md b/README.md index 853288be..d1175d3d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## This was forked from the original [aoldershaw/github-pr-resource](https://github.com/aoldershaw/github-pr-resource) -to add an additional security check when looking at PR approvals. +to add an additional security check when looking at PR approvals. You can find it on Dockerhub as the `tasruntime/github-pr-instances-resource` image. @@ -58,6 +58,33 @@ As noted earlier, this resource can either track a list of PRs to a repository, or track commits to a single PR. The different modes of operation have different configuration options. +### Authentication + +This resource supports two authentication methods: + +1. **Personal Access Token**: The traditional method using a GitHub access token +2. **GitHub App**: Authentication using a GitHub App installation (new feature) + +You must choose one authentication method - they cannot be used together. + +#### Personal Access Token Authentication + +| Parameter | Required | Example | Description | +|----------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `access_token` | Yes* | | A Github Access Token with repository access. For private repositories, set `repo:full` permissions on the token. For public repositories, `repo:status` is sufficient. *Required unless using GitHub App authentication* | + +#### GitHub App Authentication + +| Parameter | Required | Example | Description | +|-------------------------------|----------|------------------------|------------------------------------------------------------------------| +| `github_app_id` | Yes* | `12345` | The ID of your GitHub App | +| `github_app_installation_id` | Yes* | `67890` | The installation ID of your GitHub App | +| `github_app_private_key` | Yes** | `-----BEGIN RSA...` | The private key content for your GitHub App | +| `github_app_private_key_path` | Yes** | `/path/to/private.key` | Path to a file containing the private key for your GitHub App | + +*Required only when using GitHub App authentication instead of access_token +**Either github_app_private_key OR github_app_private_key_path must be provided + ### List of PRs By omitting the `source.number` parameter, this resource will list PRs in a @@ -223,6 +250,8 @@ requires two pipeline templates. For this example, assume you have a resource named `ci`, a repo which contains the following pipeline files: +### Example using Personal Access Token + `ci/pipelines/parent.yml` ```yaml resource_types: @@ -315,6 +344,195 @@ resources: get_params: {skip_download: true} ``` +### Example using GitHub App Authentication + +`ci/pipelines/parent.yml` +```yaml +resource_types: +- name: pull-request + type: registry-image + source: + repository: tasruntime/github-pr-resource + +resources: +- name: pull-requests + type: pull-request + source: + repository: itsdalmo/test-repository + github_app_id: ((github-app-id)) + github_app_installation_id: ((github-app-installation-id)) + github_app_private_key: ((github-app-private-key)) + +- name: ci + type: git + source: + uri: https://github.com/concourse/ci + +jobs: +- name: update-pr-pipelines + plan: + - get: ci + - get: pull-requests + trigger: true + - load_var: pull_requests + file: pull-requests/prs.json + - across: + - var: pr + values: ((.:pull_requests)) + set_pipeline: prs + file: ci/pipelines/child.yml + instance_vars: {number: ((.:pr.number))} +``` + +`ci/pipelines/child.yml` +```yaml +resource_types: +- name: pull-request + type: registry-image + source: + repository: tasruntime/github-pr-resource + +resources: +- name: pull-request + type: pull-request + source: + repository: itsdalmo/test-repository + github_app_id: ((github-app-id)) + github_app_installation_id: ((github-app-installation-id)) + github_app_private_key: ((github-app-private-key)) + number: ((number)) + +jobs: +- name: test + plan: + - get: pull-request + trigger: true + - put: pull-request-status + resource: pull-request + params: + path: pull-request + status: pending + get_params: {skip_download: true} + # Rest of job definition remains the same +``` + +### Example using GitHub App Authentication with Submodules + +For repositories that contain git submodules, you can enable submodule support by setting `submodules: true` in the `get_params`. + +`ci/pipelines/parent.yml` (with GitHub App and Submodules) +```yaml +resource_types: +- name: pull-request + type: registry-image + source: + repository: tasruntime/github-pr-resource + +resources: +- name: pull-requests + type: pull-request + source: + repository: itsdalmo/test-repository + github_app_id: ((github-app-id)) + github_app_installation_id: ((github-app-installation-id)) + github_app_private_key: ((github-app-private-key)) + +- name: ci + type: git + source: + uri: https://github.com/concourse/ci + +jobs: +- name: update-pr-pipelines + plan: + - get: ci + - get: pull-requests + trigger: true + - load_var: pull_requests + file: pull-requests/prs.json + - across: + - var: pr + values: ((.:pull_requests)) + set_pipeline: prs + file: ci/pipelines/child.yml + instance_vars: {number: ((.:pr.number))} +``` + +`ci/pipelines/child.yml` (with GitHub App and Submodules) +```yaml +resource_types: +- name: pull-request + type: registry-image + source: + repository: tasruntime/github-pr-resource + +resources: +- name: pull-request + type: pull-request + source: + repository: itsdalmo/test-repository + github_app_id: ((github-app-id)) + github_app_installation_id: ((github-app-installation-id)) + github_app_private_key: ((github-app-private-key)) + number: ((number)) + +jobs: +- name: test + plan: + - get: pull-request + trigger: true + get_params: + submodules: true # Enable recursive submodule cloning + integration_tool: merge # Optional: merge, rebase, or checkout + - put: pull-request-status + resource: pull-request + params: + path: pull-request + status: pending + get_params: {skip_download: true} + - task: unit-test + config: + platform: linux + image_resource: + type: registry-image + source: {repository: alpine/git, tag: latest} + inputs: + - name: pull-request + run: + path: /bin/sh + args: + - -xce + - | + cd pull-request + # Submodules are now available in subdirectories + ls -la + git log --graph -n 10 --color --pretty=format:"%x1b[31m%h%x09%x1b[32m%d%x1b[0m%x20%s" > log.txt + cat log.txt + on_success: + put: pull-request + params: + path: pull-request + status: success + get_params: {skip_download: true} + on_failure: + put: pull-request + params: + path: pull-request + status: failure + get_params: {skip_download: true} +``` + +**Notes on Submodule Support:** + +* When `submodules: true` is set, the resource will recursively initialize and update all git submodules +* Submodules are supported with both `access_token` and `github_app_*` authentication methods +* The `integration_tool` parameter determines how submodules are updated after merging: + * `merge` (default): Uses `git submodule update --init --recursive --merge` + * `rebase`: Uses `git submodule update --init --recursive --rebase` + * `checkout`: Uses `git submodule update --init --recursive --checkout` +* All submodule repositories are automatically authenticated using the same credentials as the main repository +* Git LFS is supported alongside submodules through the `disable_git_lfs` parameter in the source + ## Costs The Github API(s) have a rate limit of 5000 requests per hour (per user). For the V3 API this essentially diff --git a/Taskfile.yml b/Taskfile.yml index 1365c919..cdf93347 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' vars: BUILD_DIR: build diff --git a/go.mod b/go.mod index 64dc6d46..041ae0c2 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,28 @@ module github.com/cloudfoundry-community/github-pr-instances-resource require ( - github.com/google/go-github/v28 v28.1.1 - github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3 - github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9 - github.com/stretchr/testify v1.3.0 - github.com/telia-oss/github-pr-resource v0.23.0 - golang.org/x/oauth2 v0.8.0 + github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 + github.com/google/go-github/v75 v75.0.0 + github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 + github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 + github.com/stretchr/testify v1.11.1 + golang.org/x/oauth2 v0.32.0 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/mod v0.2.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/tools v0.0.0-20200423205358-59e73619c742 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) -go 1.20 +go 1.24.0 + +tool github.com/maxbrunsfeld/counterfeiter/v6 diff --git a/go.sum b/go.sum index 80731f7c..25250e85 100644 --- a/go.sum +++ b/go.sum @@ -1,120 +1,55 @@ -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= -github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= +github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/joefitzgerald/rainbow-reporter v0.1.0 h1:AuMG652zjdzI0YCCnXAqATtRBpGXMcAnrajcaTrSeuo= -github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3 h1:z1lXirM9f9WTcdmzSZahKh/t+LCqPiiwK2/DB1kLlI4= -github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3/go.mod h1:1ftk08SazyElaaNvmqAfZWGwJzshjCfBXDLoQtPAMNk= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg= +github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= -github.com/shurcooL/githubv4 v0.0.0-20200414012201-bbc966b061dd/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= -github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9 h1:nCBaIs5/R0HFP5+aPW/SzFUF8z0oKuCXmuDmHWaxzjY= -github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= -github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc= -github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/telia-oss/github-pr-resource v0.23.0 h1:6d0ilcSQ1QQ6S/YIj3NfN3JBIODoNteOZf+ItK50z2I= -github.com/telia-oss/github-pr-resource v0.23.0/go.mod h1:sHeCU+dRqgLPVX+1Qe38aJ8t53RIG4y4dIaJkyzmnms= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200301222351-066e0c02454c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200423205358-59e73619c742 h1:9OGWpORUXvk8AsaBJlpzzDx7Srv/rSK6rvjcsJq4rJo= -golang.org/x/tools v0.0.0-20200423205358-59e73619c742/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M= +github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/models/fakes/fake_git.go b/models/fakes/fake_git.go index f920d713..60eb0b7b 100644 --- a/models/fakes/fake_git.go +++ b/models/fakes/fake_git.go @@ -139,15 +139,16 @@ func (fake *FakeGit) Checkout(arg1 string, arg2 string, arg3 bool) error { arg2 string arg3 bool }{arg1, arg2, arg3}) + stub := fake.CheckoutStub + fakeReturns := fake.checkoutReturns fake.recordInvocation("Checkout", []interface{}{arg1, arg2, arg3}) fake.checkoutMutex.Unlock() - if fake.CheckoutStub != nil { - return fake.CheckoutStub(arg1, arg2, arg3) + if stub != nil { + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 } - fakeReturns := fake.checkoutReturns return fakeReturns.result1 } @@ -203,15 +204,16 @@ func (fake *FakeGit) Fetch(arg1 string, arg2 int, arg3 int, arg4 bool, arg5 bool arg4 bool arg5 bool }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.FetchStub + fakeReturns := fake.fetchReturns fake.recordInvocation("Fetch", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.fetchMutex.Unlock() - if fake.FetchStub != nil { - return fake.FetchStub(arg1, arg2, arg3, arg4, arg5) + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 } - fakeReturns := fake.fetchReturns return fakeReturns.result1 } @@ -263,15 +265,16 @@ func (fake *FakeGit) GitCryptUnlock(arg1 string) error { fake.gitCryptUnlockArgsForCall = append(fake.gitCryptUnlockArgsForCall, struct { arg1 string }{arg1}) + stub := fake.GitCryptUnlockStub + fakeReturns := fake.gitCryptUnlockReturns fake.recordInvocation("GitCryptUnlock", []interface{}{arg1}) fake.gitCryptUnlockMutex.Unlock() - if fake.GitCryptUnlockStub != nil { - return fake.GitCryptUnlockStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1 } - fakeReturns := fake.gitCryptUnlockReturns return fakeReturns.result1 } @@ -323,15 +326,16 @@ func (fake *FakeGit) Init(arg1 *string) error { fake.initArgsForCall = append(fake.initArgsForCall, struct { arg1 *string }{arg1}) + stub := fake.InitStub + fakeReturns := fake.initReturns fake.recordInvocation("Init", []interface{}{arg1}) fake.initMutex.Unlock() - if fake.InitStub != nil { - return fake.InitStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1 } - fakeReturns := fake.initReturns return fakeReturns.result1 } @@ -384,15 +388,16 @@ func (fake *FakeGit) Merge(arg1 string, arg2 bool) error { arg1 string arg2 bool }{arg1, arg2}) + stub := fake.MergeStub + fakeReturns := fake.mergeReturns fake.recordInvocation("Merge", []interface{}{arg1, arg2}) fake.mergeMutex.Unlock() - if fake.MergeStub != nil { - return fake.MergeStub(arg1, arg2) + if stub != nil { + return stub(arg1, arg2) } if specificReturn { return ret.result1 } - fakeReturns := fake.mergeReturns return fakeReturns.result1 } @@ -448,15 +453,16 @@ func (fake *FakeGit) Pull(arg1 string, arg2 string, arg3 int, arg4 bool, arg5 bo arg4 bool arg5 bool }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.PullStub + fakeReturns := fake.pullReturns fake.recordInvocation("Pull", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.pullMutex.Unlock() - if fake.PullStub != nil { - return fake.PullStub(arg1, arg2, arg3, arg4, arg5) + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 } - fakeReturns := fake.pullReturns return fakeReturns.result1 } @@ -510,15 +516,16 @@ func (fake *FakeGit) Rebase(arg1 string, arg2 string, arg3 bool) error { arg2 string arg3 bool }{arg1, arg2, arg3}) + stub := fake.RebaseStub + fakeReturns := fake.rebaseReturns fake.recordInvocation("Rebase", []interface{}{arg1, arg2, arg3}) fake.rebaseMutex.Unlock() - if fake.RebaseStub != nil { - return fake.RebaseStub(arg1, arg2, arg3) + if stub != nil { + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 } - fakeReturns := fake.rebaseReturns return fakeReturns.result1 } @@ -583,15 +590,16 @@ func (fake *FakeGit) RevList(arg1 *string, arg2 []string, arg3 []string, arg4 bo arg3 []string arg4 bool }{arg1, arg2Copy, arg3Copy, arg4}) + stub := fake.RevListStub + fakeReturns := fake.revListReturns fake.recordInvocation("RevList", []interface{}{arg1, arg2Copy, arg3Copy, arg4}) fake.revListMutex.Unlock() - if fake.RevListStub != nil { - return fake.RevListStub(arg1, arg2, arg3, arg4) + if stub != nil { + return stub(arg1, arg2, arg3, arg4) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.revListReturns return fakeReturns.result1, fakeReturns.result2 } @@ -646,15 +654,16 @@ func (fake *FakeGit) RevParse(arg1 string) (string, error) { fake.revParseArgsForCall = append(fake.revParseArgsForCall, struct { arg1 string }{arg1}) + stub := fake.RevParseStub + fakeReturns := fake.revParseReturns fake.recordInvocation("RevParse", []interface{}{arg1}) fake.revParseMutex.Unlock() - if fake.RevParseStub != nil { - return fake.RevParseStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.revParseReturns return fakeReturns.result1, fakeReturns.result2 } @@ -706,24 +715,6 @@ func (fake *FakeGit) RevParseReturnsOnCall(i int, result1 string, result2 error) func (fake *FakeGit) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.checkoutMutex.RLock() - defer fake.checkoutMutex.RUnlock() - fake.fetchMutex.RLock() - defer fake.fetchMutex.RUnlock() - fake.gitCryptUnlockMutex.RLock() - defer fake.gitCryptUnlockMutex.RUnlock() - fake.initMutex.RLock() - defer fake.initMutex.RUnlock() - fake.mergeMutex.RLock() - defer fake.mergeMutex.RUnlock() - fake.pullMutex.RLock() - defer fake.pullMutex.RUnlock() - fake.rebaseMutex.RLock() - defer fake.rebaseMutex.RUnlock() - fake.revListMutex.RLock() - defer fake.revListMutex.RUnlock() - fake.revParseMutex.RLock() - defer fake.revParseMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/models/fakes/fake_github.go b/models/fakes/fake_github.go index 5e3fbedb..e34d0816 100644 --- a/models/fakes/fake_github.go +++ b/models/fakes/fake_github.go @@ -98,15 +98,16 @@ func (fake *FakeGithub) DeletePreviousComments(arg1 int) error { fake.deletePreviousCommentsArgsForCall = append(fake.deletePreviousCommentsArgsForCall, struct { arg1 int }{arg1}) + stub := fake.DeletePreviousCommentsStub + fakeReturns := fake.deletePreviousCommentsReturns fake.recordInvocation("DeletePreviousComments", []interface{}{arg1}) fake.deletePreviousCommentsMutex.Unlock() - if fake.DeletePreviousCommentsStub != nil { - return fake.DeletePreviousCommentsStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1 } - fakeReturns := fake.deletePreviousCommentsReturns return fakeReturns.result1 } @@ -159,15 +160,16 @@ func (fake *FakeGithub) GetPullRequest(arg1 int, arg2 string) (*models.PullReque arg1 int arg2 string }{arg1, arg2}) + stub := fake.GetPullRequestStub + fakeReturns := fake.getPullRequestReturns fake.recordInvocation("GetPullRequest", []interface{}{arg1, arg2}) fake.getPullRequestMutex.Unlock() - if fake.GetPullRequestStub != nil { - return fake.GetPullRequestStub(arg1, arg2) + if stub != nil { + return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.getPullRequestReturns return fakeReturns.result1, fakeReturns.result2 } @@ -222,15 +224,16 @@ func (fake *FakeGithub) ListModifiedFiles(arg1 int) ([]string, error) { fake.listModifiedFilesArgsForCall = append(fake.listModifiedFilesArgsForCall, struct { arg1 int }{arg1}) + stub := fake.ListModifiedFilesStub + fakeReturns := fake.listModifiedFilesReturns fake.recordInvocation("ListModifiedFiles", []interface{}{arg1}) fake.listModifiedFilesMutex.Unlock() - if fake.ListModifiedFilesStub != nil { - return fake.ListModifiedFilesStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.listModifiedFilesReturns return fakeReturns.result1, fakeReturns.result2 } @@ -290,15 +293,16 @@ func (fake *FakeGithub) ListPullRequests(arg1 []githubv4.PullRequestState) ([]*m fake.listPullRequestsArgsForCall = append(fake.listPullRequestsArgsForCall, struct { arg1 []githubv4.PullRequestState }{arg1Copy}) + stub := fake.ListPullRequestsStub + fakeReturns := fake.listPullRequestsReturns fake.recordInvocation("ListPullRequests", []interface{}{arg1Copy}) fake.listPullRequestsMutex.Unlock() - if fake.ListPullRequestsStub != nil { - return fake.ListPullRequestsStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.listPullRequestsReturns return fakeReturns.result1, fakeReturns.result2 } @@ -354,15 +358,16 @@ func (fake *FakeGithub) PostComment(arg1 int, arg2 string) error { arg1 int arg2 string }{arg1, arg2}) + stub := fake.PostCommentStub + fakeReturns := fake.postCommentReturns fake.recordInvocation("PostComment", []interface{}{arg1, arg2}) fake.postCommentMutex.Unlock() - if fake.PostCommentStub != nil { - return fake.PostCommentStub(arg1, arg2) + if stub != nil { + return stub(arg1, arg2) } if specificReturn { return ret.result1 } - fakeReturns := fake.postCommentReturns return fakeReturns.result1 } @@ -419,15 +424,16 @@ func (fake *FakeGithub) UpdateCommitStatus(arg1 string, arg2 string, arg3 string arg5 string arg6 string }{arg1, arg2, arg3, arg4, arg5, arg6}) + stub := fake.UpdateCommitStatusStub + fakeReturns := fake.updateCommitStatusReturns fake.recordInvocation("UpdateCommitStatus", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) fake.updateCommitStatusMutex.Unlock() - if fake.UpdateCommitStatusStub != nil { - return fake.UpdateCommitStatusStub(arg1, arg2, arg3, arg4, arg5, arg6) + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5, arg6) } if specificReturn { return ret.result1 } - fakeReturns := fake.updateCommitStatusReturns return fakeReturns.result1 } @@ -476,18 +482,6 @@ func (fake *FakeGithub) UpdateCommitStatusReturnsOnCall(i int, result1 error) { func (fake *FakeGithub) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.deletePreviousCommentsMutex.RLock() - defer fake.deletePreviousCommentsMutex.RUnlock() - fake.getPullRequestMutex.RLock() - defer fake.getPullRequestMutex.RUnlock() - fake.listModifiedFilesMutex.RLock() - defer fake.listModifiedFilesMutex.RUnlock() - fake.listPullRequestsMutex.RLock() - defer fake.listPullRequestsMutex.RUnlock() - fake.postCommentMutex.RLock() - defer fake.postCommentMutex.RUnlock() - fake.updateCommitStatusMutex.RLock() - defer fake.updateCommitStatusMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/models/git.go b/models/git.go index 6ace4f0f..6ef9333c 100644 --- a/models/git.go +++ b/models/git.go @@ -1,14 +1,17 @@ package models import ( + "crypto/tls" "fmt" "io" - "io/ioutil" + "net/http" "net/url" "os" "os/exec" "strconv" "strings" + + "github.com/bradleyfalzon/ghinstallation/v2" ) // Git interface for testing purposes. @@ -27,6 +30,11 @@ type Git interface { } func NewGitClient(common CommonConfig, disableGitLFS bool, dir string, output io.Writer) (*GitClient, error) { + // Check if GitHub App authentication should be used + if common.GithubAppID != "" { + return NewGitClientWithApp(common, disableGitLFS, dir, output) + } + if common.SkipSSLVerification { os.Setenv("GIT_SSL_NO_VERIFY", "true") } @@ -40,11 +48,77 @@ func NewGitClient(common CommonConfig, disableGitLFS bool, dir string, output io }, nil } +// NewGitClientWithApp creates a GitClient authenticated via GitHub App +func NewGitClientWithApp(common CommonConfig, disableGitLFS bool, dir string, output io.Writer) (*GitClient, error) { + if disableGitLFS { + os.Setenv("GIT_LFS_SKIP_SMUDGE", "true") + } + + // Configure HTTP transport for GitHub App authentication + var tr *http.Transport + if common.SkipSSLVerification { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + os.Setenv("GIT_SSL_NO_VERIFY", "true") + } else { + tr = http.DefaultTransport.(*http.Transport) + } + + githubAppID, err := toInt64(common.GithubAppID) + if err != nil { + return nil, fmt.Errorf("github_app_id: %v", err) + } + + githubAppInstallationID, err := toInt64(common.GithubAppInstallationID) + if err != nil { + return nil, fmt.Errorf("github_app_installation: %v", err) + } + + // Create a GitHub App transport using provided credentials + var transport *ghinstallation.Transport + // var err error + if common.GithubAppPrivateKeyPath != "" { + transport, err = ghinstallation.NewKeyFromFile( + tr, + githubAppID, + githubAppInstallationID, + common.GithubAppPrivateKeyPath, + ) + } else { + transport, err = ghinstallation.New( + tr, + githubAppID, + githubAppInstallationID, + []byte(common.GithubAppPrivateKey), + ) + } + if err != nil { + return nil, fmt.Errorf("failed to create GitHub App transport: %v", err) + } + + // Generate a token for Git authentication + token, err := transport.Token(nil) + if err != nil { + return nil, fmt.Errorf("failed to generate GitHub App token: %v", err) + } + + return &GitClient{ + AccessToken: token, + Directory: dir, + Output: output, + // Store the transport for potential token refresh if needed + transport: transport, + }, nil +} + // GitClient ... type GitClient struct { AccessToken string Directory string Output io.Writer + + transport *ghinstallation.Transport // For GitHub App authentication } func (g *GitClient) silentCommand(name string, arg ...string) *exec.Cmd { @@ -79,9 +153,12 @@ func (g *GitClient) Init(branch *string) error { if err := g.command("git", "config", "user.email", "concourse@local").Run(); err != nil { return fmt.Errorf("failed to configure git email: %s", err) } - if err := g.command("git", "config", "url.https://x-oauth-basic@github.com/.insteadOf", "git@github.com:").Run(); err != nil { + if err := g.command("git", "config", "--global", "url.https://x-oauth-basic@github.com/.insteadOf", "git@github.com:").Run(); err != nil { return fmt.Errorf("failed to configure github url: %s", err) } + if err := g.command("git", "config", "--global", "credential.helper", fmt.Sprintf("!f() { echo \"username=x-access-token\"; echo \"password=%s\"; }; f", g.AccessToken)).Run(); err != nil { + return fmt.Errorf("failed to configure credential.helper") + } if err := g.command("git", "config", "url.https://.insteadOf", "git://").Run(); err != nil { return fmt.Errorf("failed to configure github url: %s", err) } @@ -112,8 +189,8 @@ func (g *GitClient) Pull(uri, branch string, depth int, submodules bool, fetchTa cmd := g.command("git", args...) // Discard output to have zero chance of logging the access token. - cmd.Stdout = ioutil.Discard - cmd.Stderr = ioutil.Discard + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard if err := cmd.Run(); err != nil { return fmt.Errorf("pull failed: %s", cmd) @@ -216,8 +293,8 @@ func (g *GitClient) Fetch(uri string, prNumber int, depth int, submodules bool, cmd := g.command("git", args...) // Discard output to have zero chance of logging the access token. - cmd.Stdout = ioutil.Discard - cmd.Stderr = ioutil.Discard + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard if err := cmd.Run(); err != nil { return fmt.Errorf("fetch failed: %v", err) @@ -289,6 +366,16 @@ func (g *GitClient) Endpoint(uri string) (string, error) { if err != nil { return "", fmt.Errorf("failed to parse commit url: %s", err) } + + // If using GitHub App authentication and token needs refresh + if g.transport != nil { + token, err := g.transport.Token(nil) + if err != nil { + return "", fmt.Errorf("failed to refresh GitHub App token: %v", err) + } + g.AccessToken = token + } + endpoint.User = url.UserPassword("x-oauth-basic", g.AccessToken) return endpoint.String(), nil } diff --git a/models/git_test.go b/models/git_test.go new file mode 100644 index 00000000..7c592e83 --- /dev/null +++ b/models/git_test.go @@ -0,0 +1,304 @@ +package models_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/cloudfoundry-community/github-pr-instances-resource/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewGitClient(t *testing.T) { + tests := []struct { + description string + common models.CommonConfig + disableGitLFS bool + expectedErr string + }{ + { + description: "create Git client with access token", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + disableGitLFS: false, + expectedErr: "", + }, + { + description: "create Git client with access token and skip SSL verification", + common: models.CommonConfig{ + AccessToken: "test-token", + SkipSSLVerification: true, + }, + disableGitLFS: false, + expectedErr: "", + }, + { + description: "create Git client with disabled Git LFS", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + disableGitLFS: true, + expectedErr: "", + }, + { + description: "create Git client with all options enabled", + common: models.CommonConfig{ + AccessToken: "test-token", + SkipSSLVerification: true, + }, + disableGitLFS: true, + expectedErr: "", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + output := &bytes.Buffer{} + + client, err := models.NewGitClient(tc.common, tc.disableGitLFS, tmpDir, output) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, "test-token", client.AccessToken) + assert.Equal(t, tmpDir, client.Directory) + assert.Equal(t, output, client.Output) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + assert.Nil(t, client) + } + }) + } +} + +func TestNewGitClientWithApp(t *testing.T) { + // Create a temporary private key file for testing + tmpDir := t.TempDir() + privateKeyPath := filepath.Join(tmpDir, "private.key") + + // Write a dummy private key (not valid, just for testing path handling) + err := os.WriteFile(privateKeyPath, []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a..."), 0600) + require.NoError(t, err) + + tests := []struct { + description string + common models.CommonConfig + disableGitLFS bool + expectedErr string + }{ + { + description: "create Git client with GitHub App using private key", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + disableGitLFS: false, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create Git client with GitHub App using private key path", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKeyPath: privateKeyPath, + }, + disableGitLFS: false, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create Git client with GitHub App and disabled Git LFS", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + disableGitLFS: true, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create Git client with GitHub App and skip SSL verification", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + SkipSSLVerification: true, + }, + disableGitLFS: false, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create Git client with GitHub App with invalid app ID", + common: models.CommonConfig{ + GithubAppID: "not-a-number", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + disableGitLFS: false, + expectedErr: "github_app_id", + }, + { + description: "create Git client with GitHub App with invalid installation ID", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "not-a-number", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + disableGitLFS: false, + expectedErr: "github_app_installation", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + workDir := t.TempDir() + output := &bytes.Buffer{} + + client, err := models.NewGitClient(tc.common, tc.disableGitLFS, workDir, output) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, workDir, client.Directory) + assert.Equal(t, output, client.Output) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + assert.Nil(t, client) + } + }) + } +} + +func TestNewGitClientRouting(t *testing.T) { + tests := []struct { + description string + common models.CommonConfig + expectedAuthType string + }{ + { + description: "should route to standard token auth when no GitHub App ID", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + expectedAuthType: "token", + }, + { + description: "should route to GitHub App when GitHub App ID is set", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + expectedAuthType: "github-app", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + output := &bytes.Buffer{} + + client, err := models.NewGitClient(tc.common, false, tmpDir, output) + + // For token auth: should succeed + // For GitHub App: will error due to invalid key, but that's expected + if tc.expectedAuthType == "token" { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, "test-token", client.AccessToken) + } else { + // GitHub App path will error due to invalid private key + assert.Error(t, err) + } + }) + } +} + +func TestNewGitClientEnvironmentVariables(t *testing.T) { + tmpDir := t.TempDir() + output := &bytes.Buffer{} + + // Save original environment variables + originalSSLNoVerify := os.Getenv("GIT_SSL_NO_VERIFY") + originalLFSSkipSmudge := os.Getenv("GIT_LFS_SKIP_SMUDGE") + defer func() { + os.Setenv("GIT_SSL_NO_VERIFY", originalSSLNoVerify) + os.Setenv("GIT_LFS_SKIP_SMUDGE", originalLFSSkipSmudge) + }() + + tests := []struct { + description string + common models.CommonConfig + disableGitLFS bool + expectSSLNoVerify bool + expectLFSSkipSmudge bool + }{ + { + description: "no environment variables set when skip SSL and disable LFS are false", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + disableGitLFS: false, + expectSSLNoVerify: false, + expectLFSSkipSmudge: false, + }, + { + description: "GIT_SSL_NO_VERIFY set when skip SSL is true", + common: models.CommonConfig{ + AccessToken: "test-token", + SkipSSLVerification: true, + }, + disableGitLFS: false, + expectSSLNoVerify: true, + expectLFSSkipSmudge: false, + }, + { + description: "GIT_LFS_SKIP_SMUDGE set when disable LFS is true", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + disableGitLFS: true, + expectSSLNoVerify: false, + expectLFSSkipSmudge: true, + }, + { + description: "both environment variables set when both options are true", + common: models.CommonConfig{ + AccessToken: "test-token", + SkipSSLVerification: true, + }, + disableGitLFS: true, + expectSSLNoVerify: true, + expectLFSSkipSmudge: true, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + // Clean environment + os.Unsetenv("GIT_SSL_NO_VERIFY") + os.Unsetenv("GIT_LFS_SKIP_SMUDGE") + + _, err := models.NewGitClient(tc.common, tc.disableGitLFS, tmpDir, output) + assert.NoError(t, err) + + if tc.expectSSLNoVerify { + assert.Equal(t, "true", os.Getenv("GIT_SSL_NO_VERIFY")) + } else { + assert.Equal(t, "", os.Getenv("GIT_SSL_NO_VERIFY")) + } + + if tc.expectLFSSkipSmudge { + assert.Equal(t, "true", os.Getenv("GIT_LFS_SKIP_SMUDGE")) + } else { + assert.Equal(t, "", os.Getenv("GIT_LFS_SKIP_SMUDGE")) + } + }) + } +} diff --git a/models/github.go b/models/github.go index 3dd6327f..451f0f26 100644 --- a/models/github.go +++ b/models/github.go @@ -9,9 +9,11 @@ import ( "net/url" "os" "path" + "strconv" "strings" - "github.com/google/go-github/v28/github" + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/google/go-github/v75/github" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" ) @@ -19,6 +21,12 @@ import ( type CommonConfig struct { AccessToken string `json:"access_token"` SkipSSLVerification bool `json:"skip_ssl_verification"` + + // GitHub Apps support + GithubAppID string `json:"github_app_id,omitempty"` + GithubAppInstallationID string `json:"github_app_installation_id,omitempty"` + GithubAppPrivateKey string `json:"github_app_private_key,omitempty"` + GithubAppPrivateKeyPath string `json:"github_app_private_key_path,omitempty"` } type GithubConfig struct { @@ -59,6 +67,12 @@ type GithubClient struct { // NewGithubClient ... func NewGithubClient(common CommonConfig, config GithubConfig) (*GithubClient, error) { + + // 檢查是否使用 GitHub Apps + if common.GithubAppID != "" { + return NewGithubClientWithApp(common, config) + } + owner, repository, err := parseRepository(config.Repository) if err != nil { return nil, err @@ -372,3 +386,99 @@ func parseRepository(s string) (string, string, error) { } return parts[0], parts[1], nil } + +// helper function to convert string to int64 +func toInt64(s string) (int64, error) { + num, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, fmt.Errorf("'%s' is not a valid integer", s) + } + return num, nil +} + +func NewGithubClientWithApp(common CommonConfig, config GithubConfig) (*GithubClient, error) { + // Parse the repository string to extract owner and repository name + owner, repository, err := parseRepository(config.Repository) + if err != nil { + return nil, err // Error if repository parsing fails + } + + githubAppID, err := toInt64(common.GithubAppID) + if err != nil { + return nil, fmt.Errorf("github_app_id: %v", err) + } + + githubAppInstallationID, err := toInt64(common.GithubAppInstallationID) + if err != nil { + return nil, fmt.Errorf("github_app_installation: %v", err) + } + + // Configure HTTP transport to skip SSL verification if enabled, for self-signed certificates + var tr *http.Transport + if common.SkipSSLVerification { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } else { + tr = http.DefaultTransport.(*http.Transport) // Use default transport + } + + // Create a GitHub App transport using the provided credentials + var transport *ghinstallation.Transport + if common.GithubAppPrivateKeyPath != "" { + transport, err = ghinstallation.NewKeyFromFile( + tr, + githubAppID, + githubAppInstallationID, + common.GithubAppPrivateKeyPath, + ) + } else { + transport, err = ghinstallation.New( + tr, + githubAppID, + githubAppInstallationID, + []byte(common.GithubAppPrivateKey), + ) + } + if err != nil { + return nil, fmt.Errorf("failed to create GitHub App transport: %v", err) + } + + // Initialize the HTTP client with the custom transport + client := &http.Client{Transport: transport} + + // Create a GitHub v3 client, using a custom endpoint if specified + var v3 *github.Client + if config.V3Endpoint != "" { + endpoint, err := url.Parse(config.V3Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to parse v3 endpoint: %v", err) + } + v3, err = github.NewClient(client).WithEnterpriseURLs(endpoint.String(), endpoint.String()) + if err != nil { + return nil, err + } + } else { + v3 = github.NewClient(client) // Use default v3 client + } + + // Create a GitHub v4 client, using a custom endpoint if specified + var v4 *githubv4.Client + if config.V4Endpoint != "" { + endpoint, err := url.Parse(config.V4Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to parse v4 endpoint: %v", err) + } + v4 = githubv4.NewEnterpriseClient(endpoint.String(), client) + } else { + v4 = githubv4.NewClient(client) // Use default v4 client + } + + return &GithubClient{ + HostingEndpoint: config.HostingEndpoint, + V3: v3, + V4: v4, + Owner: owner, + Repository: repository, + }, nil +} diff --git a/models/github_test.go b/models/github_test.go new file mode 100644 index 00000000..5f1e710a --- /dev/null +++ b/models/github_test.go @@ -0,0 +1,304 @@ +package models_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/cloudfoundry-community/github-pr-instances-resource/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewGithubClient(t *testing.T) { + tests := []struct { + description string + common models.CommonConfig + config models.GithubConfig + expectedErr string + }{ + { + description: "create GitHub client with access token", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "", + }, + { + description: "create GitHub client with access token and skip SSL verification", + common: models.CommonConfig{ + AccessToken: "test-token", + SkipSSLVerification: true, + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "", + }, + { + description: "create GitHub client with invalid repository format", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + config: models.GithubConfig{ + Repository: "invalid-repo-format", + }, + expectedErr: "malformed repository", + }, + { + description: "create GitHub client with v3 and v4 endpoints", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + expectedErr: "", + }, + { + description: "create GitHub client with invalid v3 endpoint URL", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "://invalid-url", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + expectedErr: "failed to parse v3 endpoint", + }, + { + description: "create GitHub client with invalid v4 endpoint URL", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "://invalid-url", + }, + expectedErr: "failed to parse v4 endpoint", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + client, err := models.NewGithubClient(tc.common, tc.config) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.NotNil(t, client.V3) + assert.NotNil(t, client.V4) + assert.Equal(t, "repo", client.Repository) + assert.Equal(t, "owner", client.Owner) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + assert.Nil(t, client) + } + }) + } +} + +func TestNewGithubClientWithApp(t *testing.T) { + // Create a temporary private key file for testing + tmpDir := t.TempDir() + privateKeyPath := filepath.Join(tmpDir, "private.key") + + // Write a dummy private key (this is not a valid key, just for testing path handling) + err := os.WriteFile(privateKeyPath, []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a..."), 0600) + require.NoError(t, err) + + tests := []struct { + description string + common models.CommonConfig + config models.GithubConfig + expectedErr string + }{ + { + description: "create GitHub client with GitHub App using private key", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create GitHub client with GitHub App using private key path", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKeyPath: privateKeyPath, + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create GitHub client with GitHub App with invalid app ID", + common: models.CommonConfig{ + GithubAppID: "not-a-number", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "github_app_id", + }, + { + description: "create GitHub client with GitHub App with invalid installation ID", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "not-a-number", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "github_app_installation", + }, + { + description: "create GitHub client with GitHub App and invalid repository", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "invalid-repo", + }, + expectedErr: "malformed repository", + }, + { + description: "create GitHub client with GitHub App and skip SSL verification", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + SkipSSLVerification: true, + }, + config: models.GithubConfig{ + Repository: "owner/repo", + }, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create GitHub client with GitHub App and custom v3 endpoint", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create GitHub client with GitHub App and invalid v3 endpoint", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "://invalid-url", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + expectedErr: "failed to create GitHub App transport", + }, + { + description: "create GitHub client with GitHub App and invalid v4 endpoint", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + config: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "://invalid-url", + }, + expectedErr: "failed to create GitHub App transport", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + client, err := models.NewGithubClient(tc.common, tc.config) + + if tc.expectedErr == "" { + assert.NoError(t, err) + assert.NotNil(t, client) + assert.NotNil(t, client.V3) + assert.NotNil(t, client.V4) + assert.Equal(t, "repo", client.Repository) + assert.Equal(t, "owner", client.Owner) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + assert.Nil(t, client) + } + }) + } +} + +func TestNewGithubClientRouting(t *testing.T) { + tests := []struct { + description string + common models.CommonConfig + expectedAuthType string + }{ + { + description: "should route to standard OAuth2 when no GitHub App ID", + common: models.CommonConfig{ + AccessToken: "test-token", + }, + expectedAuthType: "oauth2", + }, + { + description: "should route to GitHub App when GitHub App ID is set", + common: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2a...", + }, + expectedAuthType: "github-app", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + config := models.GithubConfig{ + Repository: "owner/repo", + } + + client, err := models.NewGithubClient(tc.common, config) + + // For OAuth2: should succeed with valid config + // For GitHub App: will error due to invalid key, but that's expected + if tc.expectedAuthType == "oauth2" { + assert.NoError(t, err) + assert.NotNil(t, client) + } else { + // GitHub App path will error due to invalid private key + assert.Error(t, err) + } + }) + } +} diff --git a/pr/models.go b/pr/models.go index 06822d43..cd36c45b 100644 --- a/pr/models.go +++ b/pr/models.go @@ -20,9 +20,19 @@ type Source struct { // Validate the source configuration. func (s *Source) Validate() error { - if s.AccessToken == "" { - return errors.New("access_token must be set") + // Check if there is at least one authentication method + hasAccessToken := s.AccessToken != "" + hasGithubApp := s.GithubAppID != "" && s.GithubAppInstallationID != "" && + (s.GithubAppPrivateKey != "" || s.GithubAppPrivateKeyPath != "") + + if !hasAccessToken && !hasGithubApp { + return errors.New("either access_token or github app credentials (github_app_id, github_app_installation_id, and github_app_private_key/github_app_private_key_path) must be set") + } + + if hasAccessToken && hasGithubApp { + return errors.New("cannot use both access_token and github app credentials") } + if s.Repository == "" { return errors.New("repository must be set") } diff --git a/pr/models_test.go b/pr/models_test.go new file mode 100644 index 00000000..bbc136bf --- /dev/null +++ b/pr/models_test.go @@ -0,0 +1,175 @@ +package pr_test + +import ( + "testing" + + "github.com/cloudfoundry-community/github-pr-instances-resource/models" + "github.com/cloudfoundry-community/github-pr-instances-resource/pr" + "github.com/stretchr/testify/assert" +) + +func TestSourceValidateAuthentication(t *testing.T) { + tests := []struct { + description string + source pr.Source + expectedErr string + }{ + { + description: "validate succeeds with access token", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate succeeds with GitHub App using private key", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate succeeds with GitHub App using private key path", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKeyPath: "/path/to/private.key", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate fails with neither access token nor GitHub App", + source: pr.Source{ + CommonConfig: models.CommonConfig{}, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with both access token and GitHub App", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "cannot use both access_token and github app credentials", + }, + { + description: "validate fails with missing repository", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "", + }, + }, + expectedErr: "repository must be set", + }, + { + description: "validate fails with GitHub App missing installation ID", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with GitHub App missing private key and path", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with mismatched endpoint configuration - only hosting endpoint", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + HostingEndpoint: "https://github.enterprise.com", + }, + }, + expectedErr: "if any of hosting_endpoint, v3_endpoint, or v4_endpoint are set, all of them must be set", + }, + { + description: "validate fails with mismatched endpoint configuration - only v3 endpoint", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + }, + }, + expectedErr: "if any of hosting_endpoint, v3_endpoint, or v4_endpoint are set, all of them must be set", + }, + { + description: "validate succeeds with all endpoints configured", + source: pr.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + HostingEndpoint: "https://github.enterprise.com", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + }, + expectedErr: "", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + err := tc.source.Validate() + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + } + }) + } +} diff --git a/prlist/check_test.go b/prlist/check_test.go index 4bea9e91..f18aa71c 100644 --- a/prlist/check_test.go +++ b/prlist/check_test.go @@ -30,6 +30,7 @@ var ( ) func TestCheck(t *testing.T) { + now := time.Now() tests := []struct { description string source prlist.Source @@ -52,7 +53,7 @@ func TestCheck(t *testing.T) { pullRequests: testPullRequests, files: [][]string{}, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -66,11 +67,11 @@ func TestCheck(t *testing.T) { AccessToken: "oauthtoken", }, }, - version: &prlist.Version{PRs: "[2]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[2]", Timestamp: now.Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests[1:2], files: [][]string{}, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[2]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[2]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -84,12 +85,12 @@ func TestCheck(t *testing.T) { AccessToken: "oauthtoken", }, }, - version: &prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, files: [][]string{}, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -104,7 +105,7 @@ func TestCheck(t *testing.T) { }, Paths: []string{"terraform/*/*.tf", "terraform/*/*/*.tf"}, }, - version: &prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, files: [][]string{ {"README.md", "travis.yml"}, @@ -112,8 +113,8 @@ func TestCheck(t *testing.T) { {"terraform/modules/variables.tf", "travis.yml"}, }, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[2,3]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[2,3]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -128,7 +129,7 @@ func TestCheck(t *testing.T) { }, IgnorePaths: []string{"*.md", "*.yml"}, }, - version: &prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, files: [][]string{ {"README.md", "travis.yml"}, // Applies to PR 1 @@ -137,8 +138,8 @@ func TestCheck(t *testing.T) { // Subsequent calls will be empty }, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[2,3]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[2,3]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -153,11 +154,11 @@ func TestCheck(t *testing.T) { }, DisableCISkip: true, }, - version: &prlist.Version{PRs: "[2]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[2]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[2]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[2]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -172,11 +173,11 @@ func TestCheck(t *testing.T) { }, IgnoreDrafts: true, }, - version: &prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[1,2,4,5,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,4,5,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -191,11 +192,11 @@ func TestCheck(t *testing.T) { }, IgnoreDrafts: false, }, - version: &prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[4]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[4]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,3,4,5,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -210,11 +211,11 @@ func TestCheck(t *testing.T) { }, DisableForks: true, }, - version: &prlist.Version{PRs: "[6]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[6]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[6]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[1,2,3,4,6,7,8,9,12]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[6]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[1,2,3,4,6,7,8,9,12]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -248,11 +249,11 @@ func TestCheck(t *testing.T) { }, RequiredReviewApprovals: 1, }, - version: &prlist.Version{PRs: "[9]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[9]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[9]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[8]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[9]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[8]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, @@ -324,12 +325,12 @@ func TestCheck(t *testing.T) { }, States: []githubv4.PullRequestState{githubv4.PullRequestStateClosed, githubv4.PullRequestStateMerged}, }, - version: &prlist.Version{PRs: "[12]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + version: &prlist.Version{PRs: "[12]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, pullRequests: testPullRequests, files: [][]string{}, expected: prlist.CheckResponse{ - prlist.Version{PRs: "[12]", Timestamp: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, - prlist.Version{PRs: "[10,11]", Timestamp: time.Now().Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[12]", Timestamp: now.Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}, + prlist.Version{PRs: "[10,11]", Timestamp: now.Format("2006-01-02 15:04:05")}, }, }, } diff --git a/prlist/models.go b/prlist/models.go index 7d881c48..8ad64e3e 100644 --- a/prlist/models.go +++ b/prlist/models.go @@ -28,9 +28,19 @@ type Source struct { // Validate the source configuration. func (s *Source) Validate() error { - if s.AccessToken == "" { - return errors.New("access_token must be set") + // Check if there is at least one authentication method + hasAccessToken := s.AccessToken != "" + hasGithubApp := s.GithubAppID != "" && s.GithubAppInstallationID != "" && + (s.GithubAppPrivateKey != "" || s.GithubAppPrivateKeyPath != "") + + if !hasAccessToken && !hasGithubApp { + return errors.New("either access_token or github app credentials (github_app_id, github_app_installation_id, and github_app_private_key/github_app_private_key_path) must be set") + } + + if hasAccessToken && hasGithubApp { + return errors.New("cannot use both access_token and github app credentials") } + if s.Repository == "" { return errors.New("repository must be set") } @@ -46,7 +56,7 @@ func (s *Source) Validate() error { case githubv4.PullRequestStateClosed: case githubv4.PullRequestStateMerged: default: - return errors.New(fmt.Sprintf("states value \"%s\" must be one of: OPEN, MERGED, CLOSED", state)) + return fmt.Errorf("states value \"%s\" must be one of: OPEN, MERGED, CLOSED", state) } } return nil diff --git a/prlist/models_test.go b/prlist/models_test.go new file mode 100644 index 00000000..6bd64c83 --- /dev/null +++ b/prlist/models_test.go @@ -0,0 +1,208 @@ +package prlist_test + +import ( + "testing" + + "github.com/cloudfoundry-community/github-pr-instances-resource/models" + "github.com/cloudfoundry-community/github-pr-instances-resource/prlist" + "github.com/shurcooL/githubv4" + "github.com/stretchr/testify/assert" +) + +func TestSourceValidateAuthentication(t *testing.T) { + tests := []struct { + description string + source prlist.Source + expectedErr string + }{ + { + description: "validate succeeds with access token", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate succeeds with GitHub App using private key", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate succeeds with GitHub App using private key path", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKeyPath: "/path/to/private.key", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "", + }, + { + description: "validate fails with neither access token nor GitHub App", + source: prlist.Source{ + CommonConfig: models.CommonConfig{}, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with both access token and GitHub App", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + GithubAppID: "12345", + GithubAppInstallationID: "67890", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "cannot use both access_token and github app credentials", + }, + { + description: "validate fails with missing repository", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "", + }, + }, + expectedErr: "repository must be set", + }, + { + description: "validate fails with GitHub App missing installation ID", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with GitHub App missing private key and path", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + GithubAppID: "12345", + GithubAppInstallationID: "67890", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + }, + expectedErr: "either access_token or github app credentials", + }, + { + description: "validate fails with only v3 endpoint", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + }, + }, + expectedErr: "v4_endpoint must be set together with v3_endpoint", + }, + { + description: "validate fails with only v4 endpoint", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + }, + expectedErr: "v3_endpoint must be set together with v4_endpoint", + }, + { + description: "validate succeeds with both v3 and v4 endpoints", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + V3Endpoint: "https://github.enterprise.com/api/v3", + V4Endpoint: "https://github.enterprise.com/api/graphql", + }, + }, + expectedErr: "", + }, + { + description: "validate succeeds with valid PR states", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + States: []githubv4.PullRequestState{ + githubv4.PullRequestStateOpen, + githubv4.PullRequestStateMerged, + githubv4.PullRequestStateClosed, + }, + }, + expectedErr: "", + }, + { + description: "validate fails with invalid PR state", + source: prlist.Source{ + CommonConfig: models.CommonConfig{ + AccessToken: "my-token", + }, + GithubConfig: models.GithubConfig{ + Repository: "owner/repo", + }, + States: []githubv4.PullRequestState{ + githubv4.PullRequestStateOpen, + "INVALID_STATE", + }, + }, + expectedErr: "must be one of: OPEN, MERGED, CLOSED", + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + err := tc.source.Validate() + + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + } + }) + } +}