amritk-scalar-test 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Code generated by Scalar SDK Generator. DO NOT EDIT.
4
+ module ScalarRubyTest
5
+ VERSION = "0.1.1" # x-release-please-version
6
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Code generated by Scalar SDK Generator. DO NOT EDIT.
4
+ require "base64"
5
+ require "json"
6
+ require "openssl"
7
+
8
+ module ScalarRubyTest
9
+ module Webhooks
10
+ EVENT_NAMES = [].freeze
11
+
12
+
13
+ class WebhookEvent
14
+ attr_reader :type, :payload
15
+
16
+ def initialize(type:, payload:)
17
+ @type = type
18
+ @payload = payload
19
+ end
20
+ end
21
+
22
+ class VerificationError < StandardError; end
23
+
24
+ def self.verify_signature(
25
+ payload, secret, signature_header,
26
+ algorithm: "hmac-sha256", public_key: nil,
27
+ timestamp: nil, tolerance: nil,
28
+ replay_store: nil, replay_id: nil, asymmetric_verifier: nil
29
+ )
30
+ return verify_asymmetric(payload, signature_header, public_key || secret.to_s, algorithm, asymmetric_verifier) unless algorithm == "hmac-sha256"
31
+ secrets = secret.is_a?(Array) ? secret : [secret]
32
+ raise VerificationError, "webhook secret is required" if secrets.empty? || secrets.first.to_s.empty?
33
+ if timestamp && tolerance && (Time.now.to_i - timestamp.to_i).abs > tolerance.to_i
34
+ raise VerificationError, "webhook timestamp is outside the allowed tolerance"
35
+ end
36
+ if replay_store
37
+ id = replay_id || signature_header
38
+ raise VerificationError, "webhook replay detected" if replay_store.include?(id)
39
+ replay_store << id if replay_store.respond_to?(:<<)
40
+ end
41
+ signature_candidates(signature_header).any? do |candidate|
42
+ decoded = decode_signature(candidate)
43
+ decoded && secrets.any? { |item| secure_compare(decoded, OpenSSL::HMAC.digest("SHA256", item.to_s, payload)) }
44
+ end
45
+ end
46
+
47
+ def self.verify_rsa_sha256(payload, public_key, signature_header)
48
+ verify_asymmetric(payload, signature_header, public_key, "rsa-sha256", nil)
49
+ end
50
+
51
+ def self.verify_ecdsa_sha256(payload, public_key, signature_header)
52
+ verify_asymmetric(payload, signature_header, public_key, "ecdsa-sha256", nil)
53
+ end
54
+
55
+ def self.verify_ed25519(payload, public_key, signature_header, verifier)
56
+ verify_asymmetric(payload, signature_header, public_key, "ed25519", verifier)
57
+ end
58
+
59
+ def self.parse_event(payload, headers: {}, secret: nil, signature_header: "webhook-signature")
60
+ if secret
61
+ signature = header_value(headers, signature_header)
62
+ raise VerificationError, "missing webhook signature header" unless signature
63
+ raise VerificationError, "webhook signature verification failed" unless verify_signature(payload, secret, signature)
64
+ end
65
+ data = JSON.parse(payload)
66
+ type = data.is_a?(Hash) ? data["type"] : nil
67
+ raise VerificationError, "webhook payload is missing discriminator: type" unless type.is_a?(String)
68
+ raise VerificationError, "unknown webhook event type: #{type}" if EVENT_NAMES.any? && !EVENT_NAMES.include?(type)
69
+ WebhookEvent.new(type: type, payload: data)
70
+ end
71
+
72
+ def self.signature_candidates(header)
73
+ header.to_s.split(/[,\s]+/).map { |part| part.sub(/\A(sha256|v0|v1|sig|signature)=/i, "").strip }.reject { |part| part.empty? || part.match?(/\A(t|ts|timestamp)=/i) }
74
+ end
75
+
76
+ def self.decode_signature(candidate)
77
+ return [candidate].pack("H*") if candidate.match?(/\A[0-9a-f]+\z/i) && candidate.length.even?
78
+ Base64.strict_decode64(candidate)
79
+ rescue ArgumentError
80
+ nil
81
+ end
82
+
83
+ def self.verify_asymmetric(payload, signature_header, public_key, algorithm, verifier)
84
+ raise VerificationError, "webhook public key is required" if public_key.to_s.empty?
85
+ signature_candidates(signature_header).any? do |candidate|
86
+ decoded = decode_signature(candidate)
87
+ next false unless decoded
88
+ return true if verifier&.call(payload, decoded, public_key, algorithm)
89
+ key = OpenSSL::PKey.read(public_key)
90
+ digest = OpenSSL::Digest.new("SHA256")
91
+ case algorithm
92
+ when "rsa-sha256", "ecdsa-sha256"
93
+ key.verify(digest, decoded, payload)
94
+ when "ed25519"
95
+ raise VerificationError, "Ed25519 verification requires a verifier callback"
96
+ else
97
+ raise VerificationError, "unsupported webhook signature algorithm: #{algorithm}"
98
+ end
99
+ end
100
+ end
101
+
102
+ def self.secure_compare(left, right)
103
+ return false unless left.bytesize == right.bytesize
104
+ OpenSSL.fixed_length_secure_compare(left, right)
105
+ end
106
+
107
+ def self.header_value(headers, name)
108
+ headers.find { |key, _| key.to_s.downcase == name.downcase }&.last
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "amritk-scalar-test/version"
4
+ require_relative "amritk-scalar-test/request_options"
5
+ require_relative "amritk-scalar-test/file_part"
6
+ require_relative "amritk-scalar-test/internal/cursor_page"
7
+ require_relative "amritk-scalar-test/client"
8
+ require_relative "amritk-scalar-test/webhooks"
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Code generated by Scalar SDK Generator. DO NOT EDIT.
4
+ require "json"
5
+ require "stringio"
6
+ require "time"
7
+
8
+ root = File.exist?(File.join(__dir__, "lib", "amritk-scalar-test.rb")) ? __dir__ : File.expand_path("..", __dir__)
9
+ $LOAD_PATH.unshift(File.join(root, "lib"))
10
+ require "amritk-scalar-test"
11
+
12
+ # Smoke test: calls every generated operation once to confirm the SDK can reach each endpoint.
13
+ # Run it from this repo with `ruby tests/smoke-test.rb`. The generator also runs this file
14
+ # against a mock server and reads the JSON report produced via SCALAR_SMOKE_REPORT.
15
+ client = ScalarRubyTest::Client.new(max_retries: 0, timeout: 30)
16
+
17
+ cases = [
18
+ {
19
+ operation: "ping",
20
+ method: "GET",
21
+ path: "/ping",
22
+ run: -> {
23
+ client.ping.ping
24
+ },
25
+ },
26
+ ]
27
+
28
+ def run_case(test_case)
29
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
+ begin
31
+ test_case[:run].call
32
+ { operation: test_case[:operation], method: test_case[:method], path: test_case[:path], status: "passed", durationMs: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).to_i }
33
+ rescue StandardError => e
34
+ { operation: test_case[:operation], method: test_case[:method], path: test_case[:path], status: "failed", durationMs: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).to_i, error: ([e.class.name, e.message] + e.backtrace.to_a).join("\n") }
35
+ end
36
+ end
37
+
38
+ def probe_websocket(request)
39
+ socket = request.connect
40
+ ensure
41
+ socket&.close
42
+ end
43
+
44
+ filter = ENV["SCALAR_SMOKE_FILTER"]
45
+ needles = filter ? filter.split(",").map(&:strip).reject(&:empty?) : []
46
+ selected = needles.empty? ? cases : cases.select { |test_case| needles.any? { |needle| test_case[:operation].include?(needle) || test_case[:path].include?(needle) } }
47
+
48
+ results = selected.map do |test_case|
49
+ Thread.new(test_case) do |smoke_case|
50
+ run_case(smoke_case)
51
+ end
52
+ end.map(&:value)
53
+
54
+ failed = results.select { |result| result[:status] == "failed" }
55
+ if ENV["SCALAR_SMOKE_REPORT"]
56
+ File.write(ENV.fetch("SCALAR_SMOKE_REPORT"), JSON.generate({ total: results.length, failed: failed.length, results: results }))
57
+ else
58
+ results.each do |result|
59
+ if result[:status] == "passed"
60
+ puts "PASS #{result[:operation]} (#{result[:method]} #{result[:path]}) #{result[:durationMs]}ms"
61
+ else
62
+ warn "FAIL #{result[:operation]} (#{result[:method]} #{result[:path]})\n#{result[:error]}"
63
+ end
64
+ end
65
+ warn "No code samples ran (empty SDK or a SCALAR_SMOKE_FILTER that matched nothing)." if results.empty?
66
+ puts "\n#{results.length - failed.length}/#{results.length} samples passed" unless results.empty?
67
+ end
68
+
69
+ exit(1) if failed.any? || results.empty?
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amritk-scalar-test
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Scalar SDK Generator
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - SECURITY.md
22
+ - SKILL.md
23
+ - lib/amritk-scalar-test.rb
24
+ - lib/amritk-scalar-test/client.rb
25
+ - lib/amritk-scalar-test/errors.rb
26
+ - lib/amritk-scalar-test/file_part.rb
27
+ - lib/amritk-scalar-test/internal/cursor_page.rb
28
+ - lib/amritk-scalar-test/models.rb
29
+ - lib/amritk-scalar-test/request_options.rb
30
+ - lib/amritk-scalar-test/resources/ping.rb
31
+ - lib/amritk-scalar-test/runtime.rb
32
+ - lib/amritk-scalar-test/version.rb
33
+ - lib/amritk-scalar-test/webhooks.rb
34
+ - tests/smoke-test.rb
35
+ homepage: https://github.com/amritk/scalar-ruby
36
+ licenses:
37
+ - Apache-2.0
38
+ metadata:
39
+ source_code_uri: https://github.com/amritk/scalar-ruby
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '3.2'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.4.19
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Minimal API used to test the generated Ruby SDK release pipeline.
59
+ test_files: []