From aa6813853a88f0b446ed663b31f3b5403650fc44 Mon Sep 17 00:00:00 2001 From: Mikhail Varabyou Date: Mon, 22 Jun 2026 19:11:48 +0200 Subject: [PATCH 1/2] add headers to upper layers of client. * define gem as user_agent * allow to configure gem level headers * allow to configure client level headers --- CHANGELOG.md | 3 ++ Gemfile.lock | 1 + lib/kaze_client.rb | 1 + lib/kaze_client/client.rb | 9 +++- lib/kaze_client/configuration.rb | 60 ++++++++++++++++++++++++++ lib/kaze_client/request/request.rb | 39 ++++++++++++----- spec/kaze_client/configuration_spec.rb | 55 +++++++++++++++++++++++ 7 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 lib/kaze_client/configuration.rb create mode 100644 spec/kaze_client/configuration_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e306723..c25379e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - Add new request: Update a job's cell. - Add new request: Delete a collection's item. - Fetch the details for the given user. +- Add `KazeClient.configure` to centrally set headers added to every request. +- Send a default `User-Agent` header (`kaze_client/`) on every request. +- Allow passing `headers:` to `KazeClient::Client.new` to add headers to every request from that client. ## [0.4.0] - 2023-09-12 diff --git a/Gemfile.lock b/Gemfile.lock index e09e268..76c9e6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,6 +86,7 @@ GEM PLATFORMS aarch64-linux arm64-darwin-22 + arm64-darwin-24 x86_64-darwin-20 DEPENDENCIES diff --git a/lib/kaze_client.rb b/lib/kaze_client.rb index 8f87bda..261c9ff 100644 --- a/lib/kaze_client.rb +++ b/lib/kaze_client.rb @@ -13,6 +13,7 @@ require_relative 'kaze_client/version' # Require the different parts of the gem +require_relative 'kaze_client/configuration' require_relative 'kaze_client/json_utils' require_relative 'kaze_client/data_utils' require_relative 'kaze_client/errors' diff --git a/lib/kaze_client/client.rb b/lib/kaze_client/client.rb index 1de2e1e..b76926e 100644 --- a/lib/kaze_client/client.rb +++ b/lib/kaze_client/client.rb @@ -14,12 +14,18 @@ class Client # @return [String,nil] The last authentication token # @see KazeClient::Client#login attr_reader :token + # @return [Hash] Headers added to every request executed by this client + attr_reader :headers # @param base_url [String] The server's base URL (e.g. https://app.kaze.so) # @param token [String] The authentication token - def initialize(base_url, token: nil) + # @param headers [Hash] Headers added to every request executed by this client. + # They override the default and globally configured headers but are overridden by + # per-request headers. + def initialize(base_url, token: nil, headers: {}) @base_url = base_url @token = token + @headers = headers || {} @login = nil @password = nil end @@ -98,6 +104,7 @@ def do_execute(request) raise Error::NoEndpoint, @base_url if @base_url.blank? @request = request + @request.with_client_headers(@headers) request_url = "#{@base_url}/#{request.url}" @response = HTTParty.send(request.method, request_url, **@request.parameters) diff --git a/lib/kaze_client/configuration.rb b/lib/kaze_client/configuration.rb new file mode 100644 index 0000000..2b07aa3 --- /dev/null +++ b/lib/kaze_client/configuration.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module KazeClient + + # Holds the global configuration shared by every request. + # + # The most common use case is to add custom headers that must be sent on + # every request to the Kaze API (e.g. an API key, a tracing header, ...). + # + # @see KazeClient.configure + # @since 0.4.0 + # @example Configure custom headers added to every request + # KazeClient.configure do |config| + # config.headers['X-Api-Key'] = 'my-api-key' + # config.headers['X-Request-Id'] = SecureRandom.uuid + # end + class Configuration + + # @return [Hash] The headers added to every request. + # These are merged after +KazeClient::Request::DEFAULT_HEADERS+ but before + # the per-request headers, so a per-request header still takes precedence. + attr_accessor :headers + + def initialize + @headers = {} + end + + end + + class << self + + # @return [KazeClient::Configuration] The global configuration object. + def configuration + @configuration ||= Configuration.new + end + + # Configure the client globally. + # + # @yieldparam config [KazeClient::Configuration] The configuration to mutate. + # @return [KazeClient::Configuration] The configuration after the block ran. + # @example + # KazeClient.configure do |config| + # config.headers['X-Api-Key'] = 'my-api-key' + # end + def configure + yield(configuration) if block_given? + + configuration + end + + # Reset the global configuration back to its defaults. + # + # @return [KazeClient::Configuration] The fresh configuration object. + def reset_configuration! + @configuration = Configuration.new + end + + end + +end diff --git a/lib/kaze_client/request/request.rb b/lib/kaze_client/request/request.rb index a972e6c..4e7552c 100644 --- a/lib/kaze_client/request/request.rb +++ b/lib/kaze_client/request/request.rb @@ -12,7 +12,8 @@ class Request # Those headers are added on all requests by default DEFAULT_HEADERS = { 'Content-Type' => 'application/json', - 'Accept' => 'application/json' + 'Accept' => 'application/json', + 'User-Agent' => "kaze_client/#{KazeClient::VERSION}" }.freeze # @return [String, Symbol] The HTTP verb to use for the request @@ -44,11 +45,24 @@ class Request # @param method [Symbol] The HTTP verb to use for the request (e.g. :get) # @param url [String] The API endpoint (e.g. /jobs) def initialize(method, url) - @method = method - @url = url - @query = nil - @body = nil - @headers = {} + @method = method + @url = url + @query = nil + @body = nil + @headers = {} + @client_headers = {} + end + + # Store the headers configured on the executing client so they can be merged + # into the request. Called by KazeClient::Client at execution time. + # + # @param headers [Hash, nil] The client-level headers + # @return [KazeClient::Request] self (to chain methods) + # @see KazeClient::Client + def with_client_headers(headers) + @client_headers = headers.is_a?(Hash) ? headers : {} + + self end # @return [Hash] The arguments to give to the HTTParty call @@ -77,12 +91,17 @@ def error_for(response) protected # @return [Hash] The headers for the request - # If +@headers+ is blank or is not a Hash, it returns the +DEFAULT_HEADERS+. Else it merges - # the +DEFAULT_HEADERS+ with +@headers+ allowing +@headers+ to override +DEFAULT_HEADERS+. + # Headers are merged with the following precedence (later overrides earlier): + # +DEFAULT_HEADERS+, the globally configured headers (+KazeClient.configuration.headers+), + # the client-level headers (+KazeClient::Client.new(headers:)+), then the per-request +@headers+. + # @see KazeClient.configure + # @see KazeClient::Client def make_headers - return DEFAULT_HEADERS if @headers.blank? || !@headers.is_a?(Hash) + base = DEFAULT_HEADERS.merge(KazeClient.configuration.headers).merge(@client_headers) + + return base if @headers.blank? || !@headers.is_a?(Hash) - DEFAULT_HEADERS.merge(@headers) + base.merge(@headers) end # @return [nil, String] The body for the request diff --git a/spec/kaze_client/configuration_spec.rb b/spec/kaze_client/configuration_spec.rb new file mode 100644 index 0000000..397cb01 --- /dev/null +++ b/spec/kaze_client/configuration_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe KazeClient::Configuration do + after { KazeClient.reset_configuration! } + + describe 'KazeClient.configure' do + it 'yields the configuration object' do + KazeClient.configure do |config| + expect(config).to be(KazeClient.configuration) + end + end + + it 'stores configured headers' do + KazeClient.configure do |config| + config.headers['X-Api-Key'] = 'my-api-key' + end + + expect(KazeClient.configuration.headers).to eq('X-Api-Key' => 'my-api-key') + end + end + + describe 'header injection on requests' do + let(:request) { KazeClient::Request.new(:get, '/api/companies') } + + before do + KazeClient.configure do |config| + config.headers['X-Api-Key'] = 'my-api-key' + end + end + + it 'adds the configured headers to every request' do + expect(request.parameters[:headers]).to include( + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'X-Api-Key' => 'my-api-key' + ) + end + + it 'lets per-request headers override the configured ones' do + request.headers['X-Api-Key'] = 'overridden' + + expect(request.parameters[:headers]).to include('X-Api-Key' => 'overridden') + end + end + + describe 'KazeClient.reset_configuration!' do + it 'clears previously configured headers' do + KazeClient.configure { |config| config.headers['X-Api-Key'] = 'my-api-key' } + + KazeClient.reset_configuration! + + expect(KazeClient.configuration.headers).to be_empty + end + end +end From d9e1656b766cf3788f0bbb29dbe1c08e92155bf1 Mon Sep 17 00:00:00 2001 From: Mikhail Varabyou Date: Mon, 22 Jun 2026 19:39:21 +0200 Subject: [PATCH 2/2] webmock tests. --- Gemfile | 2 + Gemfile.lock | 13 +++ spec/fixtures/job.json | 92 +++++++++++++++++++++ spec/fixtures/jobs.json | 97 ++++++++++++++++++++++ spec/fixtures/profile.json | 159 +++++++++++++++++++++++++++++++++++++ spec/kaze_client_spec.rb | 52 +++++++++++- spec/spec_helper.rb | 20 +++++ 7 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/job.json create mode 100644 spec/fixtures/jobs.json create mode 100644 spec/fixtures/profile.json diff --git a/Gemfile b/Gemfile index cc06e6c..11c26cb 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,8 @@ gem 'rake', '~> 13.0' gem 'rspec', '~> 3.0' +gem 'webmock', '~> 3.0' + gem 'rubocop', '~> 1.30' gem 'rubocop-rake', '~> 0.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 76c9e6c..af30bc7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,15 +8,22 @@ PATH GEM remote: https://rubygems.org/ specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) ast (2.4.2) base64 (0.1.1) + bigdecimal (4.1.2) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) + crack (1.0.1) + bigdecimal + rexml debug (1.8.0) irb (>= 1.5.0) reline (>= 0.3.1) diff-lcs (1.5.0) + hashdiff (1.2.1) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) @@ -37,6 +44,7 @@ GEM racc psych (5.1.0) stringio + public_suffix (7.0.5) racc (1.7.1) rainbow (3.1.1) rake (13.0.6) @@ -81,6 +89,10 @@ GEM stringio (3.0.8) thor (1.2.2) unicode-display_width (2.4.2) + webmock (3.26.2) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) yard (0.9.36) PLATFORMS @@ -98,6 +110,7 @@ DEPENDENCIES rubocop (~> 1.30) rubocop-rake (~> 0.6.0) rubocop-rspec (~> 2.11.1) + webmock (~> 3.0) yard (~> 0.9.36) BUNDLED WITH diff --git a/spec/fixtures/job.json b/spec/fixtures/job.json new file mode 100644 index 0000000..845536b --- /dev/null +++ b/spec/fixtures/job.json @@ -0,0 +1,92 @@ +{ + "id": "c616f8de-7d7e-4a35-96c9-41c1345a6234", + "title": "Test with widget", + "reference": "", + "owner_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "target_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "job_workflow_id": "03e70f4d-e9bf-4e14-a9a1-d45aff5832ff", + "status": "assigned", + "current_step_id": "41c569e7-16ea-4207-852d-f66203dcd163", + "first_not_completed_step_id": null, + "watcher_ids": [], + "owner_name": "Test", + "target_name": "Test", + "own": false, + "annex": false, + "annex_performers": [], + "supervisors": [], + "status_name": "Assigned", + "restrict_completed_update": false, + "restrict_cancelled_update": false, + "assigned_tag_ids": [], + "tags": [], + "workflow": { + "type": "workflow", + "id": "caaa82de-7240-4214-8ca4-b43df00a5d49", + "children": [ + { + "type": "template_job_info", + "id": "41c569e7-16ea-4207-852d-f66203dcd163", + "label": "Job Summary", + "access": 133, + "sms_link": true, + "sms_sender": "Kaze", + "display_expanded": true, + "generate_documents": [], + "city": "Issy-les-Moulineaux", + "state": "IDF", + "country_code": "fr", + "zip_code": 92130, + "job_title": "Test with widget", + "job_due_date": 1654106520000, + "job_address": "2 Rue Michelet, 92130 Issy-les-Moulineaux, France", + "job_location": "48.8279341,2.2818154", + "performer_estimation": 60, + "files": [], + "rules": [] + }, + { + "type": "template_blank", + "id": "test_template_blank", + "label": "Template blank", + "access": 133, + "sms_link": true, + "sms_sender": "Kaze", + "display_expanded": true, + "generate_documents": [], + "children": [ + { + "type": "section", + "id": "33934fe4-480f-4949-988d-9bd6ab63d0ab", + "access": 111, + "direction": "col", + "children": [ + { + "type": "widget_text", + "id": "test_widget_text", + "label": "Test widget text", + "access": 133, + "data_type": "string", + "data": "this is a test." + } + ] + } + ] + } + ], + "access": 0 + }, + "white_label_id": null, + "due_date": 1654106520000, + "start_date": 1654106520000, + "end_date": 1654110120000, + "job_notes_updated_at": null, + "job_data_updated_at": null, + "created_at": 1654099359647, + "updated_at": 1654099451343, + "bwa_link": "https://staging.kaze.so/b/j/0-kNNEHG1NnwNcdD_rGVIQ", + "work_order_address": { + "address": "2 Rue Michelet, 92130 Issy-les-Moulineaux, France", + "location": "48.827934,2.281815" + } +} \ No newline at end of file diff --git a/spec/fixtures/jobs.json b/spec/fixtures/jobs.json new file mode 100644 index 0000000..02ddb5b --- /dev/null +++ b/spec/fixtures/jobs.json @@ -0,0 +1,97 @@ +{ + "meta": { + "page": 1, + "per_page": 100, + "filter": { + "status": [ + "waiting" + ] + }, + "order_field": "created_at", + "order_direction": "desc", + "total_pages": 1, + "total_count": 2 + }, + "data": [ + { + "id": "c616f8de-7d7e-4a35-96c9-41c1345a6234", + "owner_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "owner_name": "Test", + "target_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "target_name": "Test", + "title": "Test with widget", + "reference": "", + "own": true, + "status": "assigned", + "current_step_id": "41c569e7-16ea-4207-852d-f66203dcd163", + "first_not_completed_step_id": null, + "steps": [ + { + "id": "41c569e7-16ea-4207-852d-f66203dcd163", + "label": "Job Summary" + }, + { + "id": "test_template_blank", + "label": "Template blank" + } + ], + "performer_estimation": 60, + "favorite_widgets": [], + "job_workflow_id": "03e70f4d-e9bf-4e14-a9a1-d45aff5832ff", + "annex": false, + "status_name": "Assigned", + "due_date": 1654106520000, + "start_date": 1654106520000, + "end_date": 1654110120000, + "completed_at": null, + "status_started_at": null, + "job_notes_updated_at": null, + "job_data_updated_at": null, + "created_at": 1654099359647, + "updated_at": 1654099451343, + "work_order_address": { + "address": "2 Rue Michelet, 92130 Issy-les-Moulineaux, France", + "location": "48.827934,2.281815" + }, + "tags": [] + }, + { + "id": "ee33d67a-3211-4781-b589-4b76d1bb7462", + "owner_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "owner_name": "Test", + "target_id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "target_name": "Test", + "title": "Test", + "reference": "test", + "own": true, + "status": "assigned", + "current_step_id": "73174ebd-d32a-43df-9742-007c7ec78706", + "first_not_completed_step_id": null, + "steps": [ + { + "id": "73174ebd-d32a-43df-9742-007c7ec78706", + "label": "Job Summary" + } + ], + "performer_estimation": 60, + "favorite_widgets": [], + "job_workflow_id": "e0d12be4-7532-4282-8a4f-4c444a4c0dc5", + "annex": false, + "status_name": "Assigned", + "due_date": 4133980740000, + "start_date": 4133980740000, + "end_date": 4133984340000, + "completed_at": 1634806835317, + "status_started_at": null, + "job_notes_updated_at": null, + "job_data_updated_at": null, + "created_at": 1619266723664, + "updated_at": 1634806835317, + "work_order_address": { + "address": "Paris", + "location": "48.856697,2.351462" + }, + "tags": [] + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/profile.json b/spec/fixtures/profile.json new file mode 100644 index 0000000..b47eb8d --- /dev/null +++ b/spec/fixtures/profile.json @@ -0,0 +1,159 @@ +{ + "user": { + "id": "7ca43f70-38e5-44c9-9690-2a7e7ab51629", + "phone": null, + "email": "test@test.test", + "roles": [ + "admin" + ], + "account": { + "first_name": "Test", + "last_name": "Test", + "locale": "en", + "timezone": "UTC" + }, + "notification_settings": [ + "email_partner_invites", + "email_job_proposals", + "email_proposal_reminders", + "email_partner_documents", + "email_qr_code_scans", + "email_invoice_visitor_actions", + "email_webhook_alerts", + "web_push_partner_invites", + "web_push_job_proposals", + "web_push_proposal_reminders", + "web_push_partner_documents", + "web_push_qr_code_scans", + "web_push_invoice_visitor_actions", + "web_push_webhook_alerts", + "mobile_push_partner_invites", + "mobile_push_job_proposals", + "mobile_push_proposal_reminders", + "mobile_push_partner_documents", + "mobile_push_qr_code_scans", + "mobile_push_invoice_visitor_actions", + "mobile_push_performer_job_assigned" + ], + "access_group_id": null, + "access_policy": { + "performer": true, + "performer_collections": true, + "unassigned_performers": true, + "performer_jobs": true, + "performer_update_jobs": true, + "job_assign_myself": true, + "jobs": true, + "update_jobs": true, + "update_completed_jobs": true, + "update_cancelled_jobs": true, + "create_jobs": true, + "cancel_jobs": true, + "update_job_supervisors": true, + "job_assign_performer": true, + "job_proposals": true, + "create_job_proposals": true, + "accept_job_proposals": true, + "job_assign_provider": true, + "job_change_provider": true, + "supervised_jobs": true, + "update_supervised_jobs": true, + "update_completed_supervised_jobs": true, + "duplicate_supervised_jobs": true, + "cancel_supervised_jobs": true, + "update_supervised_job_supervisors": true, + "workflows": true, + "update_workflows": true, + "create_workflows": true, + "delete_workflows": true, + "download_workflow_json": true, + "collections": true, + "update_collection_items": true, + "create_collection_items": true, + "delete_collection_items": true, + "delete_collection": true, + "update_collection_structure": true, + "create_collections": true, + "export_collections": true, + "collaborators": true, + "update_collaborators": true, + "create_collaborators": true, + "delete_collaborators": true, + "update_collaborators_access_group": true, + "update_collaborators_agencies": true, + "performer_statistic": true, + "provider_performance": true, + "export_jobs": true, + "manage_partner_invites": true, + "invoices": true, + "update_invoices": true, + "create_invoices": true, + "settings_general_info": true, + "settings_billing_config": true, + "settings_jobs_config": true, + "settings_mobile_config": true, + "update_company_page": true, + "settings_api_config": true, + "tags": true, + "create_tags": true, + "update_tags": true, + "delete_tags": true, + "cancel_reasons": true, + "create_cancel_reasons": true, + "update_cancel_reasons": true, + "delete_cancel_reasons": true, + "automated_documents": true, + "create_automated_documents": true, + "update_automated_documents": true, + "delete_automated_documents": true, + "webhooks": true, + "create_webhooks": true, + "update_webhooks": true, + "delete_webhooks": true, + "agenda_events": true, + "create_agenda_events": true, + "update_agenda_events": true, + "delete_agenda_events": true, + "user_agenda_events": true, + "create_user_agenda_events": true, + "update_user_agenda_events": true, + "delete_user_agenda_events": true, + "api_tokens": true, + "create_api_tokens": true, + "delete_api_tokens": true, + "access_groups": true, + "create_access_groups": true, + "update_access_groups": true, + "delete_access_groups": true, + "agencies": true, + "create_agencies": true, + "update_agencies": true, + "delete_agencies": true, + "crud_groups": true, + "create_crud_groups": true, + "update_crud_groups": true, + "delete_crud_groups": true, + "assign_crud_group_users": true, + "manage_collaborator_events": true + } + }, + "company": { + "id": "0b0cc46c-03b6-4033-91ab-6da31f1a403b", + "name": "Test", + "company_logo": { + "url": "https://staging.kaze.so/assets/logo_kaze_short-dda090a967bfc823fa31728b727a674c61862dddcdc219dda16d109003e88a02.png", + "created_at": null + }, + "mobile_config": { + "jobs_view_mode": "default", + "display_due_date_mode_performer": "date_hour", + "display_address_distance_performer": true, + "callout_message": { + "color": "#de4437", + "text": "", + "sticky": false + }, + "annex_performer_message": null + } + } +} \ No newline at end of file diff --git a/spec/kaze_client_spec.rb b/spec/kaze_client_spec.rb index cb45bd0..e5ee494 100644 --- a/spec/kaze_client_spec.rb +++ b/spec/kaze_client_spec.rb @@ -3,9 +3,55 @@ # rubocop:disable Metrics/BlockLength RSpec.describe KazeClient do - # rubocop:disable Layout/LineLength - let(:auth_token) { 'g0gMaMK3wy53OhrOb250NpYeebpEsKA9JJI70l7G8bHtZgHtDDj8bBOsc4euXph5HEldQumppYytrreeADwkEiQBOr1Revqx56AuvNZqaS2NyvXqME3KLGe6R2YjKY5i' } - # rubocop:enable Layout/LineLength + # A fake token: requests are stubbed (see the before block), so its value is never verified. + let(:auth_token) { 'test-auth-token' } + + let(:base_url) { 'https://staging.kaze.so' } + + # The +Content-Type+ is required: HTTParty only parses the body as JSON when the server + # announces it, so without it the response stays a raw String. + let(:json_headers) { { 'Content-Type' => 'application/json' } } + + # Stub every endpoint the suite exercises with a captured (or crafted) response so the tests + # never hit the real server. WebMock raises on any unstubbed request (see spec_helper). + before do + # Unknown API: '/login' resolves to '//login' and the server replies 404. + stub_request(:post, "#{base_url}//login") + .to_return(status: 404, headers: json_headers, body: { status: 404, error: 'Not Found' }.to_json) + + # Login with valid credentials returns a token. + stub_request(:post, "#{base_url}/api/login") + .with(body: { user: { login: 'test@test.test', password: 'password' } }.to_json) + .to_return(status: 200, headers: json_headers, body: { token: auth_token }.to_json) + + # Login with invalid credentials is rejected. + stub_request(:post, "#{base_url}/api/login") + .with(body: { user: { login: 'login', password: 'password' } }.to_json) + .to_return(status: 401, headers: json_headers, + body: { error: 'invalid_credentials', message: 'Invalid Login or Password' }.to_json) + + stub_request(:get, "#{base_url}/api/profile") + .to_return(status: 200, headers: json_headers, body: fixture('profile')) + + # /api/companies (partners) echoes the pagination/sorting/filtering metadata back. The query + # is matched loosely so any combination of list parameters resolves to the same response. + stub_request(:get, %r{#{base_url}/api/partners}).to_return( + status: 200, + headers: json_headers, + body: { + meta: { + page: 5, per_page: 42, order_field: 'id', order_direction: 'desc', + filter: { id: 'a', email: %w[a] }, total_pages: 0, total_count: 0 + }, + data: [] + }.to_json + ) + + stub_request(:get, "#{base_url}/api/jobs") + .to_return(status: 200, headers: json_headers, body: fixture('jobs')) + stub_request(:get, "#{base_url}/api/jobs/c616f8de-7d7e-4a35-96c9-41c1345a6234") + .to_return(status: 200, headers: json_headers, body: fixture('job')) + end it 'has a version number' do expect(KazeClient::VERSION).not_to be nil diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3362849..7975e7b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,7 +3,27 @@ require 'kaze_client' require 'debug' +# Stub all HTTP and forbid real network connections: any unstubbed request raises. +require 'webmock/rspec' +WebMock.disable_net_connect! + +module SpecHelpers + + # Read a captured response fixture from spec/fixtures. + # @param name [String] The fixture base name (without extension) + # @return [String] The raw fixture body + def fixture(name) + File.read(File.expand_path("fixtures/#{name}.json", __dir__)) + end + +end + RSpec.configure do |config| + config.include SpecHelpers + + # The global configuration is process-wide; reset it between examples to avoid leaks. + config.after { KazeClient.reset_configuration! } + # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status'