Files
sure/test/encryption_verification_test.rb
soky srm 696ff0966b Initial security fixes (#461)
* Initial sec

* Update PII fields

* FIX add tests

* FIX safely read plaintext data on rake backfill

* Update user.rb

* FIX tests

* encryption_ready? block

* Test conditional to encryption on

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-23 22:05:28 +01:00

308 lines
8.8 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class EncryptionVerificationTest < ActiveSupport::TestCase
# Skip all tests in this file if encryption is not configured.
# This allows the test suite to pass in environments without encryption keys.
setup do
skip "Encryption not configured" unless User.encryption_ready?
end
# ============================================================================
# USER MODEL TESTS
# ============================================================================
test "user email is encrypted and can be looked up" do
user = User.create!(
email: "encryption-test@example.com",
password: "password123",
family: families(:dylan_family)
)
# Verify we can find by email (deterministic encryption)
found = User.find_by(email: "encryption-test@example.com")
assert_equal user.id, found.id
# Verify case-insensitive lookup works
found_upper = User.find_by(email: "ENCRYPTION-TEST@EXAMPLE.COM")
assert_equal user.id, found_upper.id
# Clean up
user.destroy
end
test "user email uniqueness validation works with encryption" do
user1 = User.create!(
email: "unique-test@example.com",
password: "password123",
family: families(:dylan_family)
)
# Should fail uniqueness
user2 = User.new(
email: "unique-test@example.com",
password: "password123",
family: families(:dylan_family)
)
assert_not user2.valid?
assert user2.errors[:email].any?
user1.destroy
end
test "user names are encrypted and retrievable" do
user = users(:family_admin)
original_first = user.first_name
original_last = user.last_name
# Update names
user.update!(first_name: "EncryptedFirst", last_name: "EncryptedLast")
user.reload
assert_equal "EncryptedFirst", user.first_name
assert_equal "EncryptedLast", user.last_name
# Restore
user.update!(first_name: original_first, last_name: original_last)
end
test "user MFA otp_secret is encrypted" do
user = users(:family_admin)
# Setup MFA
user.setup_mfa!
assert user.otp_secret.present?
# Reload and verify we can still read it
user.reload
assert user.otp_secret.present?
# Verify provisioning URI works
assert user.provisioning_uri.present?
# Clean up
user.disable_mfa!
end
test "user unconfirmed_email is encrypted" do
user = users(:family_admin)
original_email = user.email
# Set unconfirmed email
user.update!(unconfirmed_email: "new-email@example.com")
user.reload
assert_equal "new-email@example.com", user.unconfirmed_email
# Clean up
user.update!(unconfirmed_email: nil)
end
# ============================================================================
# INVITATION MODEL TESTS
# ============================================================================
test "invitation token is encrypted and lookups work" do
invitation = Invitation.create!(
email: "invite-test@example.com",
role: "member",
inviter: users(:family_admin),
family: families(:dylan_family)
)
# Token should be present
assert invitation.token.present?
token_value = invitation.token
# Should be able to find by token
found = Invitation.find_by(token: token_value)
assert_equal invitation.id, found.id
invitation.destroy
end
test "invitation email is encrypted and scoped uniqueness works" do
invitation1 = Invitation.create!(
email: "scoped-invite@example.com",
role: "member",
inviter: users(:family_admin),
family: families(:dylan_family)
)
# Same email, same family should fail
invitation2 = Invitation.new(
email: "scoped-invite@example.com",
role: "member",
inviter: users(:family_admin),
family: families(:dylan_family)
)
assert_not invitation2.valid?
invitation1.destroy
end
# ============================================================================
# INVITE CODE MODEL TESTS
# ============================================================================
test "invite code token is encrypted and claim works" do
token = InviteCode.generate!
assert token.present?
# Should be able to claim
result = InviteCode.claim!(token)
assert result
# Should not be able to claim again (destroyed)
result2 = InviteCode.claim!(token)
assert_nil result2
end
test "invite code case-insensitive lookup works" do
invite_code = InviteCode.create!
token = invite_code.token
# Should find with lowercase
found = InviteCode.find_by(token: token.downcase)
assert_equal invite_code.id, found.id
invite_code.destroy
end
# ============================================================================
# SESSION MODEL TESTS
# ============================================================================
test "session user_agent is encrypted" do
Current.user_agent = "Mozilla/5.0 Test Browser"
Current.ip_address = "192.168.1.100"
begin
session = Session.create!(user: users(:family_admin))
assert_equal "Mozilla/5.0 Test Browser", session.user_agent
assert session.ip_address_digest.present?
# Reload and verify
session.reload
assert_equal "Mozilla/5.0 Test Browser", session.user_agent
# Verify IP hash is consistent
expected_hash = Digest::SHA256.hexdigest("192.168.1.100")
assert_equal expected_hash, session.ip_address_digest
session.destroy
ensure
Current.user_agent = nil
Current.ip_address = nil
end
end
# ============================================================================
# MOBILE DEVICE MODEL TESTS
# ============================================================================
test "mobile device device_id is encrypted and uniqueness works" do
device = MobileDevice.create!(
user: users(:family_admin),
device_id: "test-device-12345",
device_name: "Test iPhone",
device_type: "ios"
)
# Should be able to find by device_id
found = MobileDevice.find_by(device_id: "test-device-12345", user: users(:family_admin))
assert_equal device.id, found.id
# Same device_id for same user should fail
device2 = MobileDevice.new(
user: users(:family_admin),
device_id: "test-device-12345",
device_name: "Another iPhone",
device_type: "ios"
)
assert_not device2.valid?
# Same device_id for different user should work
device3 = MobileDevice.new(
user: users(:family_member),
device_id: "test-device-12345",
device_name: "Their iPhone",
device_type: "ios"
)
assert device3.valid?
device.destroy
end
# ============================================================================
# PROVIDER ITEM TESTS (if fixtures exist)
# ============================================================================
test "lunchflow item credentials and payloads are encrypted" do
skip "No lunchflow items in fixtures" unless LunchflowItem.any?
item = LunchflowItem.first
original_payload = item.raw_payload
# Should be able to read
assert item.api_key.present? || item.raw_payload.present?
# Update payload
item.update!(raw_payload: { test: "data" })
item.reload
assert_equal({ "test" => "data" }, item.raw_payload)
# Restore
item.update!(raw_payload: original_payload)
end
test "lunchflow account payloads are encrypted" do
skip "No lunchflow accounts in fixtures" unless LunchflowAccount.any?
account = LunchflowAccount.first
original_payload = account.raw_payload
# Should be able to read encrypted fields without error
account.reload
assert_nothing_raised { account.raw_payload }
assert_nothing_raised { account.raw_transactions_payload }
# Update and verify
account.update!(raw_payload: { account_test: "value" })
account.reload
assert_equal({ "account_test" => "value" }, account.raw_payload)
# Restore
account.update!(raw_payload: original_payload)
end
# ============================================================================
# DATABASE VERIFICATION TESTS
# ============================================================================
test "encrypted fields are not stored as plaintext in database" do
user = User.create!(
email: "plaintext-check@example.com",
password: "password123",
first_name: "PlaintextFirst",
last_name: "PlaintextLast",
family: families(:dylan_family)
)
# Query raw database value
raw_email = ActiveRecord::Base.connection.select_value(
User.where(id: user.id).select(:email).to_sql
)
# Should NOT be plaintext (should be encrypted blob or different)
assert_not_equal "plaintext-check@example.com", raw_email,
"Email should be encrypted in database, not stored as plaintext"
user.destroy
end
end