From 374b89d3d1d738a175d34453648c309ab4c4fa72 Mon Sep 17 00:00:00 2001 From: Marc Izquierdo Date: Tue, 20 Oct 2026 19:50:45 +0200 Subject: [PATCH 1/2] Send Teamwork requests through one curl helper Each API call assembled its own curl command line. Route them all through teamwork::request so the options live in one place. The helper also passes -sS. curl prints a progress meter when its output is captured, which filled the logs with timings that change on every run; -S still shows errors. Co-Authored-By: Claude Opus 5.5 --- src/teamwork.sh | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/teamwork.sh b/src/teamwork.sh index c7da9e07..4439a1df 100644 --- a/src/teamwork.sh +++ b/src/teamwork.sh @@ -16,6 +16,20 @@ teamwork::get_task_id_from_body() { echo "$task_ids_str" } +# Calls the Teamwork API and prints the response body. +teamwork::request() { + local -r method=$1 + local -r path=$2 + local -r payload=${3:-} + + local args=(-sS -X "$method" -u "$TEAMWORK_API_TOKEN"':') + if [ -n "$payload" ]; then + args+=(-H 'Content-Type: application/json; charset=utf-8' -d "$payload") + fi + + curl "${args[@]}" "$TEAMWORK_URI$path" +} + teamwork::get_project_id_from_task() { local -r task_id=$1 @@ -25,7 +39,7 @@ teamwork::get_project_id_from_task() { fi response=$( - curl "$TEAMWORK_URI/projects/api/v1/tasks/$task_id.json" -u "$TEAMWORK_API_TOKEN"':' |\ + teamwork::request GET "/projects/api/v1/tasks/$task_id.json" |\ jq -r '.["todo-item"]["project-id"]' ) echo "$response" @@ -50,8 +64,7 @@ teamwork::get_workflow_stage() { # A single call scoped to the project sideloads every stage across the # project's workflow(s); each stage carries its parent workflow id, which the # move endpoint also needs. - curl -s "$TEAMWORK_URI/projects/api/v3/workflows.json?projectIds=$TEAMWORK_PROJECT_ID&include=stages" \ - -u "$TEAMWORK_API_TOKEN"':' |\ + teamwork::request GET "/projects/api/v3/workflows.json?projectIds=$TEAMWORK_PROJECT_ID&include=stages" |\ jq -r --arg stage_name "$stage_name" \ '[.included.stages[]? | select(.name | contains($stage_name)) | "\(.workflow.id) \(.id)"][0] // empty' } @@ -78,10 +91,8 @@ teamwork::move_task_to_stage() { return fi - response=$(curl -X "PATCH" "$TEAMWORK_URI/projects/api/v3/tasks/$TEAMWORK_TASK_ID/workflows/$workflow_id.json" \ - -u "$TEAMWORK_API_TOKEN"':' \ - -H 'Content-Type: application/json; charset=utf-8' \ - -d "{ \"stageId\": $stage_id, \"positionAfterTask\": -1 }" ) + response=$(teamwork::request PATCH "/projects/api/v3/tasks/$TEAMWORK_TASK_ID/workflows/$workflow_id.json" \ + "{ \"stageId\": $stage_id, \"positionAfterTask\": -1 }") log::message "$response" } @@ -94,10 +105,8 @@ teamwork::add_comment() { return fi - response=$(curl -X "POST" "$TEAMWORK_URI/tasks/$TEAMWORK_TASK_ID/comments.json" \ - -u "$TEAMWORK_API_TOKEN"':' \ - -H 'Content-Type: application/json; charset=utf-8' \ - -d "{ \"comment\": { \"body\": \"${body//\"/}\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" ) + response=$(teamwork::request POST "/tasks/$TEAMWORK_TASK_ID/comments.json" \ + "{ \"comment\": { \"body\": \"${body//\"/}\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }") log::message "$response" } @@ -111,10 +120,8 @@ teamwork::add_tag() { fi if [ "$AUTOMATIC_TAGGING" == true ]; then - response=$(curl -X "PUT" "$TEAMWORK_URI/tasks/$TEAMWORK_TASK_ID/tags.json" \ - -u "$TEAMWORK_API_TOKEN"':' \ - -H 'Content-Type: application/json; charset=utf-8' \ - -d "{ \"tags\": { \"content\": \"${tag_name//\"/}\" } }" ) + response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \ + "{ \"tags\": { \"content\": \"${tag_name//\"/}\" } }") log::message "$response" fi @@ -129,10 +136,8 @@ teamwork::remove_tag() { fi if [ "$AUTOMATIC_TAGGING" == true ]; then - response=$(curl -X "PUT" "$TEAMWORK_URI/tasks/$TEAMWORK_TASK_ID/tags.json" \ - -u "$TEAMWORK_API_TOKEN"':' \ - -H 'Content-Type: application/json; charset=utf-8' \ - -d "{ \"tags\": { \"content\": \"${tag_name//\"/}\" },\"removeProvidedTags\":\"true\" }" ) + response=$(teamwork::request PUT "/tasks/$TEAMWORK_TASK_ID/tags.json" \ + "{ \"tags\": { \"content\": \"${tag_name//\"/}\" },\"removeProvidedTags\":\"true\" }") log::message "$response" fi From 2cf73001ff5e6937fcb8c425019ac6d7fb349dba Mon Sep 17 00:00:00 2001 From: Marc Izquierdo Date: Tue, 20 Oct 2026 19:52:35 +0200 Subject: [PATCH 2/2] Test the action against a mock Teamwork API CI only ran the action in test mode, where every Teamwork request is skipped, so nothing checked what the action actually sends. tests/run.sh builds the image, starts a mock Teamwork API (tests/mock-teamwork.py) on a private Docker network, and runs each case in tests/cases the way the GitHub runner would: args built from action.yml, inputs as INPUT_* env vars. It compares the exit code, stderr and every request the mock received with the case's expected file. The expected files record the current behaviour, bugs included, so the fixes that follow show up as changes to them. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 2 + CONTRIBUTING.md | 18 ++ .../closed-merged-lightweight/event.json | 27 +++ .../cases/closed-merged-lightweight/expected | 10 ++ tests/cases/closed-merged-lightweight/inputs | 2 + tests/cases/closed-merged/event.json | 27 +++ tests/cases/closed-merged/expected | 27 +++ tests/cases/closed-merged/inputs | 5 + .../closed-unmerged-lightweight/event.json | 27 +++ .../closed-unmerged-lightweight/expected | 10 ++ .../cases/closed-unmerged-lightweight/inputs | 2 + tests/cases/closed-unmerged/event.json | 27 +++ tests/cases/closed-unmerged/expected | 23 +++ tests/cases/closed-unmerged/inputs | 5 + tests/cases/comment-fails/event.json | 27 +++ tests/cases/comment-fails/expected | 20 +++ tests/cases/comment-fails/inputs | 6 + .../deprecated-board-column-closed/event.json | 27 +++ .../deprecated-board-column-closed/expected | 16 ++ .../deprecated-board-column-closed/inputs | 3 + .../cases/deprecated-board-column/event.json | 27 +++ tests/cases/deprecated-board-column/expected | 16 ++ tests/cases/deprecated-board-column/inputs | 3 + tests/cases/ignored-project/event.json | 27 +++ tests/cases/ignored-project/expected | 3 + tests/cases/ignored-project/inputs | 3 + tests/cases/no-task-links/event.json | 27 +++ tests/cases/no-task-links/expected | 1 + tests/cases/no-task-links/inputs | 5 + tests/cases/opened-lightweight/event.json | 27 +++ tests/cases/opened-lightweight/expected | 20 +++ tests/cases/opened-lightweight/inputs | 6 + tests/cases/opened/event.json | 27 +++ tests/cases/opened/expected | 17 ++ tests/cases/opened/inputs | 5 + .../review-approved-lightweight/event.json | 31 ++++ .../review-approved-lightweight/expected | 14 ++ .../cases/review-approved-lightweight/inputs | 3 + .../cases/review-approved-no-body/event.json | 31 ++++ tests/cases/review-approved-no-body/expected | 11 ++ tests/cases/review-approved-no-body/inputs | 5 + tests/cases/review-approved/event.json | 31 ++++ tests/cases/review-approved/expected | 11 ++ tests/cases/review-approved/inputs | 5 + tests/cases/review-commented/event.json | 31 ++++ tests/cases/review-commented/expected | 3 + tests/cases/review-commented/inputs | 5 + tests/cases/review-dismissed/event.json | 31 ++++ tests/cases/review-dismissed/expected | 10 ++ tests/cases/review-dismissed/inputs | 5 + tests/cases/stage-not-found/event.json | 27 +++ tests/cases/stage-not-found/expected | 12 ++ tests/cases/stage-not-found/inputs | 3 + tests/cases/tagging-disabled/event.json | 27 +++ tests/cases/tagging-disabled/expected | 16 ++ tests/cases/tagging-disabled/inputs | 4 + tests/cases/task-links/event.json | 27 +++ tests/cases/task-links/expected | 73 ++++++++ tests/cases/task-links/inputs | 2 + tests/cases/task-not-found/event.json | 27 +++ tests/cases/task-not-found/expected | 19 +++ tests/cases/task-not-found/inputs | 2 + tests/cases/unsupported-event/event.json | 27 +++ tests/cases/unsupported-event/expected | 3 + tests/cases/unsupported-event/inputs | 5 + tests/cases/wrong-token/event.json | 27 +++ tests/cases/wrong-token/expected | 10 ++ tests/cases/wrong-token/inputs | 3 + tests/mock-teamwork.py | 105 ++++++++++++ tests/run.sh | 157 ++++++++++++++++++ 70 files changed, 1328 insertions(+) create mode 100644 tests/cases/closed-merged-lightweight/event.json create mode 100644 tests/cases/closed-merged-lightweight/expected create mode 100644 tests/cases/closed-merged-lightweight/inputs create mode 100644 tests/cases/closed-merged/event.json create mode 100644 tests/cases/closed-merged/expected create mode 100644 tests/cases/closed-merged/inputs create mode 100644 tests/cases/closed-unmerged-lightweight/event.json create mode 100644 tests/cases/closed-unmerged-lightweight/expected create mode 100644 tests/cases/closed-unmerged-lightweight/inputs create mode 100644 tests/cases/closed-unmerged/event.json create mode 100644 tests/cases/closed-unmerged/expected create mode 100644 tests/cases/closed-unmerged/inputs create mode 100644 tests/cases/comment-fails/event.json create mode 100644 tests/cases/comment-fails/expected create mode 100644 tests/cases/comment-fails/inputs create mode 100644 tests/cases/deprecated-board-column-closed/event.json create mode 100644 tests/cases/deprecated-board-column-closed/expected create mode 100644 tests/cases/deprecated-board-column-closed/inputs create mode 100644 tests/cases/deprecated-board-column/event.json create mode 100644 tests/cases/deprecated-board-column/expected create mode 100644 tests/cases/deprecated-board-column/inputs create mode 100644 tests/cases/ignored-project/event.json create mode 100644 tests/cases/ignored-project/expected create mode 100644 tests/cases/ignored-project/inputs create mode 100644 tests/cases/no-task-links/event.json create mode 100644 tests/cases/no-task-links/expected create mode 100644 tests/cases/no-task-links/inputs create mode 100644 tests/cases/opened-lightweight/event.json create mode 100644 tests/cases/opened-lightweight/expected create mode 100644 tests/cases/opened-lightweight/inputs create mode 100644 tests/cases/opened/event.json create mode 100644 tests/cases/opened/expected create mode 100644 tests/cases/opened/inputs create mode 100644 tests/cases/review-approved-lightweight/event.json create mode 100644 tests/cases/review-approved-lightweight/expected create mode 100644 tests/cases/review-approved-lightweight/inputs create mode 100644 tests/cases/review-approved-no-body/event.json create mode 100644 tests/cases/review-approved-no-body/expected create mode 100644 tests/cases/review-approved-no-body/inputs create mode 100644 tests/cases/review-approved/event.json create mode 100644 tests/cases/review-approved/expected create mode 100644 tests/cases/review-approved/inputs create mode 100644 tests/cases/review-commented/event.json create mode 100644 tests/cases/review-commented/expected create mode 100644 tests/cases/review-commented/inputs create mode 100644 tests/cases/review-dismissed/event.json create mode 100644 tests/cases/review-dismissed/expected create mode 100644 tests/cases/review-dismissed/inputs create mode 100644 tests/cases/stage-not-found/event.json create mode 100644 tests/cases/stage-not-found/expected create mode 100644 tests/cases/stage-not-found/inputs create mode 100644 tests/cases/tagging-disabled/event.json create mode 100644 tests/cases/tagging-disabled/expected create mode 100644 tests/cases/tagging-disabled/inputs create mode 100644 tests/cases/task-links/event.json create mode 100644 tests/cases/task-links/expected create mode 100644 tests/cases/task-links/inputs create mode 100644 tests/cases/task-not-found/event.json create mode 100644 tests/cases/task-not-found/expected create mode 100644 tests/cases/task-not-found/inputs create mode 100644 tests/cases/unsupported-event/event.json create mode 100644 tests/cases/unsupported-event/expected create mode 100644 tests/cases/unsupported-event/inputs create mode 100644 tests/cases/wrong-token/event.json create mode 100644 tests/cases/wrong-token/expected create mode 100644 tests/cases/wrong-token/inputs create mode 100644 tests/mock-teamwork.py create mode 100755 tests/run.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f6ea6c9..f7037cdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,3 +20,5 @@ jobs: WORKFLOW_STAGE_OPENED: "PR Open" WORKFLOW_STAGE_MERGED: "Ready to Test" WORKFLOW_STAGE_CLOSED: "Rejected" + - name: Run the tests against a mock Teamwork API + run: ./tests/run.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e142fd0f..aca161b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,24 @@ * [Install Docker](https://docs.docker.com/get-docker/) * [Install act tool](https://github.com/nektos/act) +## Running the tests + +The tests run the action's Docker image against a mock Teamwork API (`tests/mock-teamwork.py`) and check what it sends: + +``` +./tests/run.sh +``` + +Each directory in `tests/cases` is a test case: + +* `event.json`: the GitHub event the action receives +* `inputs`: the action's inputs as the `INPUT_*` environment variables GitHub creates for them, plus `GITHUB_EVENT_NAME` and any other environment variables such as `IGNORE_PROJECT_IDS` +* `expected`: the exit code, errors and requests the mock Teamwork API should end up with + +`./tests/run.sh ` only runs the cases whose name contains the filter. + +To add a case, create its `event.json` and `inputs`, run `./tests/run.sh --update ` to record its `expected` file, and check that file is right before committing it. The mock's data (which tasks exist, their projects and the workflow stages) is at the top of `tests/mock-teamwork.py`. + ## Usage Simulate a new pull request diff --git a/tests/cases/closed-merged-lightweight/event.json b/tests/cases/closed-merged-lightweight/event.json new file mode 100644 index 00000000..97732713 --- /dev/null +++ b/tests/cases/closed-merged-lightweight/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": true + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/closed-merged-lightweight/expected b/tests/cases/closed-merged-lightweight/expected new file mode 100644 index 00000000..3c02d13c --- /dev/null +++ b/tests/cases/closed-merged-lightweight/expected @@ -0,0 +1,10 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) merged by **octocat**" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/closed-merged-lightweight/inputs b/tests/cases/closed-merged-lightweight/inputs new file mode 100644 index 00000000..76857bf4 --- /dev/null +++ b/tests/cases/closed-merged-lightweight/inputs @@ -0,0 +1,2 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/closed-merged/event.json b/tests/cases/closed-merged/event.json new file mode 100644 index 00000000..97732713 --- /dev/null +++ b/tests/cases/closed-merged/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": true + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/closed-merged/expected b/tests/cases/closed-merged/expected new file mode 100644 index 00000000..5fb0536e --- /dev/null +++ b/tests/cases/closed-merged/expected @@ -0,0 +1,27 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 400 + invalid JSON: Invalid control character at: line 1 column 25 (char 24) + "{ \"comment\": { \"body\": \"\n**octocat** merged a PR: **Add OAuth login**\n[https://github.com/acme/webapp/pull/42](https://github.com/acme/webapp/pull/42)\n\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Merged" + +PUT /tasks/1001/tags.json -> 200 + removeProvidedTags: "true" + tags: + content: "PR Open" + +PUT /tasks/1001/tags.json -> 200 + removeProvidedTags: "true" + tags: + content: "PR Approved" + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 73 diff --git a/tests/cases/closed-merged/inputs b/tests/cases/closed-merged/inputs new file mode 100644 index 00000000..9d43a813 --- /dev/null +++ b/tests/cases/closed-merged/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/closed-unmerged-lightweight/event.json b/tests/cases/closed-unmerged-lightweight/event.json new file mode 100644 index 00000000..09ccdcd9 --- /dev/null +++ b/tests/cases/closed-unmerged-lightweight/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/closed-unmerged-lightweight/expected b/tests/cases/closed-unmerged-lightweight/expected new file mode 100644 index 00000000..e4430984 --- /dev/null +++ b/tests/cases/closed-unmerged-lightweight/expected @@ -0,0 +1,10 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) closed without merging by **octocat**" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/closed-unmerged-lightweight/inputs b/tests/cases/closed-unmerged-lightweight/inputs new file mode 100644 index 00000000..76857bf4 --- /dev/null +++ b/tests/cases/closed-unmerged-lightweight/inputs @@ -0,0 +1,2 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/closed-unmerged/event.json b/tests/cases/closed-unmerged/event.json new file mode 100644 index 00000000..09ccdcd9 --- /dev/null +++ b/tests/cases/closed-unmerged/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/closed-unmerged/expected b/tests/cases/closed-unmerged/expected new file mode 100644 index 00000000..ab0f25ce --- /dev/null +++ b/tests/cases/closed-unmerged/expected @@ -0,0 +1,23 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 400 + invalid JSON: Invalid control character at: line 1 column 25 (char 24) + "{ \"comment\": { \"body\": \"\n**octocat** closed a PR without merging: **Add OAuth login**\n[https://github.com/acme/webapp/pull/42](https://github.com/acme/webapp/pull/42)\n\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" + +PUT /tasks/1001/tags.json -> 200 + removeProvidedTags: "true" + tags: + content: "PR Open" + +PUT /tasks/1001/tags.json -> 200 + removeProvidedTags: "true" + tags: + content: "PR Approved" + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 74 diff --git a/tests/cases/closed-unmerged/inputs b/tests/cases/closed-unmerged/inputs new file mode 100644 index 00000000..9d43a813 --- /dev/null +++ b/tests/cases/closed-unmerged/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/comment-fails/event.json b/tests/cases/comment-fails/event.json new file mode 100644 index 00000000..dd7b97e2 --- /dev/null +++ b/tests/cases/comment-fails/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1003", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/comment-fails/expected b/tests/cases/comment-fails/expected new file mode 100644 index 00000000..faeb3aba --- /dev/null +++ b/tests/cases/comment-fails/expected @@ -0,0 +1,20 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1003.json -> 200 + +POST /tasks/1003/comments.json -> 500 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +PUT /tasks/1003/tags.json -> 200 + tags: + content: "PR Open" + +GET /projects/api/v3/workflows.json?projectIds=300&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1003/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 72 diff --git a/tests/cases/comment-fails/inputs b/tests/cases/comment-fails/inputs new file mode 100644 index 00000000..8a3fe736 --- /dev/null +++ b/tests/cases/comment-fails/inputs @@ -0,0 +1,6 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/deprecated-board-column-closed/event.json b/tests/cases/deprecated-board-column-closed/event.json new file mode 100644 index 00000000..09ccdcd9 --- /dev/null +++ b/tests/cases/deprecated-board-column-closed/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/deprecated-board-column-closed/expected b/tests/cases/deprecated-board-column-closed/expected new file mode 100644 index 00000000..3d2d9efc --- /dev/null +++ b/tests/cases/deprecated-board-column-closed/expected @@ -0,0 +1,16 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) closed without merging by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 74 diff --git a/tests/cases/deprecated-board-column-closed/inputs b/tests/cases/deprecated-board-column-closed/inputs new file mode 100644 index 00000000..3a803811 --- /dev/null +++ b/tests/cases/deprecated-board-column-closed/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true +INPUT_BOARD_COLUMN_CLOSED=Rejected diff --git a/tests/cases/deprecated-board-column/event.json b/tests/cases/deprecated-board-column/event.json new file mode 100644 index 00000000..5f80ac3b --- /dev/null +++ b/tests/cases/deprecated-board-column/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/deprecated-board-column/expected b/tests/cases/deprecated-board-column/expected new file mode 100644 index 00000000..395ab4d1 --- /dev/null +++ b/tests/cases/deprecated-board-column/expected @@ -0,0 +1,16 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 73 diff --git a/tests/cases/deprecated-board-column/inputs b/tests/cases/deprecated-board-column/inputs new file mode 100644 index 00000000..28f49173 --- /dev/null +++ b/tests/cases/deprecated-board-column/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true +INPUT_BOARD_COLUMN_OPENED=Ready to Test diff --git a/tests/cases/ignored-project/event.json b/tests/cases/ignored-project/event.json new file mode 100644 index 00000000..a68d412c --- /dev/null +++ b/tests/cases/ignored-project/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Tasks: http://teamwork:8080/app/tasks/1001 and http://teamwork:8080/app/tasks/1002", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/ignored-project/expected b/tests/cases/ignored-project/expected new file mode 100644 index 00000000..19f4cbd7 --- /dev/null +++ b/tests/cases/ignored-project/expected @@ -0,0 +1,3 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 diff --git a/tests/cases/ignored-project/inputs b/tests/cases/ignored-project/inputs new file mode 100644 index 00000000..db11e710 --- /dev/null +++ b/tests/cases/ignored-project/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true +IGNORE_PROJECT_IDS=100 300 diff --git a/tests/cases/no-task-links/event.json b/tests/cases/no-task-links/event.json new file mode 100644 index 00000000..4517e6e7 --- /dev/null +++ b/tests/cases/no-task-links/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Refactors the login form.", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/no-task-links/expected b/tests/cases/no-task-links/expected new file mode 100644 index 00000000..01fe490f --- /dev/null +++ b/tests/cases/no-task-links/expected @@ -0,0 +1 @@ +exit code: 0 diff --git a/tests/cases/no-task-links/inputs b/tests/cases/no-task-links/inputs new file mode 100644 index 00000000..9d43a813 --- /dev/null +++ b/tests/cases/no-task-links/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/opened-lightweight/event.json b/tests/cases/opened-lightweight/event.json new file mode 100644 index 00000000..d524f93d --- /dev/null +++ b/tests/cases/opened-lightweight/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Handle \"null\" in C:\\temp", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/opened-lightweight/expected b/tests/cases/opened-lightweight/expected new file mode 100644 index 00000000..55e3f681 --- /dev/null +++ b/tests/cases/opened-lightweight/expected @@ -0,0 +1,20 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Handle null in C:\temp**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Open" + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 72 diff --git a/tests/cases/opened-lightweight/inputs b/tests/cases/opened-lightweight/inputs new file mode 100644 index 00000000..8a3fe736 --- /dev/null +++ b/tests/cases/opened-lightweight/inputs @@ -0,0 +1,6 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/opened/event.json b/tests/cases/opened/event.json new file mode 100644 index 00000000..f3579555 --- /dev/null +++ b/tests/cases/opened/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add \"OAuth\" login", + "body": "## Summary\r\nAdds OAuth login for http://teamwork:8080/app/tasks/1001\r\n\r\n```sh\r\n# configure the \"client\" secret\r\nexport CONFIG_DIR=C:\\Users\\dev\\config\r\n```", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/opened/expected b/tests/cases/opened/expected new file mode 100644 index 00000000..e550bec5 --- /dev/null +++ b/tests/cases/opened/expected @@ -0,0 +1,17 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 400 + invalid JSON: Invalid control character at: line 1 column 25 (char 24) + "{ \"comment\": { \"body\": \"\n**octocat** opened a PR: **Add OAuth login**\n[https://github.com/acme/webapp/pull/42](https://github.com/acme/webapp/pull/42)\n`main` \u2b05\ufe0f `feature/oauth`\n\n---\n\n#### Summary\r\nAdds OAuth login for http://teamwork:8080/app/tasks/1001\r\n\r\n```sh\r\n#### configure the client secret\r\nexport CONFIG_DIR=C:\\Users\\dev\\config\r\n```\n\n---\n\n\ud83d\udd22 3 commits / \ud83d\udcdd 5 files updated / \u2795 120 additions / \u2796 8 deletions\n\n\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Open" + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 72 diff --git a/tests/cases/opened/inputs b/tests/cases/opened/inputs new file mode 100644 index 00000000..9d43a813 --- /dev/null +++ b/tests/cases/opened/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/review-approved-lightweight/event.json b/tests/cases/review-approved-lightweight/event.json new file mode 100644 index 00000000..48e8f8f7 --- /dev/null +++ b/tests/cases/review-approved-lightweight/event.json @@ -0,0 +1,31 @@ +{ + "action": "submitted", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + }, + "review": { + "state": "approved", + "body": "Looks good!" + } +} diff --git a/tests/cases/review-approved-lightweight/expected b/tests/cases/review-approved-lightweight/expected new file mode 100644 index 00000000..9d4207f1 --- /dev/null +++ b/tests/cases/review-approved-lightweight/expected @@ -0,0 +1,14 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) approved by **octocat**" + content-type: "text" + isprivate: false + notify: true + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Approved" diff --git a/tests/cases/review-approved-lightweight/inputs b/tests/cases/review-approved-lightweight/inputs new file mode 100644 index 00000000..1f98d2ae --- /dev/null +++ b/tests/cases/review-approved-lightweight/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request_review +INPUT_AUTOMATIC_TAGGING=true +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/review-approved-no-body/event.json b/tests/cases/review-approved-no-body/event.json new file mode 100644 index 00000000..ef9793c8 --- /dev/null +++ b/tests/cases/review-approved-no-body/event.json @@ -0,0 +1,31 @@ +{ + "action": "submitted", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + }, + "review": { + "state": "approved", + "body": null + } +} diff --git a/tests/cases/review-approved-no-body/expected b/tests/cases/review-approved-no-body/expected new file mode 100644 index 00000000..84ff709f --- /dev/null +++ b/tests/cases/review-approved-no-body/expected @@ -0,0 +1,11 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 400 + invalid JSON: Invalid control character at: line 1 column 25 (char 24) + "{ \"comment\": { \"body\": \"\n**octocat** submitted a review to the PR: **Add OAuth login**\n[https://github.com/acme/webapp/pull/42](https://github.com/acme/webapp/pull/42)\n\n---\n\nReview: **approved**\n\n\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Approved" diff --git a/tests/cases/review-approved-no-body/inputs b/tests/cases/review-approved-no-body/inputs new file mode 100644 index 00000000..bf4f6030 --- /dev/null +++ b/tests/cases/review-approved-no-body/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request_review +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/review-approved/event.json b/tests/cases/review-approved/event.json new file mode 100644 index 00000000..3e77ae9d --- /dev/null +++ b/tests/cases/review-approved/event.json @@ -0,0 +1,31 @@ +{ + "action": "submitted", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + }, + "review": { + "state": "approved", + "body": "Looks good!\r\nOne nit: rename `cfg` to \"config\"." + } +} diff --git a/tests/cases/review-approved/expected b/tests/cases/review-approved/expected new file mode 100644 index 00000000..f24cf9cb --- /dev/null +++ b/tests/cases/review-approved/expected @@ -0,0 +1,11 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 400 + invalid JSON: Invalid control character at: line 1 column 25 (char 24) + "{ \"comment\": { \"body\": \"\n**octocat** submitted a review to the PR: **Add OAuth login**\n[https://github.com/acme/webapp/pull/42](https://github.com/acme/webapp/pull/42)\n\n---\n\nReview: **approved**\nLooks good!\r\nOne nit: rename `cfg` to config.\n\", \"notify\": true, \"content-type\": \"text\", \"isprivate\": false } }" + +PUT /tasks/1001/tags.json -> 200 + tags: + content: "PR Approved" diff --git a/tests/cases/review-approved/inputs b/tests/cases/review-approved/inputs new file mode 100644 index 00000000..bf4f6030 --- /dev/null +++ b/tests/cases/review-approved/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request_review +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/review-commented/event.json b/tests/cases/review-commented/event.json new file mode 100644 index 00000000..aa67d482 --- /dev/null +++ b/tests/cases/review-commented/event.json @@ -0,0 +1,31 @@ +{ + "action": "submitted", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + }, + "review": { + "state": "commented", + "body": "Why this change?" + } +} diff --git a/tests/cases/review-commented/expected b/tests/cases/review-commented/expected new file mode 100644 index 00000000..19f4cbd7 --- /dev/null +++ b/tests/cases/review-commented/expected @@ -0,0 +1,3 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 diff --git a/tests/cases/review-commented/inputs b/tests/cases/review-commented/inputs new file mode 100644 index 00000000..bf4f6030 --- /dev/null +++ b/tests/cases/review-commented/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request_review +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/review-dismissed/event.json b/tests/cases/review-dismissed/event.json new file mode 100644 index 00000000..3ad66cf5 --- /dev/null +++ b/tests/cases/review-dismissed/event.json @@ -0,0 +1,31 @@ +{ + "action": "dismissed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + }, + "review": { + "state": "dismissed", + "body": null + } +} diff --git a/tests/cases/review-dismissed/expected b/tests/cases/review-dismissed/expected new file mode 100644 index 00000000..73b63c87 --- /dev/null +++ b/tests/cases/review-dismissed/expected @@ -0,0 +1,10 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "Review dismissed by octocat" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/review-dismissed/inputs b/tests/cases/review-dismissed/inputs new file mode 100644 index 00000000..bf4f6030 --- /dev/null +++ b/tests/cases/review-dismissed/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request_review +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/stage-not-found/event.json b/tests/cases/stage-not-found/event.json new file mode 100644 index 00000000..5f80ac3b --- /dev/null +++ b/tests/cases/stage-not-found/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/stage-not-found/expected b/tests/cases/stage-not-found/expected new file mode 100644 index 00000000..b1796459 --- /dev/null +++ b/tests/cases/stage-not-found/expected @@ -0,0 +1,12 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 diff --git a/tests/cases/stage-not-found/inputs b/tests/cases/stage-not-found/inputs new file mode 100644 index 00000000..c14aee62 --- /dev/null +++ b/tests/cases/stage-not-found/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true +INPUT_WORKFLOW_STAGE_OPENED=Code Review diff --git a/tests/cases/tagging-disabled/event.json b/tests/cases/tagging-disabled/event.json new file mode 100644 index 00000000..97732713 --- /dev/null +++ b/tests/cases/tagging-disabled/event.json @@ -0,0 +1,27 @@ +{ + "action": "closed", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": true + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/tagging-disabled/expected b/tests/cases/tagging-disabled/expected new file mode 100644 index 00000000..c39de2a7 --- /dev/null +++ b/tests/cases/tagging-disabled/expected @@ -0,0 +1,16 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) merged by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v3/workflows.json?projectIds=100&include=stages -> 200 + +PATCH /projects/api/v3/tasks/1001/workflows/7.json -> 204 + positionAfterTask: -1 + stageId: 73 diff --git a/tests/cases/tagging-disabled/inputs b/tests/cases/tagging-disabled/inputs new file mode 100644 index 00000000..60904cd2 --- /dev/null +++ b/tests/cases/tagging-disabled/inputs @@ -0,0 +1,4 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=false +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/task-links/event.json b/tests/cases/task-links/event.json new file mode 100644 index 00000000..94875ab9 --- /dev/null +++ b/tests/cases/task-links/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Linked twice: [http://teamwork:8080/app/tasks/1001](http://teamwork:8080/app/tasks/1001)\nUpper case, old route: HTTP://TEAMWORK:8080/#/tasks/1002\nAgain: http://teamwork:8080/#tasks/1001\nNot task links: subtasks/5, https://github.com/acme/webapp/blob/main/scripts/tasks/2024_cleanup.py\nAnother Teamwork site: https://acme.teamwork.com/app/tasks/1003\nLook-alike host: http://evil-teamwork:8080/app/tasks/1003", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/task-links/expected b/tests/cases/task-links/expected new file mode 100644 index 00000000..8bc306ea --- /dev/null +++ b/tests/cases/task-links/expected @@ -0,0 +1,73 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1002.json -> 200 + +POST /tasks/1002/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/5.json -> 404 + +POST /tasks/5/comments.json -> 404 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/2024.json -> 404 + +POST /tasks/2024/comments.json -> 404 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1003.json -> 200 + +POST /tasks/1003/comments.json -> 500 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1003.json -> 200 + +POST /tasks/1003/comments.json -> 500 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/task-links/inputs b/tests/cases/task-links/inputs new file mode 100644 index 00000000..76857bf4 --- /dev/null +++ b/tests/cases/task-links/inputs @@ -0,0 +1,2 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/task-not-found/event.json b/tests/cases/task-not-found/event.json new file mode 100644 index 00000000..c9ba3a23 --- /dev/null +++ b/tests/cases/task-not-found/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Tasks: http://teamwork:8080/app/tasks/9999 and http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/task-not-found/expected b/tests/cases/task-not-found/expected new file mode 100644 index 00000000..57c82da5 --- /dev/null +++ b/tests/cases/task-not-found/expected @@ -0,0 +1,19 @@ +exit code: 0 + +GET /projects/api/v1/tasks/9999.json -> 404 + +POST /tasks/9999/comments.json -> 404 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true + +GET /projects/api/v1/tasks/1001.json -> 200 + +POST /tasks/1001/comments.json -> 201 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/task-not-found/inputs b/tests/cases/task-not-found/inputs new file mode 100644 index 00000000..76857bf4 --- /dev/null +++ b/tests/cases/task-not-found/inputs @@ -0,0 +1,2 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/cases/unsupported-event/event.json b/tests/cases/unsupported-event/event.json new file mode 100644 index 00000000..da136fb5 --- /dev/null +++ b/tests/cases/unsupported-event/event.json @@ -0,0 +1,27 @@ +{ + "action": "synchronize", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/unsupported-event/expected b/tests/cases/unsupported-event/expected new file mode 100644 index 00000000..19f4cbd7 --- /dev/null +++ b/tests/cases/unsupported-event/expected @@ -0,0 +1,3 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 200 diff --git a/tests/cases/unsupported-event/inputs b/tests/cases/unsupported-event/inputs new file mode 100644 index 00000000..9d43a813 --- /dev/null +++ b/tests/cases/unsupported-event/inputs @@ -0,0 +1,5 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_AUTOMATIC_TAGGING=true +INPUT_WORKFLOW_STAGE_OPENED=PR Open +INPUT_WORKFLOW_STAGE_MERGED=Ready to Test +INPUT_WORKFLOW_STAGE_CLOSED=Rejected diff --git a/tests/cases/wrong-token/event.json b/tests/cases/wrong-token/event.json new file mode 100644 index 00000000..5f80ac3b --- /dev/null +++ b/tests/cases/wrong-token/event.json @@ -0,0 +1,27 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "title": "Add OAuth login", + "body": "Task: http://teamwork:8080/app/tasks/1001", + "html_url": "https://github.com/acme/webapp/pull/42", + "head": { + "ref": "feature/oauth", + "repo": { + "full_name": "acme/webapp" + } + }, + "base": { + "ref": "main" + }, + "commits": 3, + "changed_files": 5, + "additions": 120, + "deletions": 8, + "merged": false + }, + "sender": { + "login": "octocat" + } +} diff --git a/tests/cases/wrong-token/expected b/tests/cases/wrong-token/expected new file mode 100644 index 00000000..a86eb133 --- /dev/null +++ b/tests/cases/wrong-token/expected @@ -0,0 +1,10 @@ +exit code: 0 + +GET /projects/api/v1/tasks/1001.json -> 401 + +POST /tasks/1001/comments.json -> 401 + comment: + body: "PR [**Add OAuth login**](https://github.com/acme/webapp/pull/42) opened by **octocat**" + content-type: "text" + isprivate: false + notify: true diff --git a/tests/cases/wrong-token/inputs b/tests/cases/wrong-token/inputs new file mode 100644 index 00000000..1e001d12 --- /dev/null +++ b/tests/cases/wrong-token/inputs @@ -0,0 +1,3 @@ +GITHUB_EVENT_NAME=pull_request +INPUT_TEAMWORK_API_TOKEN=wrong-token +INPUT_LIGHTWEIGHT_COMMENT=true diff --git a/tests/mock-teamwork.py b/tests/mock-teamwork.py new file mode 100644 index 00000000..2058c2ba --- /dev/null +++ b/tests/mock-teamwork.py @@ -0,0 +1,105 @@ +"""Mock Teamwork API for tests/run.sh. + +Answers from the fixed data below and records every request it receives, in a +readable form, to the file given as its only argument. +""" +import base64 +import json +import re +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer + +LOG_PATH = sys.argv[1] +AUTHORIZATION = "Basic " + base64.b64encode(b"test-token:").decode() + +# Task id -> project id. Requests about any other task fail with a 404. +TASKS = {"1001": 100, "1002": 200, "1003": 300} +# Adding a comment to these tasks fails with a server error. +FAILING_COMMENTS = {"1003"} +# Every project uses the same workflow (id 7), with these stages. +STAGES = { + str(stage_id): {"id": stage_id, "name": name, "workflow": {"id": 7, "type": "workflows"}} + for stage_id, name in [(71, "To Do"), (72, "PR Open"), (73, "Ready to Test"), (74, "Rejected")] +} + + +def render(value, indent=1): + """Renders a JSON body as indented "key: value" lines. Multi-line strings are + shown line by line after a "|" gutter, with control characters such as \\r + shown as their Unicode pictures (␍).""" + pad = " " * indent + if not isinstance(value, dict): + return [pad + json.dumps(value, ensure_ascii=False)] + lines = [] + for key in sorted(value): + item = value[key] + if isinstance(item, dict): + lines += [f"{pad}{key}:", *render(item, indent + 1)] + elif isinstance(item, str) and "\n" in item: + lines.append(f"{pad}{key}:") + for line in item.split("\n"): + line = "".join(chr(0x2400 + ord(c)) if ord(c) < 0x20 else c for c in line) + lines.append(f"{pad} | {line}".rstrip()) + else: + lines.append(f"{pad}{key}: {json.dumps(item, ensure_ascii=False)}") + return lines + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def handle_request(self): + raw = self.rfile.read(int(self.headers.get("Content-Length") or 0)).decode() + status, response, details = self.respond(raw) + # Record before replying, so the log is complete once the action exits + with open(LOG_PATH, "a") as log: + log.write("\n" + "\n".join([f"{self.command} {self.path} -> {status}", *details]) + "\n") + + body = json.dumps(response).encode() if response is not None else b"" + self.send_response(status) + if body: + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + do_GET = do_POST = do_PUT = do_PATCH = handle_request + + def respond(self, raw): + """Returns the status, response body and recorded details of a request.""" + details = [] + if raw: + if not self.headers.get("Content-Type", "").startswith("application/json"): + return 415, {"MESSAGE": "Expected a JSON body"}, [" " + json.dumps(raw)] + try: + details = render(json.loads(raw)) + except ValueError as error: + return 400, {"MESSAGE": "Invalid JSON"}, [f" invalid JSON: {error}", " " + json.dumps(raw)] + + if self.headers.get("Authorization") != AUTHORIZATION: + return 401, {"MESSAGE": "Unauthorized"}, details + + task = re.search(r"/tasks/(\d+)", self.path) + if task and task[1] not in TASKS: + return 404, {"MESSAGE": "Not found"}, details + + request = f"{self.command} {self.path}" + if task := re.fullmatch(r"GET /projects/api/v1/tasks/(\d+)\.json", request): + return 200, {"todo-item": {"id": int(task[1]), "project-id": TASKS[task[1]]}}, details + if re.fullmatch(r"GET /projects/api/v3/workflows\.json\?projectIds=\d+&include=stages", request): + return 200, {"workflows": [{"id": 7, "name": "Development"}], "included": {"stages": STAGES}}, details + if task := re.fullmatch(r"POST /tasks/(\d+)/comments\.json", request): + if task[1] in FAILING_COMMENTS: + return 500, {"MESSAGE": "Internal server error"}, details + return 201, {"STATUS": "OK", "commentId": "1"}, details + if re.fullmatch(r"PUT /tasks/\d+/tags\.json", request): + return 200, {"STATUS": "OK"}, details + if re.fullmatch(r"PATCH /projects/api/v3/tasks/\d+/workflows/7\.json", request): + return 204, None, details + return 404, {"MESSAGE": "Unknown route"}, details + + +server = HTTPServer(("", 8080), Handler) +print("listening", flush=True) +server.serve_forever() diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 00000000..f2cd198c --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Runs the action's Docker image against a mock Teamwork API +# (tests/mock-teamwork.py) for every case in tests/cases, and compares the exit +# code, stderr and requests the mock received with the case's expected file. +# +# A case is a directory with: +# event.json the GitHub event payload +# inputs KEY=VALUE lines: GITHUB_EVENT_NAME, the INPUT_* env vars GitHub +# creates for the action's inputs, and any other env such as +# IGNORE_PROJECT_IDS. Inputs that aren't set are empty. +# expected what the run should produce +# +# Usage: tests/run.sh [--update] [filter] +# --update rewrites the expected files from the current output +# filter only runs the cases whose name contains it +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +IMAGE=github-sync:test +NETWORK=github-sync-test +MOCK=github-sync-teamwork + +update=false +filter= +for arg in "$@"; do + if [ "$arg" == "--update" ]; then + update=true + else + filter=$arg + fi +done + +stop_mock() { + docker rm --force "$MOCK" >/dev/null 2>&1 || true + docker network rm "$NETWORK" >/dev/null 2>&1 || true +} + +work=$(mktemp -d) +trap 'stop_mock; rm -rf "$work"' EXIT + +# The action's args, in action.yml order, as the input each one comes from +# (empty for the '' placeholders). The GitHub runner builds them the same way. +arg_inputs=() +while IFS= read -r item || [ -n "$item" ]; do + case $item in + "''") arg_inputs+=("") ;; + "\${{ inputs."*" }}") + item=${item#"\${{ inputs."} + arg_inputs+=("${item%" }}"}") ;; + *) + echo "Unsupported item in action.yml's runs.args: $item" >&2 + exit 1 ;; + esac +done < <(sed -n '/^ args:/,/^[^ #]/ s/^ - //p' "$ROOT/action.yml") + +# Runs the action for the case in $1 the way GitHub does: inputs as INPUT_* env +# vars, passed by name only, and as positional args built from action.yml. +run_case() ( + local -r dir=$1 + + export INPUT_TEAMWORK_URI=http://teamwork:8080 + export INPUT_TEAMWORK_API_TOKEN=test-token + local env_names=(INPUT_TEAMWORK_URI INPUT_TEAMWORK_API_TOKEN) + local line + while IFS= read -r line || [ -n "$line" ]; do + case $line in ''|'#'*) continue ;; esac + export "${line%%=*}=${line#*=}" + env_names+=("${line%%=*}") + done < "$dir/inputs" + + local docker_args=(--rm --network "$NETWORK" --volume "$dir/event.json:/github/workflow/event.json:ro" + --env GITHUB_REPOSITORY=acme/webapp --env GITHUB_EVENT_PATH=/github/workflow/event.json) + local env_name + for env_name in "${env_names[@]}"; do + docker_args+=(--env "$env_name") + done + + local action_args=() input value + for input in "${arg_inputs[@]}"; do + value= + if [ -n "$input" ]; then + env_name="INPUT_$input" + value=${!env_name:-} + fi + action_args+=("$value") + done + + docker run "${docker_args[@]}" "$IMAGE" "${action_args[@]}" +) + +docker build --quiet --tag "$IMAGE" "$ROOT" >/dev/null + +stop_mock +docker network create "$NETWORK" >/dev/null +docker run --detach --name "$MOCK" --network "$NETWORK" --network-alias teamwork \ + --volume "$ROOT/tests/mock-teamwork.py:/mock-teamwork.py:ro" --volume "$work:/out" \ + python:3.13-alpine python /mock-teamwork.py /out/requests >/dev/null +for _ in $(seq 50); do + if docker logs "$MOCK" 2>&1 | grep -q listening; then + break + fi + sleep 0.1 +done +if ! docker logs "$MOCK" 2>&1 | grep -q listening; then + echo "The mock Teamwork API didn't start:" >&2 + docker logs "$MOCK" >&2 + exit 1 +fi + +ran=0 +failed=0 +for dir in "$ROOT"/tests/cases/*/; do + dir=${dir%/} + name=$(basename "$dir") + case $name in *"$filter"*) ;; *) continue ;; esac + ran=$((ran + 1)) + + : > "$work/requests" + code=0 + run_case "$dir" > "$work/stdout" 2> "$work/stderr" || code=$? + { + echo "exit code: $code" + if [ -s "$work/stderr" ]; then + echo + echo "stderr:" + sed 's/^/ /' "$work/stderr" + fi + cat "$work/requests" + } > "$work/actual" + + if $update; then + cp "$work/actual" "$dir/expected" + echo "updated $name" + elif diff -u "$dir/expected" "$work/actual" > "$work/diff" 2>&1; then + echo "ok $name" + else + failed=$((failed + 1)) + echo "FAIL $name" + sed 's/^/ /' "$work/diff" + echo " action stdout:" + sed 's/^/ /' "$work/stdout" + fi +done + +echo +if ((ran == 0)); then + echo "No cases match '$filter'" + exit 1 +fi +if $update; then + echo "Updated $ran expected file(s), review them before committing" +elif ((failed > 0)); then + echo "$failed of $ran case(s) failed" + exit 1 +else + echo "All $ran case(s) passed" +fi