Skip to content

Commit d337964

Browse files
authored
Merge pull request #5 from rameerez/cursor/ensure-comprehensive-testing-and-production-readiness-d999
Ensure comprehensive testing and production readiness
2 parents d9a4f83 + cd273e8 commit d337964

File tree

17 files changed

+596
-136
lines changed

17 files changed

+596
-136
lines changed

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ test/dummy/node_modules/
1616
test/dummy/config/master.key
1717
test/dummy/storage
1818
test/dummy/storage/*.sqlite3*
19+
# Ignore dummy app vendored bundles completely
20+
test/dummy/vendor/
21+
test/dummy/.bundle/
1922
.DS_Store
2023

2124
/dist/
2225

2326
.vscode
2427
.cursor
28+
.claude
2529

2630
TODO
2731

2832
**/.kamal/
29-
deploy.yml
33+
deploy.yml
34+
35+
# Global bundler/vendor ignores
36+
vendor/
37+
vendor/bundle/

Gemfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ gem "mocha", "~> 2.0"
2828

2929
# For testing against a Rails app (later)
3030
# gem "rails", "~> 7.0"
31-
gem "sqlite3", "~> 1.4"
31+
gem "sqlite3", ">= 2.1"
3232

3333
# Debugging
3434
# gem "debug"
35+
36+
# Speed up boot time for the dummy app when engine tasks load
37+
gem "bootsnap", require: false

Gemfile.lock

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
PATH
22
remote: .
33
specs:
4-
api_keys (0.1.0)
5-
activerecord (>= 7.0)
6-
activesupport (>= 7.0)
4+
api_keys (0.2.1)
5+
activerecord (>= 6.0)
6+
activesupport (>= 6.0)
77
base58 (~> 0.2)
88
bcrypt (~> 3.1)
99
rails (>= 6.1)
@@ -88,6 +88,8 @@ GEM
8888
bcrypt (3.1.20)
8989
benchmark (0.4.0)
9090
bigdecimal (3.1.9)
91+
bootsnap (1.18.6)
92+
msgpack (~> 1.2)
9193
builder (3.3.0)
9294
concurrent-ruby (1.3.5)
9395
connection_pool (2.5.1)
@@ -115,7 +117,6 @@ GEM
115117
net-smtp
116118
marcel (1.0.4)
117119
mini_mime (1.1.5)
118-
mini_portile2 (2.8.8)
119120
minitest (5.25.5)
120121
minitest-reporters (1.7.1)
121122
ansi
@@ -124,6 +125,7 @@ GEM
124125
ruby-progressbar
125126
mocha (2.7.1)
126127
ruby2_keywords (>= 0.0.5)
128+
msgpack (1.8.0)
127129
net-imap (0.5.7)
128130
date
129131
net-protocol
@@ -202,8 +204,14 @@ GEM
202204
ruby-progressbar (1.13.0)
203205
ruby2_keywords (0.0.5)
204206
securerandom (0.4.1)
205-
sqlite3 (1.7.3)
206-
mini_portile2 (~> 2.8.0)
207+
sqlite3 (2.7.3-aarch64-linux-gnu)
208+
sqlite3 (2.7.3-aarch64-linux-musl)
209+
sqlite3 (2.7.3-arm-linux-gnu)
210+
sqlite3 (2.7.3-arm-linux-musl)
211+
sqlite3 (2.7.3-arm64-darwin)
212+
sqlite3 (2.7.3-x86_64-darwin)
213+
sqlite3 (2.7.3-x86_64-linux-gnu)
214+
sqlite3 (2.7.3-x86_64-linux-musl)
207215
stringio (3.1.7)
208216
thor (1.3.2)
209217
timeout (0.4.3)
@@ -229,12 +237,13 @@ PLATFORMS
229237

230238
DEPENDENCIES
231239
api_keys!
240+
bootsnap
232241
bundler (~> 2.0)
233242
minitest (~> 5.14)
234243
minitest-reporters (~> 1.4)
235244
mocha (~> 2.0)
236245
rake (~> 13.0)
237-
sqlite3 (~> 1.4)
246+
sqlite3 (>= 2.1)
238247

239248
BUNDLED WITH
240249
2.6.4

Rakefile

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@ RDoc::Task.new(:rdoc) do |rdoc|
1616
rdoc.rdoc_files.include("lib/**/*.rb")
1717
end
1818

19-
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
20-
load "rails/tasks/engine.rake"
19+
# Removed loading of engine tasks to avoid pulling in dummy app/vendor tests
20+
# APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
21+
# load "rails/tasks/engine.rake"
2122

22-
load "rails/tasks/statistics.rake"
23+
# Removed Rails statistics task; not needed for gem tests
24+
# load "rails/tasks/statistics.rake"
2325

2426
require "rake/testtask"
2527

28+
# Ensure any test task created by engine.rake is cleared so only this suite runs
29+
Rake::Task[:test].clear if Rake::Task.task_defined?(:test)
30+
2631
Rake::TestTask.new(:test) do |t|
2732
t.libs << "test"
28-
t.pattern = "test/**/*_test.rb"
2933
t.verbose = false
34+
t.test_files = FileList['test/**/*_test.rb'].exclude('test/dummy/**/*')
3035
end
3136

3237
task default: :test

lib/api_keys/services/digestor.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ def self.match?(token:, stored_digest:, strategy: ApiKeys.configuration.hash_str
5555
comparison_proc.call(stored_digest, Digest::SHA256.hexdigest(token))
5656
else
5757
# Strategy mismatch or unsupported strategy should fail comparison safely
58-
Rails.logger.error "[ApiKeys] Digestor comparison failed: Unsupported hash strategy '#{strategy}' for digest check." if defined?(Rails.logger)
58+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
59+
Rails.logger.error "[ApiKeys] Digestor comparison failed: Unsupported hash strategy '#{strategy}' for digest check."
60+
end
5961
false
6062
end
6163
rescue ArgumentError => e
6264
# Catch potential errors from Digest or comparison proc
63-
Rails.logger.error "[ApiKeys] Digestor comparison error: #{e.message}" if defined?(Rails.logger)
65+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
66+
Rails.logger.error "[ApiKeys] Digestor comparison error: #{e.message}"
67+
end
6468
false
6569
end
6670
end

lib/api_keys/tenant_resolution.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def current_api_tenant
3333
@current_api_tenant = resolver&.call(current_api_key)
3434
rescue StandardError => e
3535
# Log error but don't break the request if resolver fails
36-
Rails.logger.error "[ApiKeys] Tenant resolution failed: #{e.message}" if defined?(Rails.logger)
36+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
37+
Rails.logger.error "[ApiKeys] Tenant resolution failed: #{e.message}"
38+
end
3739
@current_api_tenant = nil
3840
end
3941
end
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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

Comments
 (0)