1+ # frozen_string_literal: true
2+
3+ require "test_helper"
4+ require "active_job"
5+
6+ # Stub helper_method for non-Rails controller context
7+ module Kernel
8+ def helper_method ( *) ; end
9+ end
10+
11+ module ApiKeys
12+ class AuthenticationConcernTest < ApiKeys ::Test
13+ class FakeRequest
14+ attr_reader :headers , :query_parameters , :protocol
15+ def initialize ( headers : { } , query_parameters : { } , protocol : "https://" , uuid : SecureRandom . uuid )
16+ @headers = headers
17+ @query_parameters = query_parameters
18+ @protocol = protocol
19+ @uuid = uuid
20+ end
21+ def uuid
22+ @uuid
23+ end
24+ end
25+
26+ # Minimal controller-like object including the concern
27+ class FakeController
28+ include ApiKeys ::Authentication
29+
30+ attr_reader :rendered
31+ def initialize ( request )
32+ @request = request
33+ @rendered = nil
34+ end
35+
36+ def request
37+ @request
38+ end
39+
40+ def render ( json :, status :)
41+ @rendered = { json : json , status : status }
42+ end
43+ end
44+
45+ def setup
46+ super
47+ ActiveJob ::Base . queue_adapter = :test
48+ end
49+
50+ def clear_enqueued_jobs
51+ ActiveJob ::Base . queue_adapter . enqueued_jobs . clear
52+ ActiveJob ::Base . queue_adapter . performed_jobs . clear if ActiveJob ::Base . queue_adapter . respond_to? ( :performed_jobs )
53+ end
54+
55+ test "authenticate_api_key! success enqueues callbacks and stats job and sets helpers" do
56+ user = User . create! ( name : "Controller User" )
57+ key = ApiKeys ::ApiKey . create! ( owner : user , name : "Controller Key" )
58+ token = key . instance_variable_get ( :@token )
59+ request = FakeRequest . new ( headers : { "Authorization" => "Bearer #{ token } " } )
60+ controller = FakeController . new ( request )
61+
62+ ApiKeys . configure do |c |
63+ c . enable_async_operations = true
64+ c . track_requests_count = true
65+ c . before_authentication = -> ( ctx ) { ctx }
66+ c . after_authentication = -> ( ctx ) { ctx }
67+ end
68+
69+ clear_enqueued_jobs
70+ controller . send ( :authenticate_api_key! )
71+
72+ # Helpers populated
73+ assert_equal key , controller . send ( :current_api_key )
74+ assert_equal user , controller . send ( :current_api_owner )
75+ assert_equal user , controller . send ( :current_api_user )
76+
77+ # Jobs enqueued: before + after callbacks + stats
78+ jobs = ActiveJob ::Base . queue_adapter . enqueued_jobs
79+ job_classes = jobs . map { |j | j [ :job ] }
80+ assert job_classes . count { |jc | jc == ApiKeys ::Jobs ::CallbacksJob } >= 2
81+ assert_includes job_classes , ApiKeys ::Jobs ::UpdateStatsJob
82+ end
83+
84+ test "authenticate_api_key! missing scope renders error and does not enqueue stats" do
85+ user = User . create! ( name : "Scoped User" )
86+ key = ApiKeys ::ApiKey . create! ( owner : user , name : "Scoped Key" , scopes : [ "read" ] ) # no 'write'
87+ token = key . instance_variable_get ( :@token )
88+ request = FakeRequest . new ( headers : { "Authorization" => "Bearer #{ token } " } )
89+ controller = FakeController . new ( request )
90+
91+ ApiKeys . configure do |c |
92+ c . enable_async_operations = true
93+ c . before_authentication = -> ( ctx ) { ctx }
94+ c . after_authentication = -> ( ctx ) { ctx }
95+ end
96+
97+ clear_enqueued_jobs
98+ controller . send ( :authenticate_api_key! , scope : "write" )
99+
100+ # Rendered unauthorized with missing scope
101+ resp = controller . rendered
102+ assert_equal :unauthorized , resp [ :status ]
103+ assert_equal :missing_scope , resp [ :json ] [ :error ]
104+ assert_equal "write" , resp [ :json ] [ :required_scope ]
105+
106+ # Stats job not enqueued
107+ jobs = ActiveJob ::Base . queue_adapter . enqueued_jobs
108+ job_classes = jobs . map { |j | j [ :job ] }
109+ refute_includes job_classes , ApiKeys ::Jobs ::UpdateStatsJob
110+ # But callbacks are still enqueued (before and after)
111+ assert job_classes . count { |jc | jc == ApiKeys ::Jobs ::CallbacksJob } >= 2
112+ end
113+
114+ test "authenticate_api_key! with async disabled enqueues no jobs" do
115+ user = User . create! ( name : "No Async User" )
116+ key = ApiKeys ::ApiKey . create! ( owner : user , name : "No Async Key" )
117+ token = key . instance_variable_get ( :@token )
118+ request = FakeRequest . new ( headers : { "Authorization" => "Bearer #{ token } " } )
119+ controller = FakeController . new ( request )
120+
121+ ApiKeys . configure do |c |
122+ c . enable_async_operations = false
123+ c . track_requests_count = true
124+ c . before_authentication = -> ( ctx ) { ctx }
125+ c . after_authentication = -> ( ctx ) { ctx }
126+ end
127+
128+ clear_enqueued_jobs
129+ controller . send ( :authenticate_api_key! )
130+
131+ jobs = ActiveJob ::Base . queue_adapter . enqueued_jobs
132+ job_classes = jobs . map { |j | j [ :job ] }
133+ refute_includes job_classes , ApiKeys ::Jobs ::CallbacksJob
134+ refute_includes job_classes , ApiKeys ::Jobs ::UpdateStatsJob
135+ end
136+
137+ test "authenticate_api_key! with multiple required scopes succeeds only if all present" do
138+ user = User . create! ( name : "Multi Scope User" )
139+ key = ApiKeys ::ApiKey . create! ( owner : user , name : "Multi" , scopes : %w[ read write ] )
140+ token = key . instance_variable_get ( :@token )
141+ request = FakeRequest . new ( headers : { "Authorization" => "Bearer #{ token } " } )
142+ controller = FakeController . new ( request )
143+
144+ ApiKeys . configure { |c | c . enable_async_operations = false }
145+
146+ # Succeeds when both required
147+ controller . send ( :authenticate_api_key! , scope : %w[ read write ] )
148+ assert_nil controller . rendered , "Should not render when authorized"
149+
150+ # Fails when one required scope missing
151+ key_missing = ApiKeys ::ApiKey . create! ( owner : user , name : "Missing" , scopes : %w[ read ] )
152+ token2 = key_missing . instance_variable_get ( :@token )
153+ controller2 = FakeController . new ( FakeRequest . new ( headers : { "Authorization" => "Bearer #{ token2 } " } ) )
154+ controller2 . send ( :authenticate_api_key! , scope : %w[ read write ] )
155+ refute_nil controller2 . rendered
156+ assert_equal :missing_scope , controller2 . rendered [ :json ] [ :error ]
157+ assert_equal %w[ read write ] , controller2 . rendered [ :json ] [ :required_scope ]
158+ end
159+ end
160+ end
0 commit comments