зеркало из
https://github.com/glebtv/yookassa.git
synced 2026-08-28 15:16:18 +03:00
Add Rails webhook engine, docs, and dependency refresh
Этот коммит содержится в:
21
.github/workflows/main.yml
поставляемый
21
.github/workflows/main.yml
поставляемый
@@ -2,8 +2,6 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ jobs:
|
|||||||
- 3.0
|
- 3.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
@@ -28,4 +26,19 @@ jobs:
|
|||||||
- name: Rubocop
|
- name: Rubocop
|
||||||
run: bundle exec rubocop
|
run: bundle exec rubocop
|
||||||
- name: Rspec
|
- name: Rspec
|
||||||
run: bundle exec rspec
|
run: bundle exec rspec --tag ~browser
|
||||||
|
|
||||||
|
browser-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Set up Chrome
|
||||||
|
uses: browser-actions/setup-chrome@v1
|
||||||
|
- name: Browser specs (Cuprite)
|
||||||
|
run: bundle exec rspec --tag browser
|
||||||
|
|||||||
17
Gemfile
17
Gemfile
@@ -14,13 +14,18 @@ group :documentation do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
gem "capybara", "~> 3.40"
|
||||||
|
gem "cuprite", "~> 0.15"
|
||||||
gem "pry"
|
gem "pry"
|
||||||
gem "pry-byebug", "~> 3.8.0"
|
gem "pry-byebug", "~> 3.10"
|
||||||
gem "rake", "~> 13.0"
|
gem "rake", "~> 13.0"
|
||||||
gem "rspec", "~> 3.5"
|
gem "rack-test", "~> 2.1"
|
||||||
|
gem "rails", "~> 6.1.7"
|
||||||
|
gem "rspec", "~> 3.13"
|
||||||
|
gem "sqlite3", "~> 1.4"
|
||||||
gem "rubocop", "~> 1.22"
|
gem "rubocop", "~> 1.22"
|
||||||
gem "rubocop-rake", "~> 0.6.0"
|
gem "rubocop-rake", "~> 0.7"
|
||||||
gem "rubocop-rspec", "~> 2.5"
|
gem "rubocop-rspec", "~> 2.31"
|
||||||
gem "simplecov", "~> 0.16"
|
gem "simplecov", "~> 0.22"
|
||||||
gem "webmock", "~> 3.14"
|
gem "webmock", "~> 3.23"
|
||||||
end
|
end
|
||||||
|
|||||||
416
Gemfile.lock
416
Gemfile.lock
@@ -1,158 +1,364 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
yookassa (0.2.0)
|
yookassa (0.2.1)
|
||||||
dry-struct
|
dry-struct
|
||||||
dry-validation
|
dry-validation
|
||||||
http (~> 5.0.1)
|
http (>= 5.0, < 6.0)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
addressable (2.8.0)
|
actioncable (6.1.7.10)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
actionpack (= 6.1.7.10)
|
||||||
ast (2.4.2)
|
activesupport (= 6.1.7.10)
|
||||||
byebug (11.1.3)
|
nio4r (~> 2.0)
|
||||||
|
websocket-driver (>= 0.6.1)
|
||||||
|
actionmailbox (6.1.7.10)
|
||||||
|
actionpack (= 6.1.7.10)
|
||||||
|
activejob (= 6.1.7.10)
|
||||||
|
activerecord (= 6.1.7.10)
|
||||||
|
activestorage (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
mail (>= 2.7.1)
|
||||||
|
actionmailer (6.1.7.10)
|
||||||
|
actionpack (= 6.1.7.10)
|
||||||
|
actionview (= 6.1.7.10)
|
||||||
|
activejob (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
mail (~> 2.5, >= 2.5.4)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
actionpack (6.1.7.10)
|
||||||
|
actionview (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
rack (~> 2.0, >= 2.0.9)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
|
actiontext (6.1.7.10)
|
||||||
|
actionpack (= 6.1.7.10)
|
||||||
|
activerecord (= 6.1.7.10)
|
||||||
|
activestorage (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
actionview (6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
builder (~> 3.1)
|
||||||
|
erubi (~> 1.4)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
|
activejob (6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
globalid (>= 0.3.6)
|
||||||
|
activemodel (6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
activerecord (6.1.7.10)
|
||||||
|
activemodel (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
activestorage (6.1.7.10)
|
||||||
|
actionpack (= 6.1.7.10)
|
||||||
|
activejob (= 6.1.7.10)
|
||||||
|
activerecord (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
marcel (~> 1.0)
|
||||||
|
mini_mime (>= 1.1.0)
|
||||||
|
activesupport (6.1.7.10)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
tzinfo (~> 2.0)
|
||||||
|
zeitwerk (~> 2.3)
|
||||||
|
addressable (2.8.9)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
|
ast (2.4.3)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (4.0.1)
|
||||||
|
builder (3.3.0)
|
||||||
|
byebug (13.0.0)
|
||||||
|
reline (>= 0.6.0)
|
||||||
|
capybara (3.40.0)
|
||||||
|
addressable
|
||||||
|
matrix
|
||||||
|
mini_mime (>= 0.1.3)
|
||||||
|
nokogiri (~> 1.11)
|
||||||
|
rack (>= 1.6.0)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
|
xpath (~> 3.2)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.1.9)
|
concurrent-ruby (1.3.6)
|
||||||
crack (0.4.5)
|
crack (1.0.1)
|
||||||
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
diff-lcs (1.4.4)
|
crass (1.0.6)
|
||||||
docile (1.4.0)
|
cuprite (0.17)
|
||||||
domain_name (0.5.20190701)
|
capybara (~> 3.0)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
ferrum (~> 0.17.0)
|
||||||
dry-configurable (0.13.0)
|
date (3.5.1)
|
||||||
|
diff-lcs (1.6.2)
|
||||||
|
docile (1.4.1)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
drb (2.2.3)
|
||||||
|
dry-configurable (1.3.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-core (1.2.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-core (~> 0.6)
|
logger
|
||||||
dry-container (0.9.0)
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.3.1)
|
||||||
|
dry-initializer (3.2.0)
|
||||||
|
dry-logic (1.6.0)
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-configurable (~> 0.13, >= 0.13.0)
|
dry-core (~> 1.1)
|
||||||
dry-core (0.7.1)
|
zeitwerk (~> 2.6)
|
||||||
|
dry-schema (1.15.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-inflector (0.2.1)
|
dry-configurable (~> 1.0, >= 1.0.1)
|
||||||
dry-initializer (3.0.4)
|
dry-core (~> 1.1)
|
||||||
dry-logic (1.2.0)
|
dry-initializer (~> 3.2)
|
||||||
concurrent-ruby (~> 1.0)
|
dry-logic (~> 1.6)
|
||||||
dry-core (~> 0.5, >= 0.5)
|
dry-types (~> 1.8)
|
||||||
dry-schema (1.8.0)
|
zeitwerk (~> 2.6)
|
||||||
concurrent-ruby (~> 1.0)
|
dry-struct (1.8.0)
|
||||||
dry-configurable (~> 0.13, >= 0.13.0)
|
dry-core (~> 1.1)
|
||||||
dry-core (~> 0.5, >= 0.5)
|
dry-types (~> 1.8, >= 1.8.2)
|
||||||
dry-initializer (~> 3.0)
|
|
||||||
dry-logic (~> 1.0)
|
|
||||||
dry-types (~> 1.5)
|
|
||||||
dry-struct (1.4.0)
|
|
||||||
dry-core (~> 0.5, >= 0.5)
|
|
||||||
dry-types (~> 1.5)
|
|
||||||
ice_nine (~> 0.11)
|
ice_nine (~> 0.11)
|
||||||
dry-types (1.5.1)
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.9.1)
|
||||||
|
bigdecimal (>= 3.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-container (~> 0.3)
|
dry-core (~> 1.0)
|
||||||
dry-core (~> 0.5, >= 0.5)
|
dry-inflector (~> 1.0)
|
||||||
dry-inflector (~> 0.1, >= 0.1.2)
|
dry-logic (~> 1.4)
|
||||||
dry-logic (~> 1.0, >= 1.0.2)
|
zeitwerk (~> 2.6)
|
||||||
dry-validation (1.7.0)
|
dry-validation (1.11.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-container (~> 0.7, >= 0.7.1)
|
dry-core (~> 1.1)
|
||||||
dry-core (~> 0.5, >= 0.5)
|
dry-initializer (~> 3.2)
|
||||||
dry-initializer (~> 3.0)
|
dry-schema (~> 1.14)
|
||||||
dry-schema (~> 1.8, >= 1.8.0)
|
zeitwerk (~> 2.6)
|
||||||
ffi (1.15.4)
|
erubi (1.13.1)
|
||||||
ffi-compiler (1.0.1)
|
ferrum (0.17.1)
|
||||||
ffi (>= 1.0.0)
|
addressable (~> 2.5)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
concurrent-ruby (~> 1.1)
|
||||||
|
webrick (~> 1.7)
|
||||||
|
websocket-driver (~> 0.7)
|
||||||
|
ffi (1.17.3)
|
||||||
|
ffi-compiler (1.3.2)
|
||||||
|
ffi (>= 1.15.5)
|
||||||
rake
|
rake
|
||||||
hashdiff (1.0.1)
|
globalid (1.3.0)
|
||||||
http (5.0.4)
|
activesupport (>= 6.1)
|
||||||
|
hashdiff (1.2.1)
|
||||||
|
http (5.3.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
llhttp-ffi (~> 0.4.0)
|
llhttp-ffi (~> 0.5.0)
|
||||||
http-cookie (1.0.4)
|
http-cookie (1.1.0)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
|
i18n (1.14.8)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
ice_nine (0.11.2)
|
ice_nine (0.11.2)
|
||||||
llhttp-ffi (0.4.0)
|
io-console (0.8.2)
|
||||||
|
json (2.18.1)
|
||||||
|
json-schema (6.1.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
bigdecimal (>= 3.1, < 5)
|
||||||
|
language_server-protocol (3.17.0.5)
|
||||||
|
lint_roller (1.1.0)
|
||||||
|
llhttp-ffi (0.5.1)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
method_source (1.0.0)
|
logger (1.7.0)
|
||||||
parallel (1.21.0)
|
loofah (2.25.0)
|
||||||
parser (3.0.2.0)
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.12.0)
|
||||||
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
|
mini_mime (>= 0.1.1)
|
||||||
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
|
marcel (1.1.0)
|
||||||
|
matrix (0.4.3)
|
||||||
|
mcp (0.7.1)
|
||||||
|
json-schema (>= 4.1)
|
||||||
|
method_source (1.1.0)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
minitest (6.0.2)
|
||||||
|
drb (~> 2.0)
|
||||||
|
prism (~> 1.5)
|
||||||
|
net-imap (0.6.3)
|
||||||
|
date
|
||||||
|
net-protocol
|
||||||
|
net-pop (0.1.2)
|
||||||
|
net-protocol
|
||||||
|
net-protocol (0.2.2)
|
||||||
|
timeout
|
||||||
|
net-smtp (0.5.1)
|
||||||
|
net-protocol
|
||||||
|
nio4r (2.7.5)
|
||||||
|
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
parallel (1.27.0)
|
||||||
|
parser (3.3.10.2)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pry (0.14.1)
|
racc
|
||||||
|
prism (1.9.0)
|
||||||
|
pry (0.16.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-byebug (3.8.0)
|
reline (>= 0.6.0)
|
||||||
byebug (~> 11.0)
|
pry-byebug (3.12.0)
|
||||||
pry (~> 0.10)
|
byebug (~> 13.0)
|
||||||
public_suffix (4.0.6)
|
pry (>= 0.13, < 0.17)
|
||||||
rack (2.2.3)
|
public_suffix (7.0.2)
|
||||||
rainbow (3.0.0)
|
racc (1.8.1)
|
||||||
rake (13.0.6)
|
rack (2.2.22)
|
||||||
regexp_parser (2.1.1)
|
rack-test (2.2.0)
|
||||||
rexml (3.2.5)
|
rack (>= 1.3)
|
||||||
rspec (3.10.0)
|
rails (6.1.7.10)
|
||||||
rspec-core (~> 3.10.0)
|
actioncable (= 6.1.7.10)
|
||||||
rspec-expectations (~> 3.10.0)
|
actionmailbox (= 6.1.7.10)
|
||||||
rspec-mocks (~> 3.10.0)
|
actionmailer (= 6.1.7.10)
|
||||||
rspec-core (3.10.1)
|
actionpack (= 6.1.7.10)
|
||||||
rspec-support (~> 3.10.0)
|
actiontext (= 6.1.7.10)
|
||||||
rspec-expectations (3.10.1)
|
actionview (= 6.1.7.10)
|
||||||
|
activejob (= 6.1.7.10)
|
||||||
|
activemodel (= 6.1.7.10)
|
||||||
|
activerecord (= 6.1.7.10)
|
||||||
|
activestorage (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
bundler (>= 1.15.0)
|
||||||
|
railties (= 6.1.7.10)
|
||||||
|
sprockets-rails (>= 2.0.0)
|
||||||
|
rails-dom-testing (2.3.0)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
|
nokogiri (>= 1.6)
|
||||||
|
rails-html-sanitizer (1.7.0)
|
||||||
|
loofah (~> 2.25)
|
||||||
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
|
railties (6.1.7.10)
|
||||||
|
actionpack (= 6.1.7.10)
|
||||||
|
activesupport (= 6.1.7.10)
|
||||||
|
method_source
|
||||||
|
rake (>= 12.2)
|
||||||
|
thor (~> 1.0)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
rake (13.3.1)
|
||||||
|
regexp_parser (2.11.3)
|
||||||
|
reline (0.6.3)
|
||||||
|
io-console (~> 0.5)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rspec (3.13.2)
|
||||||
|
rspec-core (~> 3.13.0)
|
||||||
|
rspec-expectations (~> 3.13.0)
|
||||||
|
rspec-mocks (~> 3.13.0)
|
||||||
|
rspec-core (3.13.6)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.10.2)
|
rspec-mocks (3.13.8)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-support (3.10.2)
|
rspec-support (3.13.7)
|
||||||
rubocop (1.22.3)
|
rubocop (1.85.0)
|
||||||
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
|
mcp (~> 0.6)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml
|
rubocop-ast (>= 1.49.0, < 2.0)
|
||||||
rubocop-ast (>= 1.12.0, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.12.0)
|
rubocop-ast (1.49.0)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.3.7.2)
|
||||||
rubocop-rake (0.6.0)
|
prism (~> 1.7)
|
||||||
rubocop (~> 1.0)
|
rubocop-capybara (2.22.1)
|
||||||
rubocop-rspec (2.5.0)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.19)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
ruby-progressbar (1.11.0)
|
rubocop-factory_bot (2.28.0)
|
||||||
simplecov (0.21.2)
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rake (0.7.1)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (>= 1.72.1)
|
||||||
|
rubocop-rspec (2.31.0)
|
||||||
|
rubocop (~> 1.40)
|
||||||
|
rubocop-capybara (~> 2.17)
|
||||||
|
rubocop-factory_bot (~> 2.22)
|
||||||
|
rubocop-rspec_rails (~> 2.28)
|
||||||
|
rubocop-rspec_rails (2.29.1)
|
||||||
|
rubocop (~> 1.61)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.12.3)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.3)
|
simplecov_json_formatter (0.1.4)
|
||||||
unf (0.1.4)
|
sprockets (4.2.2)
|
||||||
unf_ext
|
concurrent-ruby (~> 1.0)
|
||||||
unf_ext (0.0.8)
|
logger
|
||||||
unicode-display_width (2.1.0)
|
rack (>= 2.2.4, < 4)
|
||||||
webmock (3.14.0)
|
sprockets-rails (3.5.2)
|
||||||
|
actionpack (>= 6.1)
|
||||||
|
activesupport (>= 6.1)
|
||||||
|
sprockets (>= 3.0.0)
|
||||||
|
sqlite3 (1.7.3-x86_64-linux)
|
||||||
|
thor (1.5.0)
|
||||||
|
timeout (0.6.0)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode-display_width (3.2.0)
|
||||||
|
unicode-emoji (~> 4.1)
|
||||||
|
unicode-emoji (4.2.0)
|
||||||
|
webmock (3.26.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.7.0)
|
webrick (1.9.2)
|
||||||
yard (0.9.26)
|
websocket-driver (0.8.0)
|
||||||
|
base64
|
||||||
|
websocket-extensions (>= 0.1.0)
|
||||||
|
websocket-extensions (0.1.5)
|
||||||
|
xpath (3.2.0)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
|
yard (0.9.38)
|
||||||
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
capybara (~> 3.40)
|
||||||
|
cuprite (~> 0.15)
|
||||||
pry
|
pry
|
||||||
pry-byebug (~> 3.8.0)
|
pry-byebug (~> 3.10)
|
||||||
rack
|
rack
|
||||||
|
rack-test (~> 2.1)
|
||||||
|
rails (~> 6.1.7)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
rspec (~> 3.5)
|
rspec (~> 3.13)
|
||||||
rubocop (~> 1.22)
|
rubocop (~> 1.22)
|
||||||
rubocop-rake (~> 0.6.0)
|
rubocop-rake (~> 0.7)
|
||||||
rubocop-rspec (~> 2.5)
|
rubocop-rspec (~> 2.31)
|
||||||
simplecov (~> 0.16)
|
simplecov (~> 0.22)
|
||||||
webmock (~> 3.14)
|
sqlite3 (~> 1.4)
|
||||||
|
webmock (~> 3.23)
|
||||||
webrick
|
webrick
|
||||||
yard
|
yard
|
||||||
yookassa!
|
yookassa!
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.22
|
2.6.7
|
||||||
|
|||||||
71
README.md
71
README.md
@@ -80,6 +80,77 @@ Yookassa.payments.capture(payment_id: '12345')
|
|||||||
Yookassa.payments.cancel(payment_id: '12345')
|
Yookassa.payments.cancel(payment_id: '12345')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rails webhook endpoint (engine)
|
||||||
|
|
||||||
|
The gem ships with a Rails engine and a default webhook controller you can use directly.
|
||||||
|
|
||||||
|
1) Configure the webhook token and (optionally) allowed source IPs:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/initializers/yookassa.rb
|
||||||
|
Yookassa.configure do |config|
|
||||||
|
config.shop_id = ENV.fetch('YOOKASSA_SHOP_ID')
|
||||||
|
config.api_key = ENV.fetch('YOOKASSA_API_KEY')
|
||||||
|
|
||||||
|
# Random, long, secret token used in webhook URL path.
|
||||||
|
config.webhook_token = ENV.fetch('YOOKASSA_WEBHOOK_TOKEN')
|
||||||
|
|
||||||
|
# Optional override. Defaults come from YooKassa docs:
|
||||||
|
# https://yookassa.ru/developers/using-api/webhooks#ip
|
||||||
|
# config.webhook_allowed_ips = ['185.71.76.0/27', ...]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Mount the engine in routes:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
mount Yookassa::Engine => '/yookassa'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This exposes:
|
||||||
|
|
||||||
|
- `POST /yookassa/webhooks/:token`
|
||||||
|
|
||||||
|
Set your YooKassa webhook URL to include the real token value, for example:
|
||||||
|
|
||||||
|
- `https://example.com/yookassa/webhooks/<long-random-token>`
|
||||||
|
|
||||||
|
The default `Yookassa::WebhooksController` verifies:
|
||||||
|
|
||||||
|
- token in URL path
|
||||||
|
- request source `request.remote_ip` is in allowlist
|
||||||
|
- webhook object matches fresh API fetch by `id` and `status`
|
||||||
|
|
||||||
|
No signature headers are used.
|
||||||
|
|
||||||
|
### Overriding webhook handling
|
||||||
|
|
||||||
|
In host app, inherit from the gem controller and implement business logic in `process_webhook`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/controllers/my_yookassa_webhooks_controller.rb
|
||||||
|
class MyYookassaWebhooksController < Yookassa::WebhooksController
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_webhook(payload)
|
||||||
|
object = payload['object'] || payload.dig('data', 'object')
|
||||||
|
return unless object
|
||||||
|
|
||||||
|
# Your app-specific processing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Then route to your controller (keeping token in path):
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
post '/webhooks/yookassa/:token', to: 'my_yookassa_webhooks#create'
|
||||||
|
```
|
||||||
|
|
||||||
### Path to 1.0
|
### Path to 1.0
|
||||||
|
|
||||||
**Настройки SDK API ЮKassa**
|
**Настройки SDK API ЮKassa**
|
||||||
|
|||||||
83
app/controllers/yookassa/webhooks_controller.rb
Обычный файл
83
app/controllers/yookassa/webhooks_controller.rb
Обычный файл
@@ -0,0 +1,83 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "ipaddr"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class WebhooksController < ::ActionController::Base
|
||||||
|
skip_before_action :verify_authenticity_token, raise: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
payload = parsed_payload
|
||||||
|
return head :unauthorized unless authentic_webhook?(payload)
|
||||||
|
|
||||||
|
process_webhook(payload)
|
||||||
|
head :ok
|
||||||
|
rescue JSON::ParserError
|
||||||
|
head :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_webhook(_payload)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentic_webhook?(payload)
|
||||||
|
token_valid? && source_ip_allowed? && payload_matches_api_object?(payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def token_valid?
|
||||||
|
token = params[:token].to_s
|
||||||
|
configured_token = Yookassa.config.webhook_token.to_s
|
||||||
|
return false if token.empty? || configured_token.empty?
|
||||||
|
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(token, configured_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def source_ip_allowed?
|
||||||
|
source_ip = request.remote_ip
|
||||||
|
allowed_cidrs.any? { |cidr| IPAddr.new(cidr).include?(source_ip) }
|
||||||
|
rescue IPAddr::InvalidAddressError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_cidrs
|
||||||
|
ips = Yookassa.config.webhook_allowed_ips
|
||||||
|
return Yookassa::Config::DEFAULT_WEBHOOK_ALLOWED_IPS if ips.nil? || ips.empty?
|
||||||
|
|
||||||
|
ips
|
||||||
|
end
|
||||||
|
|
||||||
|
def parsed_payload
|
||||||
|
@parsed_payload ||= JSON.parse(request.raw_post)
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload_matches_api_object?(payload)
|
||||||
|
object = payload["object"] || payload.dig("data", "object")
|
||||||
|
return false unless object.is_a?(Hash)
|
||||||
|
|
||||||
|
object_id = object["id"].to_s
|
||||||
|
object_status = object["status"].to_s
|
||||||
|
return false if object_id.empty? || object_status.empty?
|
||||||
|
|
||||||
|
fetched_object = fetch_object_from_api(payload, object_id)
|
||||||
|
return false if fetched_object.nil?
|
||||||
|
|
||||||
|
fetched_object.id == object_id && fetched_object.status == object_status
|
||||||
|
rescue StandardError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_object_from_api(payload, object_id)
|
||||||
|
event_name = payload["event"].to_s
|
||||||
|
event_name = payload["type"].to_s if event_name.empty?
|
||||||
|
|
||||||
|
if event_name.start_with?("payment.")
|
||||||
|
Yookassa.payments.find(payment_id: object_id)
|
||||||
|
elsif event_name.start_with?("refund.")
|
||||||
|
Yookassa.refunds.find(payment_id: object_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3
config/routes.rb
Обычный файл
3
config/routes.rb
Обычный файл
@@ -0,0 +1,3 @@
|
|||||||
|
Yookassa::Engine.routes.draw do
|
||||||
|
post "/webhooks/:token", to: "webhooks#create"
|
||||||
|
end
|
||||||
24
docs/changelog.md
Обычный файл
24
docs/changelog.md
Обычный файл
@@ -0,0 +1,24 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Rails engine integration for webhook handling (`Yookassa::Engine`).
|
||||||
|
- Default webhook controller (`Yookassa::WebhooksController`) with built-in authenticity checks.
|
||||||
|
- Config options for webhook security:
|
||||||
|
- `webhook_token` (secret URL token in path)
|
||||||
|
- `webhook_allowed_ips` (YooKassa source allowlist, overridable)
|
||||||
|
- RSpec coverage for webhook controller behavior.
|
||||||
|
- Cuprite browser spec for real browser webhook request flow.
|
||||||
|
- CI job for browser tests in GitHub Actions.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Documentation updated to use gem-based webhook integration instead of app-side hand-rolled service/controller code.
|
||||||
|
- Webhook security guidance switched to YooKassa-documented approach:
|
||||||
|
- secret URL token
|
||||||
|
- source IP allowlist
|
||||||
|
- API re-fetch and object/status comparison
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- No webhook signature header validation is implemented because YooKassa webhook docs describe authenticity checks via source IP and object status verification.
|
||||||
|
- Source for default IP ranges: https://yookassa.ru/developers/using-api/webhooks#ip
|
||||||
135
docs/chaos-yookassa.md
Обычный файл
135
docs/chaos-yookassa.md
Обычный файл
@@ -0,0 +1,135 @@
|
|||||||
|
# YooKassa Smart Payment Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide uses the `yookassa` gem for both API calls and webhook security handling.
|
||||||
|
Do not re-implement webhook signature checks in your app: this integration uses a
|
||||||
|
secret URL token + source IP allowlist + API re-fetch verification.
|
||||||
|
|
||||||
|
## Payment flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User taps "Pay" -> backend creates payment via gem -> user pays on YooKassa page ->
|
||||||
|
YooKassa sends webhook -> gem controller verifies webhook -> app applies business logic
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1) Install and configure the gem
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Gemfile
|
||||||
|
gem "yookassa"
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/initializers/yookassa.rb
|
||||||
|
Yookassa.configure do |config|
|
||||||
|
config.shop_id = ENV.fetch("YOOKASSA_SHOP_ID")
|
||||||
|
config.api_key = ENV.fetch("YOOKASSA_SECRET_KEY")
|
||||||
|
|
||||||
|
# Required: long random token used in webhook route path.
|
||||||
|
config.webhook_token = ENV.fetch("YOOKASSA_WEBHOOK_TOKEN")
|
||||||
|
|
||||||
|
# Optional override, if you need a custom list.
|
||||||
|
# Defaults are from YooKassa docs:
|
||||||
|
# https://yookassa.ru/developers/using-api/webhooks#ip
|
||||||
|
# config.webhook_allowed_ips = ["185.71.76.0/27", ...]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) Create payments with gem client
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
payload = {
|
||||||
|
amount: {
|
||||||
|
value: "100.00",
|
||||||
|
currency: "RUB"
|
||||||
|
},
|
||||||
|
capture: true,
|
||||||
|
confirmation: {
|
||||||
|
type: "redirect",
|
||||||
|
return_url: "https://example.com/payment/return"
|
||||||
|
},
|
||||||
|
description: "Order 123"
|
||||||
|
}
|
||||||
|
|
||||||
|
payment = Yookassa.payments.create(payment: payload)
|
||||||
|
confirmation_url = payment.confirmation.confirmation_url
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Mount gem webhook engine
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
mount Yookassa::Engine => "/yookassa"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Default webhook endpoint:
|
||||||
|
|
||||||
|
- `POST /yookassa/webhooks/:token`
|
||||||
|
|
||||||
|
Your YooKassa dashboard webhook URL must include the configured token value, for example:
|
||||||
|
|
||||||
|
- `https://your-domain.com/yookassa/webhooks/<long-random-token>`
|
||||||
|
|
||||||
|
## 4) Add app-specific webhook logic by inheritance
|
||||||
|
|
||||||
|
`Yookassa::WebhooksController` handles authenticity checks. Override only processing.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/controllers/yookassa_events_controller.rb
|
||||||
|
class YookassaEventsController < Yookassa::WebhooksController
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_webhook(payload)
|
||||||
|
event = payload["event"] || payload["type"]
|
||||||
|
object = payload["object"] || payload.dig("data", "object")
|
||||||
|
return unless object
|
||||||
|
|
||||||
|
case event
|
||||||
|
when "payment.succeeded"
|
||||||
|
# apply balance / mark paid in your app
|
||||||
|
when "payment.canceled"
|
||||||
|
# mark failed in your app
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
post "/webhooks/yookassa/:token", to: "yookassa_events#create"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook security model used by gem
|
||||||
|
|
||||||
|
The default controller accepts webhook only if all checks pass:
|
||||||
|
|
||||||
|
1. route includes token and it matches `Yookassa.config.webhook_token`
|
||||||
|
2. source address from `request.remote_ip` is in allowlist
|
||||||
|
3. webhook object `id` + `status` matches a fresh API fetch
|
||||||
|
|
||||||
|
This approach intentionally does not use HMAC signature headers.
|
||||||
|
|
||||||
|
## Defaults and allowlist override
|
||||||
|
|
||||||
|
Default allowed IP ranges in gem:
|
||||||
|
|
||||||
|
- `185.71.76.0/27`
|
||||||
|
- `185.71.77.0/27`
|
||||||
|
- `77.75.153.0/25`
|
||||||
|
- `77.75.156.11`
|
||||||
|
- `77.75.156.35`
|
||||||
|
- `77.75.154.128/25`
|
||||||
|
- `2a02:5180::/32`
|
||||||
|
|
||||||
|
Source: https://yookassa.ru/developers/using-api/webhooks#ip
|
||||||
|
|
||||||
|
If needed:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Yookassa.configure do |config|
|
||||||
|
config.webhook_allowed_ips = ["203.0.113.10"]
|
||||||
|
end
|
||||||
|
```
|
||||||
70
docs/webhooks.md
Обычный файл
70
docs/webhooks.md
Обычный файл
@@ -0,0 +1,70 @@
|
|||||||
|
# Webhooks
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The gem provides a Rails engine endpoint for YooKassa webhooks and a default controller
|
||||||
|
that performs security checks before running your app-specific logic.
|
||||||
|
|
||||||
|
Default endpoint after mounting engine:
|
||||||
|
|
||||||
|
- `POST /yookassa/webhooks/:token`
|
||||||
|
|
||||||
|
## Security model
|
||||||
|
|
||||||
|
The controller accepts a webhook only if all checks pass:
|
||||||
|
|
||||||
|
1. Route token matches configured `Yookassa.config.webhook_token`.
|
||||||
|
2. Source IP from `request.remote_ip` belongs to allowed CIDRs.
|
||||||
|
3. Webhook object `id` and `status` match a fresh API fetch (`payments.find` or `refunds.find`).
|
||||||
|
|
||||||
|
This follows YooKassa webhook guidance:
|
||||||
|
|
||||||
|
- https://yookassa.ru/developers/using-api/webhooks
|
||||||
|
|
||||||
|
## Default allowed IP ranges
|
||||||
|
|
||||||
|
From YooKassa docs (source: https://yookassa.ru/developers/using-api/webhooks#ip):
|
||||||
|
|
||||||
|
- `185.71.76.0/27`
|
||||||
|
- `185.71.77.0/27`
|
||||||
|
- `77.75.153.0/25`
|
||||||
|
- `77.75.156.11`
|
||||||
|
- `77.75.156.35`
|
||||||
|
- `77.75.154.128/25`
|
||||||
|
- `2a02:5180::/32`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Yookassa.configure do |config|
|
||||||
|
config.shop_id = ENV.fetch("YOOKASSA_SHOP_ID")
|
||||||
|
config.api_key = ENV.fetch("YOOKASSA_API_KEY")
|
||||||
|
config.webhook_token = ENV.fetch("YOOKASSA_WEBHOOK_TOKEN")
|
||||||
|
|
||||||
|
# Optional override
|
||||||
|
# config.webhook_allowed_ips = ["203.0.113.10"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overriding controller behavior
|
||||||
|
|
||||||
|
Inherit from `Yookassa::WebhooksController` and override `process_webhook`.
|
||||||
|
All security checks remain in the base controller.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class MyYookassaWebhooksController < Yookassa::WebhooksController
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_webhook(payload)
|
||||||
|
event = payload["event"]
|
||||||
|
object = payload["object"]
|
||||||
|
# app-specific logic
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Route example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
post "/webhooks/yookassa/:token", to: "my_yookassa_webhooks#create"
|
||||||
|
```
|
||||||
@@ -7,6 +7,7 @@ require "yookassa/config"
|
|||||||
require "yookassa/payments"
|
require "yookassa/payments"
|
||||||
require "yookassa/refunds"
|
require "yookassa/refunds"
|
||||||
require "yookassa/receipts"
|
require "yookassa/receipts"
|
||||||
|
require "yookassa/webhooks"
|
||||||
|
|
||||||
module Yookassa
|
module Yookassa
|
||||||
class << self
|
class << self
|
||||||
@@ -31,5 +32,11 @@ module Yookassa
|
|||||||
def receipts
|
def receipts
|
||||||
@receipts ||= Receipts.new
|
@receipts ||= Receipts.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def webhooks
|
||||||
|
@webhooks ||= Webhooks.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require "yookassa/railtie" if defined?(Rails::Railtie)
|
||||||
|
|||||||
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
module Yookassa
|
module Yookassa
|
||||||
class Config
|
class Config
|
||||||
attr_accessor :shop_id, :api_key
|
# Source: https://yookassa.ru/developers/using-api/webhooks#ip
|
||||||
|
DEFAULT_WEBHOOK_ALLOWED_IPS = [
|
||||||
|
"185.71.76.0/27",
|
||||||
|
"185.71.77.0/27",
|
||||||
|
"77.75.153.0/25",
|
||||||
|
"77.75.156.11",
|
||||||
|
"77.75.156.35",
|
||||||
|
"77.75.154.128/25",
|
||||||
|
"2a02:5180::/32"
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
attr_accessor :shop_id, :api_key, :webhook_token, :webhook_allowed_ips
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@webhook_allowed_ips = DEFAULT_WEBHOOK_ALLOWED_IPS.dup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
7
lib/yookassa/engine.rb
Обычный файл
7
lib/yookassa/engine.rb
Обычный файл
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class Engine < ::Rails::Engine
|
||||||
|
isolate_namespace Yookassa
|
||||||
|
end
|
||||||
|
end
|
||||||
8
lib/yookassa/railtie.rb
Обычный файл
8
lib/yookassa/railtie.rb
Обычный файл
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "yookassa/engine"
|
||||||
|
|
||||||
|
module Yookassa
|
||||||
|
class Railtie < ::Rails::Railtie
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Yookassa
|
module Yookassa
|
||||||
VERSION = "0.2.0"
|
VERSION = "0.2.1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ end
|
|||||||
|
|
||||||
require "yookassa"
|
require "yookassa"
|
||||||
require "webmock/rspec"
|
require "webmock/rspec"
|
||||||
|
WebMock.disable_net_connect!(allow_localhost: true)
|
||||||
|
|
||||||
|
Dir[File.expand_path("support/**/*.rb", __dir__)].sort.each { |file| require file }
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
config.order = :random
|
config.order = :random
|
||||||
config.filter_run focus: true
|
config.filter_run focus: true
|
||||||
config.run_all_when_everything_filtered = true
|
config.run_all_when_everything_filtered = true
|
||||||
|
|
||||||
|
config.before do
|
||||||
|
WebMock.reset!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
102
spec/support/rails_test_app.rb
Обычный файл
102
spec/support/rails_test_app.rb
Обычный файл
@@ -0,0 +1,102 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
begin
|
||||||
|
require "logger"
|
||||||
|
require "rack/test"
|
||||||
|
require "rails"
|
||||||
|
require "action_controller/railtie"
|
||||||
|
require "capybara"
|
||||||
|
require "capybara/rspec"
|
||||||
|
require "capybara/cuprite"
|
||||||
|
rescue LoadError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined?(Rails)
|
||||||
|
require "yookassa/engine"
|
||||||
|
require File.expand_path("../../app/controllers/yookassa/webhooks_controller", __dir__)
|
||||||
|
|
||||||
|
class YookassaSpecBrowserController < ActionController::Base
|
||||||
|
def index
|
||||||
|
render inline: <<~HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<button id="send">Send webhook</button>
|
||||||
|
<div id="result">pending</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById("send").addEventListener("click", function() {
|
||||||
|
fetch("/yookassa/webhooks/<%= Yookassa.config.webhook_token %>", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
event: "payment.succeeded",
|
||||||
|
object: { id: "browser-payment", status: "succeeded" }
|
||||||
|
})
|
||||||
|
}).then(function(response) {
|
||||||
|
document.getElementById("result").textContent = String(response.status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class YookassaSpecCustomWebhooksController < Yookassa::WebhooksController
|
||||||
|
class_attribute :last_payload, default: nil
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_webhook(payload)
|
||||||
|
self.class.last_payload = payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class YookassaSpecApp < Rails::Application
|
||||||
|
config.root = File.expand_path("../..", __dir__)
|
||||||
|
config.eager_load = false
|
||||||
|
config.secret_key_base = "test-secret-key-base"
|
||||||
|
config.logger = Logger.new(nil)
|
||||||
|
config.hosts << "www.example.com"
|
||||||
|
config.hosts << "example.org"
|
||||||
|
config.hosts << "localhost"
|
||||||
|
config.hosts << "127.0.0.1"
|
||||||
|
config.consider_all_requests_local = true
|
||||||
|
end
|
||||||
|
|
||||||
|
YookassaSpecApp.initialize!
|
||||||
|
|
||||||
|
YookassaSpecApp.routes.draw do
|
||||||
|
mount Yookassa::Engine => "/yookassa"
|
||||||
|
post "/custom-yookassa/:token", to: "yookassa_spec_custom_webhooks#create"
|
||||||
|
get "/browser", to: "yookassa_spec_browser#index"
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.app = YookassaSpecApp
|
||||||
|
Capybara.server = :webrick
|
||||||
|
Capybara.default_max_wait_time = 5
|
||||||
|
Capybara.register_driver(:cuprite) do |app|
|
||||||
|
Capybara::Cuprite::Driver.new(app, headless: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include Rack::Test::Methods
|
||||||
|
|
||||||
|
config.define_derived_metadata(file_path: %r{/spec/yookassa/webhooks_}) do |metadata|
|
||||||
|
metadata[:rails] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
config.before(:each, rails: true) do
|
||||||
|
Yookassa.configure do |yookassa_config|
|
||||||
|
yookassa_config.shop_id = "shop-id"
|
||||||
|
yookassa_config.api_key = "api-key"
|
||||||
|
yookassa_config.webhook_token = "secret-token"
|
||||||
|
yookassa_config.webhook_allowed_ips = Yookassa::Config::DEFAULT_WEBHOOK_ALLOWED_IPS.dup
|
||||||
|
end
|
||||||
|
|
||||||
|
YookassaSpecCustomWebhooksController.last_payload = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
29
spec/yookassa/webhooks_browser_spec.rb
Обычный файл
29
spec/yookassa/webhooks_browser_spec.rb
Обычный файл
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Yookassa webhook endpoint", :rails, :browser do
|
||||||
|
include Capybara::DSL
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
previous_driver = Capybara.current_driver
|
||||||
|
Capybara.current_driver = :cuprite
|
||||||
|
example.run
|
||||||
|
Capybara.current_driver = previous_driver
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Yookassa.config.webhook_allowed_ips = ["127.0.0.1"]
|
||||||
|
|
||||||
|
payments_client = instance_double(Yookassa::Payments)
|
||||||
|
allow(Yookassa).to receive(:payments).and_return(payments_client)
|
||||||
|
allow(payments_client).to receive(:find).with(payment_id: "browser-payment")
|
||||||
|
.and_return(instance_double(Yookassa::Entity::Payment, id: "browser-payment", status: "succeeded"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "accepts browser-submitted webhook request" do
|
||||||
|
visit "/browser"
|
||||||
|
|
||||||
|
click_button "Send webhook"
|
||||||
|
|
||||||
|
expect(page).to have_text("200")
|
||||||
|
end
|
||||||
|
end
|
||||||
93
spec/yookassa/webhooks_controller_spec.rb
Обычный файл
93
spec/yookassa/webhooks_controller_spec.rb
Обычный файл
@@ -0,0 +1,93 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe Yookassa::WebhooksController do
|
||||||
|
def app
|
||||||
|
YookassaSpecApp
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:payload_hash) do
|
||||||
|
{
|
||||||
|
"event" => "payment.succeeded",
|
||||||
|
"object" => {
|
||||||
|
"id" => "payment-1",
|
||||||
|
"status" => "succeeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:payload) { JSON.generate(payload_hash) }
|
||||||
|
let(:headers) do
|
||||||
|
{
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"REMOTE_ADDR" => remote_addr
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:remote_addr) { "185.71.76.1" }
|
||||||
|
|
||||||
|
describe "POST /yookassa/webhooks/:token", :rails do
|
||||||
|
it "accepts webhook when token, remote_ip, and API re-fetch checks pass" do
|
||||||
|
payments_client = instance_double(Yookassa::Payments)
|
||||||
|
allow(Yookassa).to receive(:payments).and_return(payments_client)
|
||||||
|
allow(payments_client).to receive(:find).with(payment_id: "payment-1")
|
||||||
|
.and_return(instance_double(Yookassa::Entity::Payment, id: "payment-1", status: "succeeded"))
|
||||||
|
|
||||||
|
post "/yookassa/webhooks/secret-token", payload, headers
|
||||||
|
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects webhook when token is invalid" do
|
||||||
|
post "/yookassa/webhooks/wrong-token", payload, headers
|
||||||
|
|
||||||
|
expect(last_response.status).to eq(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects webhook when source remote_ip is outside allowlist" do
|
||||||
|
payments_client = instance_double(Yookassa::Payments)
|
||||||
|
allow(Yookassa).to receive(:payments).and_return(payments_client)
|
||||||
|
allow(payments_client).to receive(:find).with(payment_id: "payment-1")
|
||||||
|
.and_return(instance_double(Yookassa::Entity::Payment, id: "payment-1", status: "succeeded"))
|
||||||
|
|
||||||
|
post "/yookassa/webhooks/secret-token", payload, headers.merge("REMOTE_ADDR" => "203.0.113.10")
|
||||||
|
|
||||||
|
expect(last_response.status).to eq(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects webhook when fetched object differs by status" do
|
||||||
|
payments_client = instance_double(Yookassa::Payments)
|
||||||
|
allow(Yookassa).to receive(:payments).and_return(payments_client)
|
||||||
|
allow(payments_client).to receive(:find).with(payment_id: "payment-1")
|
||||||
|
.and_return(instance_double(Yookassa::Entity::Payment, id: "payment-1", status: "pending"))
|
||||||
|
|
||||||
|
post "/yookassa/webhooks/secret-token", payload, headers
|
||||||
|
|
||||||
|
expect(last_response.status).to eq(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "IP source check", :rails do
|
||||||
|
it "uses request.remote_ip" do
|
||||||
|
controller = described_class.new
|
||||||
|
request = instance_double(ActionDispatch::Request)
|
||||||
|
|
||||||
|
allow(request).to receive(:remote_ip).and_return("185.71.76.5")
|
||||||
|
allow(request).to receive(:ip).and_raise("request.ip should not be used")
|
||||||
|
allow(controller).to receive(:request).and_return(request)
|
||||||
|
|
||||||
|
expect(controller.send(:source_ip_allowed?)).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "controller inheritance", :rails do
|
||||||
|
it "allows app controller override via inheritance" do
|
||||||
|
payments_client = instance_double(Yookassa::Payments)
|
||||||
|
allow(Yookassa).to receive(:payments).and_return(payments_client)
|
||||||
|
allow(payments_client).to receive(:find).with(payment_id: "payment-1")
|
||||||
|
.and_return(instance_double(Yookassa::Entity::Payment, id: "payment-1", status: "succeeded"))
|
||||||
|
|
||||||
|
post "/custom-yookassa/secret-token", payload, headers
|
||||||
|
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
expect(YookassaSpecCustomWebhooksController.last_payload).to eq(payload_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,14 +15,14 @@ Gem::Specification.new do |spec|
|
|||||||
spec.license = "MIT"
|
spec.license = "MIT"
|
||||||
|
|
||||||
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
||||||
spec.test_files = spec.files.grep(/^spec/)
|
|
||||||
spec.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
|
spec.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
|
||||||
|
|
||||||
spec.require_paths = ["lib"]
|
spec.require_paths = ["lib"]
|
||||||
|
|
||||||
spec.required_ruby_version = ">= 2.6"
|
spec.required_ruby_version = ">= 2.6"
|
||||||
|
|
||||||
spec.add_runtime_dependency "dry-struct"
|
spec.add_dependency "dry-struct"
|
||||||
spec.add_runtime_dependency "dry-validation"
|
spec.add_dependency "dry-validation"
|
||||||
spec.add_runtime_dependency "http", "~> 5.0"
|
spec.add_dependency "http", ">= 5.0", "< 6.0"
|
||||||
|
spec.metadata["rubygems_mfa_required"] = "true"
|
||||||
end
|
end
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user