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/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 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