mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* 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>
308 lines
8.8 KiB
Ruby
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
|