Files
sure/test/models/api_key_test.rb
Juan José Mata 61ce5c8514 Protect demo API key from deletion (#919)
* 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>
2026-02-06 21:25:52 +01:00

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