hephaestus 0.8.11 → 0.8.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +3 -1
  5. data/bin/hephaestus +37 -19
  6. data/lib/hephaestus/app_builder.rb +23 -145
  7. data/lib/hephaestus/app_name.rb +33 -0
  8. data/lib/hephaestus/generators/app_generator.rb +79 -75
  9. data/lib/hephaestus/generators/base.rb +0 -1
  10. data/lib/hephaestus/generators/config_generator.rb +3 -114
  11. data/lib/hephaestus/generators/core_generator.rb +18 -58
  12. data/lib/hephaestus/generators/db_generator.rb +12 -0
  13. data/lib/hephaestus/generators/deployment_generator.rb +3 -8
  14. data/lib/hephaestus/generators/lib_generator.rb +0 -10
  15. data/lib/hephaestus/generators/license_generator.rb +4 -1
  16. data/lib/hephaestus/generators/rubocop_generator.rb +1 -1
  17. data/lib/hephaestus/version.rb +1 -1
  18. data/lib/hephaestus.rb +2 -1
  19. data/templates/Dockerfile +7 -75
  20. data/templates/Gemfile +73 -0
  21. data/templates/Procfile +2 -0
  22. data/templates/app/controllers/app_controller.rb.tt +35 -0
  23. data/templates/app/controllers/application_controller.rb +1 -7
  24. data/templates/app/controllers/concerns/authable.rb.tt +50 -0
  25. data/templates/app/controllers/settings_controller.rb +5 -28
  26. data/templates/app/controllers/yetto_controller.rb +9 -10
  27. data/templates/app/lib/body_parameter/yetto_parameters.rb +8 -29
  28. data/templates/app/lib/{constants/app.rb → constants.rb} +1 -3
  29. data/templates/app/services/{http_service.rb → app_service.rb.tt} +6 -6
  30. data/templates/app/views/settings/new.json.jbuilder.tt +18 -0
  31. data/templates/bin/bundle +115 -0
  32. data/templates/bin/docker-entrypoint +20 -10
  33. data/templates/bin/foreman +27 -0
  34. data/templates/bin/jobs +7 -0
  35. data/templates/bin/rails +6 -0
  36. data/templates/bin/rake +6 -0
  37. data/templates/bin/setup +28 -0
  38. data/templates/bin/tapioca +27 -0
  39. data/templates/config/application.rb.tt +36 -0
  40. data/templates/config/boot.rb +7 -0
  41. data/templates/config/environment.rb +8 -0
  42. data/templates/config/environments/blank.rb +7 -0
  43. data/templates/config/initializers/environment.rb +2 -36
  44. data/templates/config/locales/en.yml +5 -31
  45. data/templates/config/puma.rb +5 -0
  46. data/templates/config/routes.rb.tt +28 -0
  47. data/templates/config.ru +9 -0
  48. data/templates/db/queue_schema.rb +132 -0
  49. data/templates/db/schema.rb +17 -0
  50. data/templates/hephaestus_env.sample +12 -0
  51. data/templates/hephaestus_github/dependabot.yml +27 -0
  52. data/templates/hephaestus_github/workflows/automerge.yml +14 -0
  53. data/templates/hephaestus_github/workflows/deploy.yml +30 -0
  54. data/templates/hephaestus_github/workflows/licenses.yml +17 -0
  55. data/templates/hephaestus_github/workflows/lint.yml +17 -0
  56. data/templates/hephaestus_github/workflows/security.yml +19 -0
  57. data/templates/hephaestus_github/workflows/sorbet.yml +19 -0
  58. data/templates/hephaestus_github/workflows/test.yml.tt +21 -0
  59. data/templates/hephaestus_github/workflows/updater.yml +18 -0
  60. data/templates/hephaestus_vscode/extensions.json +9 -0
  61. data/templates/hephaestus_vscode/launch.json +13 -0
  62. data/templates/hephaestus_vscode/settings.json +58 -0
  63. data/templates/lib/schemas/api/2023-03-06/components/parameters/headers/yetto.json +42 -0
  64. data/templates/lib/schemas/api/2023-03-06/components/parameters/plugInstallation.json +12 -0
  65. data/templates/lib/schemas/api/2023-03-06/components/schemas/plug.json +9 -0
  66. data/templates/lib/schemas/api/2023-03-06/components/schemas/responses.json +64 -0
  67. data/templates/lib/schemas/api/2023-03-06/components/schemas/yetto.json +116 -0
  68. data/templates/lib/schemas/api/2023-03-06/openapi.json +30 -0
  69. data/templates/lib/schemas/api/2023-03-06/paths/app.json +90 -0
  70. data/templates/lib/schemas/api/2023-03-06/paths/yetto/message_created.json +51 -0
  71. data/templates/lib/schemas/api/2023-03-06/paths/yetto/plug_installation_created.json +51 -0
  72. data/templates/script/docker-build-prod.tt +11 -0
  73. data/templates/script/docker-run.tt +8 -0
  74. data/templates/script/edit-credentials +12 -3
  75. data/templates/script/hmac_text +1 -1
  76. data/templates/script/ngrok.tt +7 -0
  77. data/templates/script/server +6 -45
  78. data/templates/test/controllers/app_controller_test.rb.tt +188 -0
  79. data/templates/test/controllers/settings_controller_test.rb.tt +125 -0
  80. data/templates/test/controllers/yetto_controller_test.rb +100 -71
  81. data/templates/test/fixtures/files/plug_installation_settings/valid.json +1 -1
  82. data/templates/test/support/rails.rb +16 -36
  83. data/templates/test/support/webmocks/app_webmock.rb.tt +29 -0
  84. data/templates/test/test_helper.rb +1 -31
  85. data/templates/vendor/fly/{fly-production.toml → fly-production.toml.tt} +24 -11
  86. data/templates/vendor/fly/{fly-staging.toml → fly-staging.toml.tt} +18 -15
  87. metadata +59 -72
  88. data/lib/hephaestus/exit_on_failure.rb +0 -22
  89. data/templates/Gemfile.erb +0 -120
  90. data/templates/Procfile.debug +0 -2
  91. data/templates/Procfile.dev +0 -2
  92. data/templates/app/controllers/app_controller.rb +0 -72
  93. data/templates/app/controllers/concerns/authable.rb +0 -50
  94. data/templates/app/controllers/staff_controller.rb +0 -15
  95. data/templates/app/jobs/update_yetto_job.rb +0 -26
  96. data/templates/app/lib/headers/yetto.rb +0 -19
  97. data/templates/app/lib/headers.rb +0 -5
  98. data/templates/app/lib/path_parameter/settings_parameters.rb +0 -22
  99. data/templates/app/lib/path_parameter/yetto_parameters.rb +0 -28
  100. data/templates/app/lib/path_parameter.rb +0 -8
  101. data/templates/app/lib/plug_app/http.rb +0 -37
  102. data/templates/app/lib/plug_app/middleware/malformed_request.rb +0 -110
  103. data/templates/app/lib/plug_app/middleware/openapi_validation.rb +0 -83
  104. data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +0 -46
  105. data/templates/app/lib/query_parameter.rb +0 -6
  106. data/templates/app/serializers/error_serializer.rb +0 -16
  107. data/templates/app/services/yetto_service.rb +0 -51
  108. data/templates/app/views/settings/new.json.jbuilder +0 -21
  109. data/templates/compose.yml +0 -5
  110. data/templates/config/initializers/000-oj.rb +0 -6
  111. data/templates/config/initializers/cors.rb +0 -19
  112. data/templates/config/initializers/filter_parameter_logging.rb +0 -25
  113. data/templates/config/initializers/inflections.rb +0 -20
  114. data/templates/config/initializers/lograge.rb +0 -25
  115. data/templates/config/initializers/opentelemetry.rb +0 -32
  116. data/templates/config/initializers/sidekiq.rb +0 -11
  117. data/templates/config/initializers/slack_webhook_logger.rb +0 -17
  118. data/templates/config/sidekiq.yml +0 -20
  119. data/templates/script/ngrok +0 -5
  120. data/templates/test/controllers/settings_controller_test.rb +0 -27
  121. data/templates/test/fixtures/plug_installation_settings/invalid.json +0 -3
  122. data/templates/test/fixtures/plug_installation_settings/valid.json +0 -3
  123. data/templates/test/jobs/update_yetto_job_test.rb +0 -26
  124. data/templates/test/support/api.rb +0 -76
  125. data/templates/test/support/webmocks/slack_webmock.rb +0 -24
  126. data/templates/test/support/webmocks/yetto_webmock.rb +0 -119
  127. data/templates/test/support/webmocks.rb +0 -5
@@ -0,0 +1,116 @@
1
+ {
2
+ "CreateAppMessage": {
3
+ "type": "object",
4
+ "required": [
5
+ "yetto"
6
+ ],
7
+ "properties": {
8
+ "yetto": {
9
+ "type": "object",
10
+ "required": [
11
+ "plug_installation",
12
+ "message"
13
+ ],
14
+ "properties": {
15
+ "plug_installation": {
16
+ "type": "object",
17
+ "required": [
18
+ "id",
19
+ "settings"
20
+ ],
21
+ "properties": {
22
+ "id": {
23
+ "type": "string",
24
+ "pattern": "^(?:pli)_[A-Z0-9]{26}$"
25
+ },
26
+ "settings": {
27
+ "type": "object",
28
+ "required": [],
29
+ "properties": {}
30
+ }
31
+ }
32
+ },
33
+ "message": {
34
+ "type": "object",
35
+ "required": [
36
+ "id",
37
+ "conversation",
38
+ "text_content",
39
+ "metadata"
40
+ ],
41
+ "properties": {
42
+ "id": {
43
+ "type": "string",
44
+ "pattern": "^(?:msg)_[A-Z0-9]{26}$"
45
+ },
46
+ "text_content": {
47
+ "type": "string"
48
+ },
49
+ "metadata": {
50
+ "type": "object"
51
+ },
52
+ "conversation": {
53
+ "type": "object",
54
+ "required": [
55
+ "id",
56
+ "title",
57
+ "metadata"
58
+ ],
59
+ "properties": {
60
+ "id": {
61
+ "type": "string",
62
+ "pattern": "^(?:cnv)_[A-Z0-9]{26}$"
63
+ },
64
+ "title": {
65
+ "type": "string"
66
+ },
67
+ "metadata": {
68
+ "type": "object"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ },
75
+ "additionalProperties": false
76
+ }
77
+ },
78
+ "additionalProperties": false
79
+ },
80
+ "ConfigureApp": {
81
+ "type": "object",
82
+ "required": [
83
+ "yetto"
84
+ ],
85
+ "properties": {
86
+ "yetto": {
87
+ "type": "object",
88
+ "required": [
89
+ "plug_installation"
90
+ ],
91
+ "properties": {
92
+ "plug_installation": {
93
+ "type": "object",
94
+ "required": [
95
+ "id",
96
+ "settings"
97
+ ],
98
+ "properties": {
99
+ "id": {
100
+ "type": "string",
101
+ "pattern": "^(?:pli)_[A-Z0-9]{26}$"
102
+ },
103
+ "settings": {
104
+ "type": "object",
105
+ "required": [],
106
+ "properties": {}
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "additionalProperties": false
112
+ }
113
+ },
114
+ "additionalProperties": false
115
+ }
116
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "title": "App Plug API",
5
+ "description": "An OpenAPI definition for the App Plug REST API.",
6
+ "version": "2023-03-06",
7
+ "termsOfService": "https://yetto.app/terms/",
8
+ "license": {
9
+ "name": "Apache 2.0",
10
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
11
+ }
12
+ },
13
+ "servers": [
14
+ {
15
+ "url": "https://yetto.app/2023-03-06",
16
+ "description": "Production basepath"
17
+ }
18
+ ],
19
+ "paths": {
20
+ "/api/2023-03-06/plug_installation/created": {
21
+ "$ref": "paths/yetto/plug_installation_created.json"
22
+ },
23
+ "/api/2023-03-06/message/created": {
24
+ "$ref": "paths/yetto/message_created.json"
25
+ },
26
+ "/app/2023-03-06/{plugInstallationId}": {
27
+ "$ref": "paths/app.json"
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,90 @@
1
+ {
2
+ "post": {
3
+ "tags": [
4
+ "Inbound",
5
+ "App"
6
+ ],
7
+ "description": "This represents something",
8
+ "operationId": "Post a Yetto message",
9
+ "parameters": [
10
+ {
11
+ "$ref": "../components/parameters/plugInstallation.json#/plugInstallationId"
12
+ }
13
+ ],
14
+ "requestBody": {
15
+ "required": true,
16
+ "content": {
17
+ "application/json": {
18
+ "schema": {
19
+ "$ref": "../components/schemas/plug.json#/something"
20
+ }
21
+ },
22
+ "example": {
23
+ "message": {
24
+ "text_content": "Hello _World_",
25
+ "is_public": true,
26
+ "author": {
27
+ "name": "John Doe"
28
+ },
29
+ "metadata": {}
30
+ },
31
+ "creator": {
32
+ "id": "usr_1234567890"
33
+ }
34
+ }
35
+ }
36
+ },
37
+ "responses": {
38
+ "202": {
39
+ "description": "Accepted",
40
+ "content": {
41
+ "application/json": {
42
+ "schema": {
43
+ "$ref": "../components/schemas/responses.json#/202"
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "204": {
49
+ "description": "No Content",
50
+ "content": {
51
+ "application/json": {
52
+ "schema": {
53
+ "$ref": "../components/schemas/responses.json#/204"
54
+ }
55
+ }
56
+ }
57
+ },
58
+ "400": {
59
+ "description": "Bad Request",
60
+ "content": {
61
+ "application/json": {
62
+ "schema": {
63
+ "$ref": "../components/schemas/responses.json#/400"
64
+ }
65
+ }
66
+ }
67
+ },
68
+ "403": {
69
+ "description": "Forbidden",
70
+ "content": {
71
+ "application/json": {
72
+ "schema": {
73
+ "$ref": "../components/schemas/responses.json#/403"
74
+ }
75
+ }
76
+ }
77
+ },
78
+ "503": {
79
+ "description": "Service Unavailable",
80
+ "content": {
81
+ "application/json": {
82
+ "schema": {
83
+ "$ref": "../components/schemas/responses.json#/502"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "post": {
3
+ "tags": [
4
+ "Yetto"
5
+ ],
6
+ "description": "After a message is created in Yetto, this delivers it.",
7
+ "operationId": "Create a new message",
8
+ "requestBody": {
9
+ "required": true,
10
+ "content": {
11
+ "application/json": {
12
+ "schema": {
13
+ "$ref": "../../components/schemas/yetto.json#/CreateAppMessage"
14
+ }
15
+ }
16
+ }
17
+ },
18
+ "responses": {
19
+ "204": {
20
+ "description": "No Content",
21
+ "content": {
22
+ "application/json": {
23
+ "schema": {
24
+ "$ref": "../../components/schemas/responses.json#/204"
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "400": {
30
+ "description": "Bad Request",
31
+ "content": {
32
+ "application/json": {
33
+ "schema": {
34
+ "$ref": "../../components/schemas/responses.json#/400"
35
+ }
36
+ }
37
+ }
38
+ },
39
+ "404": {
40
+ "description": "Not Found",
41
+ "content": {
42
+ "application/json": {
43
+ "schema": {
44
+ "$ref": "../../components/schemas/responses.json#/404"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "post": {
3
+ "tags": [
4
+ "Yetto"
5
+ ],
6
+ "description": "After a plug installation is created, this executes",
7
+ "operationId": "Post a plug installation",
8
+ "requestBody": {
9
+ "required": true,
10
+ "content": {
11
+ "application/json": {
12
+ "schema": {
13
+ "$ref": "../../components/schemas/yetto.json#/ConfigureApp"
14
+ }
15
+ }
16
+ }
17
+ },
18
+ "responses": {
19
+ "204": {
20
+ "description": "No Content",
21
+ "content": {
22
+ "application/json": {
23
+ "schema": {
24
+ "$ref": "../../components/schemas/responses.json#/204"
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "400": {
30
+ "description": "Bad Request",
31
+ "content": {
32
+ "application/json": {
33
+ "schema": {
34
+ "$ref": "../../components/schemas/responses.json#/400"
35
+ }
36
+ }
37
+ }
38
+ },
39
+ "404": {
40
+ "description": "Not Found",
41
+ "content": {
42
+ "application/json": {
43
+ "schema": {
44
+ "$ref": "../../components/schemas/responses.json#/404"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This script will build a production Docker image as closely
4
+ # as it resembles on CI.
5
+
6
+ set -e
7
+
8
+ docker build --build-arg="RAILS_ENV=production" \
9
+ --build-arg="RAILS_MASTER_KEY=$(op read "op://Plug-GitHub/Production Secrets/RAILS_MASTER_KEY")" \
10
+ --build-arg="GIT_SHA:1234" \
11
+ -t yettoapp/plug-<%= plug_name %> . "$@"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This script will build a production Docker image as closely
4
+ # as it resembles on CI.
5
+
6
+ set -e
7
+
8
+ docker run -it yettoapp/plug-<%= plug_name %>:latest bash "$@"
@@ -3,6 +3,7 @@
3
3
  set -e
4
4
 
5
5
  # Initialize flags
6
+ development=false
6
7
  staging=false
7
8
  production=false
8
9
 
@@ -10,6 +11,9 @@ production=false
10
11
  for arg in "$@"
11
12
  do
12
13
  case $arg in
14
+ --development)
15
+ development=true
16
+ ;;
13
17
  --staging)
14
18
  staging=true
15
19
  ;;
@@ -22,8 +26,13 @@ do
22
26
  esac
23
27
  done
24
28
 
25
- if [[ $staging == true ]]; then
26
- RAILS_MASTER_KEY=$(op read op://Plug-App/staging-rails-master-key/...) bin/rails credentials:edit -e staging
29
+ if [[ $development == true ]]; then
30
+ RAILS_MASTER_KEY=$(op read "op://Plug-Email/Development Secrets/Common/RAILS_MASTER_KEY") bin/rails credentials:edit -e development
31
+ elif [[ $staging == true ]]; then
32
+ RAILS_MASTER_KEY=$(op read "op://Plug-Email/Staging Secrets/Common/RAILS_MASTER_KEY") bin/rails credentials:edit -e staging
33
+ elif [[ $production == true ]]; then
34
+ RAILS_MASTER_KEY=$(op read "op://Plug-Email/Production Secrets/Common/RAILS_MASTER_KEY") bin/rails credentials:edit -e production
27
35
  else
28
- RAILS_MASTER_KEY=$(op read op://Plug-App/production-rails-master-key/...) bin/rails credentials:edit -e production
36
+ echo "Please specify a target environment with one of the following flags: --development, --staging, --production"
37
+ exit 1
29
38
  fi
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env rails runner
1
+ #!/usr/bin/env bin/rails runner
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "optparse"
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ hostname="$(hostname | tr '[:upper:]' '[:lower:]' | tr '.' '-')"
6
+
7
+ ngrok http --region=us --domain=${hostname}-plug-<%= plug_name %>.ngrok.io 6661
@@ -2,38 +2,15 @@
2
2
 
3
3
  set -e
4
4
 
5
- # Initialize flags
6
- local_dev=true
7
- api_only=false
8
- debug=false
9
-
10
- function opr_cmd() {
11
- if $local_dev; then
12
- bin/opr $1
13
- else
14
- $1
15
- fi
16
- }
17
-
18
- function cleanup() {
19
- echo "Cleaning up attached services..."
20
- docker compose down
21
- echo "...Done"
22
- }
5
+ # run any pending migrations
6
+ bin/rake db:prepare
23
7
 
24
8
  # Check for and parse flags
25
9
  for arg in "$@"
26
10
  do
27
11
  case $arg in
28
- --api-only)
29
- api_only=true
30
- ;;
31
- --docker)
32
- local_dev=false
33
- ;;
34
12
  --debug)
35
13
  export DEBUG=true
36
- debug=true
37
14
  ;;
38
15
  *)
39
16
  # Unknown arguments can be ignored or handled here
@@ -41,25 +18,9 @@ do
41
18
  esac
42
19
  done
43
20
 
44
- # In non-local dev scenarios, change ownership of the application to the non-root 'plug-app' user
45
- if [[ $local_dev == false ]]; then
46
- chown -R plug-app:plug-app /usr/src/app
47
- else
48
- # In local dev scenarios, run attached compose services in detached mode
49
- docker compose up --wait
21
+ export SERVER_CMD="bin/rails server -p 6661"
22
+ if [[ -n "$DEBUG" ]]; then
23
+ export SERVER_CMD="bundle exec rdbg -n -O -c -- $SERVER_CMD"
50
24
  fi
51
25
 
52
- if $local_dev; then
53
-
54
- # Ensure docker-compose goes down when the server is stopped
55
- trap cleanup SIGINT
56
- trap cleanup SIGTSTP
57
-
58
- if $debug; then
59
- bin/foreman start -f Procfile.debug
60
- else
61
- bin/foreman start -f Procfile.dev
62
- fi
63
- else
64
- bin/puma -C config/puma.rb
65
- fi
26
+ exec overmind start
@@ -0,0 +1,188 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ class <%= capital_plug_name %>ControllerTest < ActionDispatch::IntegrationTest
7
+ include Hephaestus::API::TestHelpers
8
+
9
+ include Webmocks::<%= capital_plug_name %>Webmock
10
+ include Hephaestus::Webmocks::YettoWebmock
11
+ include Hephaestus::Webmocks::SlackWebmock
12
+
13
+ def setup
14
+ @moment = Time.zone.local(2023, 9, 1, 10, 5, 0)
15
+ @update_plug_installation_body = {
16
+ settings: {},
17
+ credentials: {
18
+ "access_token" => "ghu_access_token",
19
+ "expires_at" => "2023-09-01 18:05:00 UTC",
20
+ "refresh_token" => "ghr_",
21
+ "refresh_token_expires_at" => "2024-03-02 10:05:00 UTC",
22
+ "scope" => "",
23
+ "token_type" => "bearer",
24
+ },
25
+ }
26
+
27
+ @valid_issue_comment_created_webhook_body = {
28
+ action: "created",
29
+ repository: {
30
+ id: 8_675_309,
31
+ },
32
+ }
33
+ end
34
+
35
+ def auth_header(body = "")
36
+ "sha256=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), GITHUB_SECRET_TOKEN, body.to_json)}"
37
+ end
38
+
39
+ test "fails gracefully if state param is not present" do
40
+ plug(:get, "/callback")
41
+
42
+ assert_response :bad_request
43
+ end
44
+
45
+ test "fails gracefully if not enough state vars" do
46
+ state = Base64.urlsafe_encode64({ version: "version", salt: "salt" }.to_json)
47
+
48
+ plug(:get, "/callback?state=#{state}")
49
+
50
+ assert_response :bad_request
51
+ end
52
+
53
+ test "fails gracefully if not Base64 encoded" do
54
+ state = 3.times.each_with_object([]) do |_, memo|
55
+ memo << Faker::Alphanumeric.alphanumeric(number: 26).upcase
56
+ end.join(",")
57
+
58
+ plug(:get, "/callback?state=#{state}")
59
+
60
+ assert_response :bad_request
61
+ end
62
+
63
+ test "fails if <%= capital_plug_name %> is not the client requesting callback URL" do
64
+ assert_predicate <%= upcase_plug_name %>_NONCE, :present?
65
+
66
+ state = Base64.urlsafe_encode64(3.times.each_with_object([]) do |_, memo|
67
+ memo << Faker::Alphanumeric.alphanumeric(number: 26).upcase
68
+ end.join(","))
69
+
70
+ plug(:get, "/callback?state=#{state}")
71
+
72
+ assert_response :bad_request
73
+ end
74
+
75
+ test "permits <%= capital_plug_name %> if it's the client requesting callback URL" do
76
+ assert_predicate <%= upcase_plug_name %>_NONCE, :present?
77
+
78
+ redirect_to = Faker::Internet.url(host: "yetto.app")
79
+ state = Base64.urlsafe_encode64({ version: "version", salt: "salt", nonce: <%= upcase_plug_name %>_NONCE, plug_installation_id: "plug_installation_id", redirect_to: redirect_to }.to_json)
80
+
81
+ stub_request_user_access_token(state)
82
+ stub_post_access_token("plug_installation_id")
83
+ stub_update_plug_installation("plug_installation_id", @update_plug_installation_body)
84
+
85
+ Timecop.freeze(@moment) do
86
+ plug(:get, "/callback?state=#{state}", parse: false)
87
+ end
88
+
89
+ assert_equal last_response.headers["Location"], redirect_to
90
+ assert_requested_user_access_token(state)
91
+ assert_requested_post_access_token("plug_installation_id")
92
+ assert_requested_update_plug_installation("plug_installation_id")
93
+ end
94
+
95
+ test "does not panic if <%= capital_plug_name %> responds with invalid JSON" do
96
+ assert_predicate <%= upcase_plug_name %>_NONCE, :present?
97
+
98
+ redirect_to = Faker::Internet.url(host: "yetto.app")
99
+ state = Base64.urlsafe_encode64({ version: "version", salt: "salt", nonce: <%= upcase_plug_name %>_NONCE, plug_installation_id: "plug_installation_id", redirect_to: redirect_to }.to_json)
100
+
101
+ stub_request_user_access_token(state, valid: false)
102
+ stub_send_to_slack_log
103
+ plug(:get, "/callback?state=#{state}")
104
+
105
+ assert_response :not_acceptable
106
+
107
+ assert_requested_send_to_slack_log
108
+ end
109
+
110
+ test "does not panic if <%= capital_plug_name %> responds with bad error status" do
111
+ assert_predicate <%= upcase_plug_name %>_NONCE, :present?
112
+
113
+ redirect_to = Faker::Internet.url(host: "yetto.app")
114
+ state = Base64.urlsafe_encode64({ version: "version", salt: "salt", nonce: <%= upcase_plug_name %>_NONCE, plug_installation_id: "plug_installation_id", redirect_to: redirect_to }.to_json)
115
+
116
+ stub_request_user_access_token(state, status: 500)
117
+ stub_send_to_slack_log
118
+ plug(:get, "/callback?state=#{state}")
119
+
120
+ assert_response :not_acceptable
121
+
122
+ assert_requested_send_to_slack_log
123
+ end
124
+
125
+ test "does not panic if Yetto responds with bad error status when updating" do
126
+ assert_predicate <%= upcase_plug_name %>_NONCE, :present?
127
+
128
+ redirect_to = Faker::Internet.url(host: "yetto.app")
129
+ state = Base64.urlsafe_encode64({ version: "version", salt: "salt", nonce: <%= upcase_plug_name %>_NONCE, plug_installation_id: "plug_installation_id", redirect_to: redirect_to }.to_json)
130
+
131
+ stub_request_user_access_token(state)
132
+ stub_post_access_token("plug_installation_id")
133
+ stub_update_plug_installation("plug_installation_id", @update_plug_installation_body, status: 500)
134
+
135
+ stub_send_to_slack_log
136
+
137
+ moment = Time.zone.local(2023, 9, 1, 10, 5, 0)
138
+ Timecop.freeze(moment) do
139
+ plug(:get, "/callback?state=#{state}")
140
+ end
141
+
142
+ assert_response :not_found
143
+
144
+ assert_requested_user_access_token(state)
145
+
146
+ assert_requested_send_to_slack_log
147
+ end
148
+
149
+ test "webhook handles null headers" do
150
+ plug(:post, "/webhook", body: @valid_issue_comment_created_webhook_body, headers: nil)
151
+
152
+ assert_response :not_found
153
+ end
154
+
155
+ test "webhook handles missing headers" do
156
+ plug(:post, "/webhook", body: @valid_issue_comment_created_webhook_body)
157
+
158
+ assert_response :not_found
159
+ end
160
+
161
+ test "webhook handles incorrect headers" do
162
+ plug(:post, "/webhook", body: @valid_issue_comment_created_webhook_body, headers: { "HTTP_X_HUB_SIGNATURE_256" => "123" })
163
+
164
+ assert_response :bad_request
165
+ end
166
+
167
+ test "webhook handles null body" do
168
+ plug(:post, "/webhook", body: nil, headers: { "HTTP_X_HUB_SIGNATURE_256" => auth_header })
169
+
170
+ assert_response :bad_request
171
+ end
172
+
173
+ test "webhook handles empty body" do
174
+ plug(:post, "/webhook", body: {}, headers: { "HTTP_X_HUB_SIGNATURE_256" => auth_header })
175
+
176
+ assert_response :bad_request
177
+ end
178
+
179
+ test "webhook passes to job when issue_comment is created" do
180
+ assert_enqueued_with(job: Process<%= capital_plug_name %>IssueCommentJob) do
181
+ plug(:post, "/webhook", body: @valid_issue_comment_created_webhook_body, headers: { "HTTP_X_GITHUB_EVENT" => "issue_comment", "HTTP_X_HUB_SIGNATURE_256" => auth_header(@valid_issue_comment_created_webhook_body) })
182
+ end
183
+
184
+ assert_response :ok
185
+ end
186
+
187
+ # TODO: Add your own controller tests here
188
+ end