Skip to content

Commit f9a1c77

Browse files
committed
test: add comprehensive test coverage for Zync authentication
Adds unit and integration tests covering the new ByZyncToken authentication module and the oidc_sync token deprecation: Unit tests (by_zync_token_test.rb): - authenticate_zync_request flag setting and rejection (invalid token, master domain) - current_user resolution (impersonation admin preference, fallback to first_admin!, delegation to super for non-Zync requests) - read-only transaction enforcement for Zync-authenticated requests Integration tests (by_zync_token_test.rb): - Regular access token auth still works on Zync-capable endpoints - oidc_sync tokens rejected on Zync-capable endpoints - X-Zync-Token authentication on read actions (show, find) - Rejection scenarios (invalid token, master domain, write actions) - Read-only database transaction enforcement (write attempts fail) - Domain routing via Host and X-Forwarded-Host headers Additional test coverage: - ByAccessToken: oidc_sync token rejection at authentication layer - AccessToken model: oidc_sync method removal, constant preservation - ZyncWorker: provider_access_token returns non-empty placeholder All tests passing: 17 unit, 11 integration, plus existing coverage in related test files. Assisted-by: Claude Code
1 parent 1c43023 commit f9a1c77

6 files changed

Lines changed: 298 additions & 1 deletion

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
# Integration tests for ApiAuthentication::ByZyncToken.
6+
#
7+
# Uses a fake controller with fake routes (pattern from forbid_params_test.rb) to
8+
# avoid coupling to real Admin API controllers and their additional before_actions.
9+
module ApiAuthentication
10+
module ByZyncTokenIntegration
11+
# Minimal base that mirrors what real Admin API controllers set up:
12+
# ApplicationController -> ByAccessToken -> ByZyncToken.
13+
class FakeController < Admin::Api::BaseController
14+
include ApiAuthentication::ByZyncToken
15+
16+
def show
17+
render json: { account_id: current_account.id, user_id: current_user.id }
18+
end
19+
20+
def update
21+
render plain: 'ok'
22+
end
23+
end
24+
25+
# Controller whose show action attempts a DB write — used to prove the
26+
# read-only transaction blocks it.
27+
class WriteAttemptController < FakeController
28+
def show
29+
User.where(id: -1).update_all(username: 'hacked')
30+
render plain: 'ok'
31+
rescue ActiveRecord::StatementInvalid => e
32+
render plain: e.message, status: :forbidden
33+
end
34+
end
35+
36+
module TestHelpers
37+
ZYNC_TOKEN = 'test-zync-token'
38+
39+
def with_test_routes
40+
Rails.application.routes.draw do
41+
get '/zync_test/show' => 'api_authentication/by_zync_token_integration/fake#show'
42+
put '/zync_test/update' => 'api_authentication/by_zync_token_integration/fake#update'
43+
get '/zync_test/write_attempt' => 'api_authentication/by_zync_token_integration/write_attempt#show'
44+
end
45+
yield
46+
ensure
47+
Rails.application.routes_reloader.reload!
48+
end
49+
50+
def zync_headers(token = ZYNC_TOKEN)
51+
{ 'X-Zync-Token' => token }
52+
end
53+
end
54+
end
55+
56+
class ByZyncTokenIntegrationTest < ActionDispatch::IntegrationTest
57+
include ByZyncTokenIntegration::TestHelpers
58+
59+
def setup
60+
ThreeScale.config.stubs(:zync_authentication_token).returns(ZYNC_TOKEN)
61+
@provider = FactoryBot.create(:provider_account)
62+
host! @provider.external_admin_domain
63+
end
64+
end
65+
66+
class AccessTokenTest < ByZyncTokenIntegrationTest
67+
test 'rejects GET with no auth at all' do
68+
with_test_routes do
69+
get '/zync_test/show'
70+
assert_response :forbidden
71+
end
72+
end
73+
74+
test 'regular access token auth still works on Zync-capable endpoints' do
75+
user = FactoryBot.create(:member, account: @provider, admin_sections: %w[partners])
76+
token = FactoryBot.create(:access_token, owner: user, scopes: 'account_management', permission: 'rw')
77+
with_test_routes do
78+
get '/zync_test/show', params: { access_token: token.plaintext_value }
79+
assert_response :success
80+
end
81+
end
82+
83+
test 'oidc_sync tokens are rejected even on Zync-capable endpoints' do
84+
user = FactoryBot.create(:member, account: @provider, admin_sections: %w[partners])
85+
token = FactoryBot.create(:access_token, owner: user, scopes: 'account_management',
86+
name: AccessToken::OIDC_SYNC_TOKEN)
87+
with_test_routes do
88+
get '/zync_test/show', params: { access_token: token.plaintext_value }
89+
assert_response :forbidden
90+
end
91+
end
92+
end
93+
94+
class ZyncTokenTest < ByZyncTokenIntegrationTest
95+
disable_transactional_fixtures!
96+
97+
test 'rejects GET with an invalid X-Zync-Token' do
98+
with_test_routes do
99+
get '/zync_test/show', headers: zync_headers('wrong')
100+
assert_response :forbidden
101+
end
102+
end
103+
104+
test 'rejects requests targeting the master domain even with a valid X-Zync-Token' do
105+
host! master_account.internal_admin_domain
106+
with_test_routes do
107+
get '/zync_test/show', headers: zync_headers
108+
assert_response :forbidden
109+
end
110+
end
111+
112+
test 'does not authenticate write actions via X-Zync-Token' do
113+
with_test_routes do
114+
put '/zync_test/update', headers: zync_headers
115+
assert_response :forbidden
116+
end
117+
end
118+
119+
test 'authenticates GET requests with a valid X-Zync-Token' do
120+
with_test_routes do
121+
get '/zync_test/show', headers: zync_headers
122+
assert_response :success
123+
end
124+
end
125+
126+
test 'Zync-authenticated requests enforce a read-only DB transaction' do
127+
with_test_routes do
128+
get '/zync_test/write_attempt', headers: zync_headers
129+
assert_response :forbidden
130+
assert_match(/read.only transaction/i, response.body)
131+
end
132+
end
133+
end
134+
135+
class DomainRoutingTest < ByZyncTokenIntegrationTest
136+
disable_transactional_fixtures!
137+
138+
test 'authenticates as the admin of the provider whose domain is in the Host header' do
139+
with_test_routes do
140+
get '/zync_test/show', headers: zync_headers
141+
assert_response :success
142+
assert_equal @provider.id, response.parsed_body['account_id']
143+
end
144+
end
145+
146+
test 'X-Forwarded-Host overrides Host header for domain resolution' do
147+
provider_b = FactoryBot.create(:provider_account)
148+
with_test_routes do
149+
get '/zync_test/show', headers: zync_headers.merge('X-Forwarded-Host' => provider_b.internal_admin_domain)
150+
assert_response :success
151+
assert_equal provider_b.id, response.parsed_body['account_id']
152+
end
153+
end
154+
155+
test 'rejects master domain via X-Forwarded-Host even with valid Zync token' do
156+
with_test_routes do
157+
get '/zync_test/show', headers: zync_headers.merge('X-Forwarded-Host' => master_account.internal_admin_domain)
158+
assert_response :forbidden
159+
end
160+
end
161+
end
162+
end

test/integration/by_access_token_integration_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,15 @@ def test_index_with_access_token
114114

115115
assert_response :forbidden
116116
end
117+
118+
test 'oidc_sync tokens are rejected even with a valid plaintext value' do
119+
# Create a token with the oidc_sync name and store its plaintext value
120+
token = FactoryBot.create(:access_token, owner: @user, scopes: 'account_management',
121+
name: AccessToken::OIDC_SYNC_TOKEN)
122+
plaintext = token.plaintext_value
123+
124+
get admin_api_accounts_path(format: :xml), params: { access_token: plaintext }
125+
126+
assert_response :forbidden
127+
end
117128
end

test/models/access_token_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ def test_find_from_value_rejects_leaked_hash_as_token
312312
assert access_token.valid?
313313
end
314314

315+
test 'AccessToken.oidc_sync no longer exists' do
316+
assert_not AccessToken.respond_to?(:oidc_sync)
317+
end
318+
319+
test 'OIDC_SYNC_TOKEN constant is still defined for rejection logic' do
320+
assert_equal 'OIDC Synchronization Token', AccessToken::OIDC_SYNC_TOKEN
321+
end
322+
315323
private
316324

317325
def assert_access_token_audit_all_data(access_token, audit)

test/unit/api_authentication/by_authentication_token_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def setup
2222

2323
def mock_token(attributes = {})
2424
@params = { access_token: 'some-token' }
25-
token = mock('access-token', attributes.merge(expired?: false))
25+
token = mock('access-token', **attributes, expired?: false, name: 'access-token')
2626
@access_tokens.expects(:find_from_value).with('some-token').returns(token)
2727
token
2828
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
class ApiAuthentication::ByZyncTokenTest < ActiveSupport::TestCase
6+
# Minimal host object that includes ByZyncToken so we can call its methods directly.
7+
class Host
8+
include ActiveSupport::Callbacks
9+
define_callbacks :action
10+
include ActiveSupport::Rescuable
11+
include AbstractController::Callbacks
12+
extend AbstractController::Callbacks::ClassMethods
13+
14+
include ApiAuthentication::ByAccessToken
15+
include ApiAuthentication::ByZyncToken
16+
17+
attr_reader :request, :domain_account, :params
18+
19+
def initialize(request:, domain_account:)
20+
@request = request
21+
@domain_account = domain_account
22+
@params = {}
23+
end
24+
end
25+
26+
def setup
27+
@user = stub('user')
28+
@account = stub('account', master?: false,
29+
find_impersonation_admin: nil,
30+
first_admin!: @user)
31+
@request = stub('request', authorization: nil)
32+
@host = Host.new(request: @request, domain_account: @account)
33+
end
34+
35+
# authenticate_zync_request
36+
37+
test '#authenticate_zync_request sets zync_authenticated flag for a valid Zync request' do
38+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: true))
39+
40+
@host.send(:authenticate_zync_request)
41+
42+
assert @host.instance_variable_get(:@zync_authenticated)
43+
end
44+
45+
test '#authenticate_zync_request is a no-op when X-Zync-Token is wrong' do
46+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: false))
47+
48+
@host.send(:authenticate_zync_request)
49+
50+
assert_nil @host.instance_variable_get(:@zync_authenticated)
51+
end
52+
53+
test '#authenticate_zync_request is a no-op when domain is master' do
54+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: true))
55+
@account.stubs(:master?).returns(true)
56+
57+
@host.send(:authenticate_zync_request)
58+
59+
assert_nil @host.instance_variable_get(:@zync_authenticated)
60+
end
61+
62+
# current_user
63+
64+
test '#current_user falls back to first_admin! when no impersonation admin exists' do
65+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: true))
66+
@host.send(:authenticate_zync_request)
67+
68+
assert_equal @user, @host.send(:current_user)
69+
end
70+
71+
test '#current_user prefers the impersonation admin over first_admin!' do
72+
impersonation_admin = stub('impersonation_admin')
73+
@account.stubs(:find_impersonation_admin).returns(impersonation_admin)
74+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: true))
75+
@host.send(:authenticate_zync_request)
76+
77+
assert_equal impersonation_admin, @host.send(:current_user)
78+
end
79+
80+
test '#current_user delegates to super for non-Zync requests' do
81+
AuthenticatedSystem::Request.stubs(:new).with(@request).returns(stub(zync?: false))
82+
@host.send(:authenticate_zync_request)
83+
84+
assert_nil @host.send(:current_user)
85+
end
86+
87+
# enforce_access_token_permission
88+
89+
class EnforcePermissionTest < ApiAuthentication::ByZyncTokenTest
90+
# Read-only transaction enforcement can't run inside a transaction.
91+
self.use_transactional_tests = false
92+
93+
test '#enforce_access_token_permission enforces a read-only DB transaction for Zync requests' do
94+
@host.instance_variable_set(:@zync_authenticated, true)
95+
96+
assert_raises ApiAuthentication::ByAccessToken::PermissionError do
97+
@host.send(:enforce_access_token_permission) { User.delete_all }
98+
end
99+
end
100+
end
101+
102+
test '#enforce_access_token_permission delegates to ByAccessToken for non-Zync requests' do
103+
# @zync_authenticated is not set — falls through to ByAccessToken's version.
104+
# With no authenticated_token (no access_token param, no HTTP auth) it yields normally.
105+
executed = false
106+
@host.send(:enforce_access_token_permission) { executed = true }
107+
assert executed
108+
end
109+
end

test/workers/zync_worker_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,11 @@ class UnprocessableEntityRetryTest < ActiveSupport::TestCase
222222
assert_equal zync_event.data[:service_id], dependency_event_service.data[:id]
223223
Sidekiq::Testing.inline! { ZyncWorker.perform_async( dependency_event_service.event_id, dependency_event_service.data.as_json) }
224224
end
225+
226+
test 'provider_access_token returns a non-empty placeholder string' do
227+
provider = FactoryBot.create(:provider_account)
228+
token = ZyncWorker.provider_access_token(provider)
229+
assert_kind_of String, token
230+
assert token.present?
231+
end
225232
end

0 commit comments

Comments
 (0)