mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* feat: Protect demo monitoring API key from deletion - Add DEMO_MONITORING_KEY constant to ApiKey model - Add `demo_monitoring_key?` method to identify the monitoring key - Add `visible` scope to exclude monitoring key from UI queries - Update controller to use `visible` scope, hiding the monitoring key - Prevent revocation of the monitoring key with explicit error handling - Update Demo::Generator to use the shared constant Users on the demo instance can still create their own API keys, but cannot see or delete the monitoring key used for uptime checks. https://claude.ai/code/session_01RQFsw39K7PB5kztboVdBdB * Linter * Protect demo monitoring API key from deletion * Use monitoring source for demo API key * Add test for demo monitoring revoke guard * Disable Rack::Attack in test and development --------- Co-authored-by: Claude <noreply@anthropic.com>
272 lines
6.7 KiB
Ruby
272 lines
6.7 KiB
Ruby
require "test_helper"
|
|
|
|
class ApiKeyTest < ActiveSupport::TestCase
|
|
def setup
|
|
@user = users(:family_admin)
|
|
# Clean up any existing API keys for this user to ensure tests start fresh
|
|
@user.api_keys.destroy_all
|
|
@api_key = ApiKey.new(
|
|
user: @user,
|
|
name: "Test API Key",
|
|
key: "test_plain_key_123",
|
|
scopes: [ "read_write" ]
|
|
)
|
|
end
|
|
|
|
test "should be valid with valid attributes" do
|
|
assert @api_key.valid?
|
|
end
|
|
|
|
test "should require display_key presence after save" do
|
|
@api_key.key = nil
|
|
assert_not @api_key.valid?
|
|
end
|
|
|
|
test "should require name presence" do
|
|
@api_key.name = nil
|
|
assert_not @api_key.valid?
|
|
assert_includes @api_key.errors[:name], "can't be blank"
|
|
end
|
|
|
|
test "should require scopes presence" do
|
|
@api_key.scopes = nil
|
|
assert_not @api_key.valid?
|
|
assert_includes @api_key.errors[:scopes], "can't be blank"
|
|
end
|
|
|
|
test "should require user association" do
|
|
@api_key.user = nil
|
|
assert_not @api_key.valid?
|
|
assert_includes @api_key.errors[:user], "must exist"
|
|
end
|
|
|
|
test "should set display_key from key before saving" do
|
|
original_key = @api_key.key
|
|
@api_key.save!
|
|
|
|
# display_key should be encrypted but plain_key should return the original
|
|
assert_equal original_key, @api_key.plain_key
|
|
end
|
|
|
|
test "should find api key by plain value" do
|
|
plain_key = @api_key.key
|
|
@api_key.save!
|
|
|
|
found_key = ApiKey.find_by_value(plain_key)
|
|
assert_equal @api_key, found_key
|
|
end
|
|
|
|
test "should return nil when finding by invalid value" do
|
|
@api_key.save!
|
|
|
|
found_key = ApiKey.find_by_value("invalid_key")
|
|
assert_nil found_key
|
|
end
|
|
|
|
test "should return nil when finding by nil value" do
|
|
@api_key.save!
|
|
|
|
found_key = ApiKey.find_by_value(nil)
|
|
assert_nil found_key
|
|
end
|
|
|
|
test "key_matches? should work with plain key" do
|
|
plain_key = @api_key.key
|
|
@api_key.save!
|
|
|
|
assert @api_key.key_matches?(plain_key)
|
|
assert_not @api_key.key_matches?("wrong_key")
|
|
end
|
|
|
|
test "should be active when not revoked and not expired" do
|
|
@api_key.save!
|
|
|
|
assert @api_key.active?
|
|
end
|
|
|
|
test "should not be active when revoked" do
|
|
@api_key.save!
|
|
@api_key.revoke!
|
|
|
|
assert_not @api_key.active?
|
|
assert @api_key.revoked?
|
|
end
|
|
|
|
test "should not be active when expired" do
|
|
@api_key.expires_at = 1.day.ago
|
|
@api_key.save!
|
|
|
|
assert_not @api_key.active?
|
|
assert @api_key.expired?
|
|
end
|
|
|
|
test "should be active when expires_at is in the future" do
|
|
@api_key.expires_at = 1.day.from_now
|
|
@api_key.save!
|
|
|
|
assert @api_key.active?
|
|
assert_not @api_key.expired?
|
|
end
|
|
|
|
test "should be active when expires_at is nil" do
|
|
@api_key.expires_at = nil
|
|
@api_key.save!
|
|
|
|
assert @api_key.active?
|
|
assert_not @api_key.expired?
|
|
end
|
|
|
|
test "should generate secure key" do
|
|
key = ApiKey.generate_secure_key
|
|
|
|
assert_kind_of String, key
|
|
assert_equal 64, key.length # hex(32) = 64 characters
|
|
assert key.match?(/\A[0-9a-f]+\z/) # only hex characters
|
|
end
|
|
|
|
test "should update last_used_at when update_last_used! is called" do
|
|
@api_key.save!
|
|
original_time = @api_key.last_used_at
|
|
|
|
sleep(0.01) # Ensure time difference
|
|
@api_key.update_last_used!
|
|
|
|
assert_not_equal original_time, @api_key.last_used_at
|
|
assert @api_key.last_used_at > (original_time || Time.at(0))
|
|
end
|
|
|
|
test "should prevent user from having multiple active api keys" do
|
|
@api_key.save!
|
|
|
|
second_key = ApiKey.new(
|
|
user: @user,
|
|
name: "Second API Key",
|
|
key: "another_key_123",
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert_not second_key.valid?
|
|
assert_includes second_key.errors[:user], "can only have one active API key per source (web)"
|
|
end
|
|
|
|
test "should allow user to have new active key after revoking old one" do
|
|
@api_key.save!
|
|
@api_key.revoke!
|
|
|
|
second_key = ApiKey.new(
|
|
user: @user,
|
|
name: "Second API Key",
|
|
key: "another_key_123",
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert second_key.valid?
|
|
end
|
|
|
|
test "should allow active monitoring key alongside active web key" do
|
|
@api_key.save!
|
|
|
|
monitoring_key = ApiKey.new(
|
|
user: @user,
|
|
name: "Monitoring API Key",
|
|
key: "monitoring_key_123",
|
|
scopes: [ "read" ],
|
|
source: "monitoring"
|
|
)
|
|
|
|
assert monitoring_key.valid?
|
|
end
|
|
|
|
test "should include active api keys in active scope" do
|
|
@api_key.save!
|
|
active_keys = ApiKey.active
|
|
|
|
assert_includes active_keys, @api_key
|
|
end
|
|
|
|
test "should exclude revoked api keys from active scope" do
|
|
@api_key.save!
|
|
@api_key.revoke!
|
|
active_keys = ApiKey.active
|
|
|
|
assert_not_includes active_keys, @api_key
|
|
end
|
|
|
|
test "should exclude expired api keys from active scope" do
|
|
@api_key.expires_at = 1.day.ago
|
|
@api_key.save!
|
|
active_keys = ApiKey.active
|
|
|
|
assert_not_includes active_keys, @api_key
|
|
end
|
|
|
|
test "should return plain_key for display" do
|
|
original_key = @api_key.key
|
|
@api_key.save!
|
|
|
|
assert_equal original_key, @api_key.plain_key
|
|
end
|
|
|
|
test "should not allow multiple scopes" do
|
|
@api_key.scopes = [ "read", "read_write" ]
|
|
assert_not @api_key.valid?
|
|
assert_includes @api_key.errors[:scopes], "can only have one permission level"
|
|
end
|
|
|
|
test "should validate scope values" do
|
|
@api_key.scopes = [ "invalid_scope" ]
|
|
assert_not @api_key.valid?
|
|
assert_includes @api_key.errors[:scopes], "must be either 'read' or 'read_write'"
|
|
end
|
|
|
|
test "should prevent destroying demo monitoring api key" do
|
|
demo_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Demo Monitoring Key",
|
|
display_key: ApiKey::DEMO_MONITORING_KEY,
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert_raises(ActiveRecord::RecordNotDestroyed) { demo_key.destroy! }
|
|
assert ApiKey.exists?(demo_key.id)
|
|
end
|
|
|
|
test "should prevent revoking demo monitoring api key" do
|
|
demo_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Demo Monitoring Key",
|
|
display_key: ApiKey::DEMO_MONITORING_KEY,
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert_raises(ActiveRecord::RecordNotDestroyed) { demo_key.revoke! }
|
|
demo_key.reload
|
|
assert_nil demo_key.revoked_at
|
|
end
|
|
|
|
test "should prevent deleting demo monitoring api key" do
|
|
demo_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Demo Monitoring Key",
|
|
display_key: ApiKey::DEMO_MONITORING_KEY,
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert_raises(ActiveRecord::RecordNotDestroyed) { demo_key.delete }
|
|
assert ApiKey.exists?(demo_key.id)
|
|
end
|
|
|
|
test "should allow destroying non-demo api key" do
|
|
api_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Disposable API Key",
|
|
display_key: "disposable_key_123",
|
|
scopes: [ "read" ]
|
|
)
|
|
|
|
assert_difference("ApiKey.count", -1) do
|
|
api_key.destroy!
|
|
end
|
|
end
|
|
end
|