Update tests for the Internal API

This commit is contained in:
Stanko K.R.
2025-10-22 08:04:52 +02:00
parent 95dc6dc2ed
commit 201c3e27d3
32 changed files with 894 additions and 425 deletions
+11 -1
View File
@@ -21,6 +21,7 @@ module Authentication
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
before_action :resume_identity, **options
before_action :resume_session, **options
end
@@ -30,9 +31,14 @@ module Authentication
before_action :redirect_tenanted_request, **options
end
def require_identity(**options)
def require_identified_access(**options)
before_action :require_identity, **options
end
def require_unidentified_access(**options)
before_action :resume_identity, **options
before_action :redirect_request_with_identification, **options
end
end
private
@@ -110,6 +116,10 @@ module Authentication
redirect_to root_url if ApplicationRecord.current_tenant
end
def redirect_request_with_identification
redirect_to session_login_menu_path(script_name: nil) if Current.identity_token.present?
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
set_current_session session
@@ -1,5 +1,6 @@
class Sessions::LoginMenusController < ApplicationController
require_untenanted_access
require_identified_access
def show
@tenants = IdentityProvider.tenants_for(resume_identity)
@@ -1,5 +1,6 @@
class Sessions::MagicLinksController < ApplicationController
require_untenanted_access
require_unidentified_access
rate_limit to: 10, within: 15.minutes, only: :create, with: -> { redirect_to session_magic_link_path, alert: "Try again in 15 minutes." }
def show
@@ -1,5 +1,6 @@
class Sessions::StartsController < ApplicationController
allow_unauthenticated_access
require_identified_access
def new
end
+1
View File
@@ -1,5 +1,6 @@
class SessionsController < ApplicationController
require_untenanted_access only: %i[ new create ]
require_unidentified_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
def new
@@ -6,8 +6,12 @@ class Users::EmailAddressesController < ApplicationController
end
def create
token = @user.generate_email_address_change_token(to: new_email_address, expires_in: 30.minutes)
UserMailer.email_change_confirmation(user: @user, email_address: new_email_address, token: token).deliver_later
if User.exists?(email_address: new_email_address)
flash.now[:alert] = "Someone else already uses that email"
render :new, status: :unprocessable_entity
else
@user.send_email_address_change_confirmation(new_email_address)
end
end
private
+10 -5
View File
@@ -11,10 +11,13 @@ class Account::JoinCode < ApplicationRecord
class << self
def redeem(code)
find_by(code: code)&.tap do |join_code|
if join_code.active?
join_code.increment!(:usage_count)
end
join_code = find_by(code: code)
if join_code&.active?
join_code.increment!(:usage_count)
true
else
false
end
end
@@ -28,7 +31,9 @@ class Account::JoinCode < ApplicationRecord
end
def reset
update!(code: generate_code, usage_count: 0)
generate_code
self.usage_count = 0
save!
end
private
+3 -1
View File
@@ -1,4 +1,6 @@
module IdentityProvider
class Error < StandardError; end
Token = Data.define(:id, :updated_at) do
delegate :dig, to: :to_h
@@ -11,7 +13,7 @@ module IdentityProvider
extend self
mattr_accessor :backend, default: IdentityProvider::Simple
mattr_accessor :backend, default: IdentityProvider::LocalBackend
delegate :link, :unlink, :change_email_address, :send_magic_link, :consume_magic_link, :tenants_for, :token_for, :resolve_token, :verify_token, to: :backend
end
@@ -1,4 +1,4 @@
module IdentityProvider::Simple
module IdentityProvider::LocalBackend
extend self
def link(email_address:, to:)
+9 -4
View File
@@ -20,15 +20,20 @@ class User < ApplicationRecord
normalizes :email_address, with: ->(value) { value.strip.downcase }
def deactivate
old_email_address = email_address
sessions.delete_all
accesses.destroy_all
old_email = email_address
update! active: false, email_address: deactived_email_address
unlink_identity
update! active: false, email_address: deactivated_email_address
IdentityProvider.unlink(email_address: old_email_address, from: tenant)
rescue => e
update! active: true, email_address: old_email_address
raise e
end
private
def deactived_email_address
def deactivated_email_address
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
end
+20 -1
View File
@@ -1,8 +1,14 @@
module User::EmailAddressChangeable
EMAIL_CHANGE_TOKEN_PURPOSE = "change_email_address"
EMAIL_CHANGE_TOKEN_EXPIRATION = 30.minutes
extend ActiveSupport::Concern
def send_email_address_change_confirmation(new_email_address)
token = generate_email_address_change_token(to: new_email_address, expires_in: EMAIL_CHANGE_TOKEN_EXPIRATION)
UserMailer.email_change_confirmation(user: self, email_address: new_email_address, token: token).deliver_later
end
def generate_email_address_change_token(from: email_address, to:, **options)
options = options.reverse_merge(
for: EMAIL_CHANGE_TOKEN_PURPOSE,
@@ -23,7 +29,20 @@ module User::EmailAddressChangeable
elsif email_address != parsed_token.params.fetch("old_email_address")
raise ArgumentError, "The token was generated for a different email address"
else
update!(email_address: parsed_token.params.fetch("new_email_address"))
change_email_address(parsed_token.params.fetch("new_email_address"))
end
end
private
def change_email_address(new_email_address)
old_email_address = email_address
update!(email_address: new_email_address)
begin
IdentityProvider.change_email_address(from: old_email_address, to: new_email_address, tenant: tenant)
rescue => e
update!(email_address: old_email_address)
raise e
end
end
end
-6
View File
@@ -3,7 +3,6 @@ module User::Identifiable
included do
after_create_commit :link_identity, unless: :system?
after_update_commit :update_email_address_on_identity, if: -> { saved_change_to_email_address? && !system? }
after_destroy_commit :unlink_identity, unless: :system?
end
@@ -19,9 +18,4 @@ module User::Identifiable
def unlink_identity
IdentityProvider.unlink(email_address: email_address, from: tenant)
end
def update_email_address_on_identity
old_email, new_email = saved_change_to_email_address
IdentityProvider.change_email_address(from: old_email, to: new_email, tenant: tenant)
end
end
+9 -7
View File
@@ -1,5 +1,5 @@
<% @hide_footer_frames = true %>
<% @page_title = "Signing into #{Account.sole.name}" %>
<% @page_title = "Signing in..." %>
<% content_for :header do %>
<nav>
@@ -10,14 +10,16 @@
</nav>
<% end %>
<div
class="panel shadow center margin-block-double flex flex-column gap-half"
style="--panel-size: 42ch; view-transition-name: sign-in-panel; --popup-icon-size: 24px; --popup-item-padding-inline: 0.5rem;"
>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<div class="panel shadow center flex flex-column gap-half" style="--panel-size: 45ch;">
<header>
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none">Just a sec while we sign you in with <%= Account.sole.name %>.</p>
</header>
<%= form_with url: session_start_path, method: :post, data: { controller: "auto-submit" } do |form| %>
<%= form.button "Continue", type: "submit", class: "btn btn-primary" %>
<%= form.button "Sign in", type: "submit", class: "btn btn-primary", data: { turbo_submits_with: "Signing in..." } %>
<% end %>
</div>
Generated
+9
View File
@@ -24,6 +24,15 @@ ActiveRecord::Schema[8.2].define(version: 2025_10_29_161222) do
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "account_join_codes", force: :cascade do |t|
t.string "code", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "usage_count", default: 0, null: false
t.integer "usage_limit", default: 10, null: false
t.index ["code"], name: "index_account_join_codes_on_code", unique: true
end
create_table "accounts", force: :cascade do |t|
t.datetime "created_at", null: false
t.integer "external_account_id"
+631 -245
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
raise "Seeding is just for development" unless Rails.env.development?
IdentityProvider.backend = IdentityProvider::Simple
IdentityProvider.backend = IdentityProvider::LocalBackend
require "active_support/testing/time_helpers"
include ActiveSupport::Testing::TimeHelpers
+17 -6
View File
@@ -10,24 +10,35 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_09_24_190729) do
ActiveRecord::Schema[8.2].define(version: 2025_10_15_173452) do
create_table "identities", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "email_address"
t.datetime "updated_at", null: false
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
end
create_table "magic_links", force: :cascade do |t|
t.string "code", null: false
t.datetime "created_at", null: false
t.datetime "expires_at", null: false
t.integer "identity_id"
t.datetime "updated_at", null: false
t.index ["code"], name: "index_magic_links_on_code", unique: true
t.index ["expires_at"], name: "index_magic_links_on_expires_at"
t.index ["identity_id"], name: "index_magic_links_on_identity_id"
end
create_table "memberships", force: :cascade do |t|
t.string "account_name", null: false
t.datetime "created_at", null: false
t.string "email_address", null: false
t.integer "identity_id", null: false
t.string "tenant", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.string "user_tenant", null: false
t.index ["email_address"], name: "index_memberships_on_email_address"
t.index ["identity_id"], name: "index_memberships_on_identity_id"
t.index ["user_tenant", "user_id"], name: "index_memberships_on_user_tenant_and_user_id"
t.index ["tenant"], name: "index_memberships_on_user_tenant_and_user_id"
end
add_foreign_key "magic_links", "identities"
add_foreign_key "memberships", "identities"
end
@@ -2,22 +2,22 @@ class IdentitiesController < ApplicationController
include InternalApi
def link
IdentityProvider::Simple.link(email_address: params[:email_address], to: params[:to])
IdentityProvider::LocalBackend.link(email_address: params[:email_address], to: params[:to])
head :ok
end
def unlink
IdentityProvider::Simple.unlink(email_address: params[:email_address], from: params[:from])
IdentityProvider::LocalBackend.unlink(email_address: params[:email_address], from: params[:from])
head :ok
end
def change_email_address
IdentityProvider::Simple.change_email_address(from: params[:from], to: params[:to], tenant: params[:tenant])
IdentityProvider::LocalBackend.change_email_address(from: params[:from], to: params[:to], tenant: params[:tenant])
head :ok
end
def send_magic_link
code = IdentityProvider::Simple.send_magic_link(params[:email_address])
code = IdentityProvider::LocalBackend.send_magic_link(params[:email_address])
render json: { code: code }
end
end
@@ -2,7 +2,7 @@ class Signups::CompletionsController < ApplicationController
include Restricted
require_untenanted_access
before_action :require_identity
require_identified_access
def new
@signup = Signup.new
@@ -2,6 +2,7 @@ class SignupsController < ApplicationController
include Restricted
require_untenanted_access
require_unidentified_access
rate_limit only: :create, name: "short-term", to: 5, within: 3.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
@@ -0,0 +1,48 @@
module IdentityProvider::RemoteBackend
extend self
include Fizzy::Saas::Engine.routes.url_helpers
delegate :consume_magic_link, :token_for, :resolve_token, :verify_token, :tenants_for, to: IdentityProvider::LocalBackend
def default_url_options
Rails.application.config.action_mailer.default_url_options
end
def url_options
default_url_options.merge(script_name: nil)
end
def link(email_address:, to:)
response = InternalApiClient.new(link_identity_url).post({ email_address: email_address, to: to })
unless response.success?
raise IdentityProvider::Error, "Failed to link identity: #{response.error || response.code}"
end
end
def unlink(email_address:, from:)
response = InternalApiClient.new(unlink_identity_url).post({ email_address: email_address, from: from })
unless response.success?
raise IdentityProvider::Error, "Failed to unlink identity: #{response.error || response.code}"
end
end
def change_email_address(from:, to:, tenant:)
response = InternalApiClient.new(change_identity_email_address_url).post({ from: from, to: to, tenant: tenant })
unless response.success?
raise IdentityProvider::Error, "Failed to change email address: #{response.error || response.code}"
end
end
def send_magic_link(email_address)
response = InternalApiClient.new(send_magic_link_url).post({ email_address: email_address })
if response.success?
response.parsed_body["code"]
else
raise IdentityProvider::Error, "Failed to send magic link: #{response.error || response.code}"
end
end
end
@@ -1,83 +0,0 @@
module IdentityProvider::Saas
class Error < StandardError; end
extend self
include Fizzy::Saas::Engine.routes.url_helpers
def default_url_options
Rails.application.config.action_mailer.default_url_options
end
def url_options
default_url_options.merge(script_name: nil)
end
def link(email_address:, to:)
response = InternalApiClient.new(link_identity_url).post({ email_address: email_address, to: to })
unless response.success?
raise Error, "Failed to link identity: #{response.error || response.code}"
end
end
def unlink(email_address:, from:)
response = InternalApiClient.new(unlink_identity_url).post({ email_address: email_address, from: from })
unless response.success?
raise Error, "Failed to unlink identity: #{response.error || response.code}"
end
end
def change_email_address(from:, to:, tenant:)
response = InternalApiClient.new(change_identity_email_address_url).post({ from: from, to: to, tenant: tenant })
unless response.success?
raise Error, "Failed to change email address: #{response.error || response.code}"
end
end
def send_magic_link(email_address)
response = InternalApiClient.new(send_magic_link_url).post({ email_address: email_address })
if response.success?
response.parsed_body["code"]
else
raise Error, "Failed to send magic link: #{response.error || response.code}"
end
end
def consume_magic_link(code)
identity = MagicLink.consume(code)
wrap_identity(identity)
end
def token_for(email_address)
identity = Identity.find_by(email_address: email_address)
wrap_identity(identity)
end
def resolve_token(token)
identity = Identity.find_signed(token&.dig("id"))
identity&.email_address
end
def verify_token(token)
identity = Identity.find_signed(token&.dig("id"))
wrap_identity(identity)
end
def tenants_for(token)
Identity.find_signed(token&.dig("id")).memberships.pluck(:tenant, :account_name).map do |id, name|
IdentityProvider::Tenant.new(id: id, name: name)
end
end
private
def wrap_identity(identity)
if identity
IdentityProvider::Token.new(identity.signed_id, identity.updated_at)
else
nil
end
end
end
@@ -3,7 +3,7 @@ class InternalApiClient
TOKEN_KEY = "internal_api_client_token"
USER_AGENT = "fizzy/1.0.0 InternalApiClient"
SIGNATURE_HEADER = "X-Internal-Signature"
DEFAULT_TIMEOUT = 5.seconds
DEFAULT_TIMEOUT = 60.seconds
class Error < StandardError; end
class TimeoutError < Error; end
+1 -1
View File
@@ -5,7 +5,7 @@ module Fizzy
Queenbee.host_app = Fizzy
config.to_prepare do
IdentityProvider.backend = IdentityProvider::Saas
IdentityProvider.backend = IdentityProvider::RemoteBackend
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
@@ -33,7 +33,8 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
}, headers: http_basic_auth_headers
end
assert_redirected_to session_login_menu_path(go_to: Membership.last.tenant), "Successful completion should redirect to login menu"
tenant = Membership.last.tenant
assert_redirected_to new_session_start_url(script_name: "/#{tenant}"), "Successful completion should redirect to start session in new tenant"
post saas.signup_completion_path, params: {
signup: {
@@ -1,32 +1,32 @@
require "test_helper"
class IdentityProvider::SaasTest < ActiveSupport::TestCase
class IdentityProvider::RemoteBackendTest < ActiveSupport::TestCase
setup do
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/link})
.to_return do |request|
body = JSON.parse(request.body)
IdentityProvider::Simple.link(email_address: body["email_address"], to: body["to"])
IdentityProvider::LocalBackend.link(email_address: body["email_address"], to: body["to"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/unlink})
.to_return do |request|
body = JSON.parse(request.body)
IdentityProvider::Simple.unlink(email_address: body["email_address"], from: body["from"])
IdentityProvider::LocalBackend.unlink(email_address: body["email_address"], from: body["from"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/change_email_address})
.to_return do |request|
body = JSON.parse(request.body)
IdentityProvider::Simple.change_email_address(from: body["from"], to: body["to"], tenant: body["tenant"])
IdentityProvider::LocalBackend.change_email_address(from: body["from"], to: body["to"], tenant: body["tenant"])
{ status: 200, body: {}.to_json, headers: { "Content-Type" => "application/json" } }
end
WebMock.stub_request(:post, %r{http://example\.com(:80)?/identities/send_magic_link})
.to_return do |request|
body = JSON.parse(request.body)
code = IdentityProvider::Simple.send_magic_link(body["email_address"])
code = IdentityProvider::LocalBackend.send_magic_link(body["email_address"])
{ status: 200, body: { code: code }.to_json, headers: { "Content-Type" => "application/json" } }
end
end
@@ -37,7 +37,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
assert_difference -> { Identity.count }, 1 do
assert_difference -> { Membership.count }, 1 do
IdentityProvider::Saas.link(email_address: new_email, to: tenant)
IdentityProvider::RemoteBackend.link(email_address: new_email, to: tenant)
end
end
@@ -50,7 +50,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
assert_difference -> { Membership.count }, -1 do
IdentityProvider::Saas.unlink(email_address: identity.email_address, from: tenant)
IdentityProvider::RemoteBackend.unlink(email_address: identity.email_address, from: tenant)
end
end
@@ -59,7 +59,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
new_email = "newemail@example.com"
IdentityProvider::Saas.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
IdentityProvider::RemoteBackend.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
new_identity = Identity.find_by(email_address: new_email)
assert new_identity.memberships.exists?(tenant: tenant)
@@ -69,11 +69,11 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
code = IdentityProvider::Saas.send_magic_link(identity.email_address)
code = IdentityProvider::RemoteBackend.send_magic_link(identity.email_address)
assert_equal MagicLink::CODE_LENGTH, code.length
end
code = IdentityProvider::Saas.send_magic_link("nonexistent@example.com")
code = IdentityProvider::RemoteBackend.send_magic_link("nonexistent@example.com")
assert_nil code
end
@@ -81,21 +81,21 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
magic_link = identity.send_magic_link
token = IdentityProvider::Saas.consume_magic_link(magic_link.code)
token = IdentityProvider::RemoteBackend.consume_magic_link(magic_link.code)
assert_equal identity.signed_id, token.id
assert_not MagicLink.exists?(magic_link.id)
token = IdentityProvider::Saas.consume_magic_link("invalid")
token = IdentityProvider::RemoteBackend.consume_magic_link("invalid")
assert_nil token
end
test "token_for" do
identity = identities(:kevin)
token = IdentityProvider::Saas.token_for(identity.email_address)
token = IdentityProvider::RemoteBackend.token_for(identity.email_address)
assert_equal identity.signed_id, token.id
token = IdentityProvider::Saas.token_for("nonexistent@example.com")
token = IdentityProvider::RemoteBackend.token_for("nonexistent@example.com")
assert_nil token
end
@@ -103,10 +103,10 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
email = IdentityProvider::Saas.resolve_token(token)
email = IdentityProvider::RemoteBackend.resolve_token(token)
assert_equal identity.email_address, email
email = IdentityProvider::Saas.resolve_token({ "id" => "invalid" })
email = IdentityProvider::RemoteBackend.resolve_token({ "id" => "invalid" })
assert_nil email
end
@@ -114,10 +114,10 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
result = IdentityProvider::Saas.verify_token(token)
result = IdentityProvider::RemoteBackend.verify_token(token)
assert_equal identity.signed_id, result.id
result = IdentityProvider::Saas.verify_token({ "id" => "invalid" })
result = IdentityProvider::RemoteBackend.verify_token({ "id" => "invalid" })
assert_nil result
end
@@ -125,7 +125,7 @@ class IdentityProvider::SaasTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
tenants = IdentityProvider::Saas.tenants_for(token)
tenants = IdentityProvider::RemoteBackend.tenants_for(token)
assert tenants.all? { |t| t.is_a?(IdentityProvider::Tenant) }
assert_includes tenants.map(&:id), identity.memberships.first.tenant
end
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
# Loop through all tenants and create identities for users
ApplicationRecord.with_each_tenant do |tenant|
puts "Processing tenant: #{tenant}"
User.find_each do |user|
next if user.system?
# Use IdentityProvider to link the user's email to this tenant
# This will find_or_create the identity and link it to the tenant
identity = IdentityProvider.link(email_address: user.email_address, to: tenant)
puts " ✅ Linked identity for user #{user.id} (#{user.email_address}) to tenant '#{tenant}'"
end
puts " Completed tenant: #{tenant}"
puts
end
puts "All identities created successfully!"
@@ -1,13 +1,13 @@
require "test_helper"
class IdentityProvider::SimpleTest < ActiveSupport::TestCase
class IdentityProvider::LocalBackendTest < ActiveSupport::TestCase
test "link" do
new_email = "newuser@example.com"
tenant = ApplicationRecord.current_tenant
assert_difference -> { Identity.count }, 1 do
assert_difference -> { Membership.count }, 1 do
IdentityProvider::Simple.link(email_address: new_email, to: tenant)
IdentityProvider::LocalBackend.link(email_address: new_email, to: tenant)
end
end
@@ -20,7 +20,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
assert_difference -> { Membership.count }, -1 do
IdentityProvider::Simple.unlink(email_address: identity.email_address, from: tenant)
IdentityProvider::LocalBackend.unlink(email_address: identity.email_address, from: tenant)
end
assert_not identity.reload.memberships.exists?(tenant: tenant), "removes membership from tenant"
@@ -31,7 +31,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
tenant = ApplicationRecord.current_tenant
new_email = "newemail@example.com"
IdentityProvider::Simple.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
IdentityProvider::LocalBackend.change_email_address(from: identity.email_address, to: new_email, tenant: tenant)
assert_not identity.reload.memberships.exists?(tenant: tenant), "removes old identity membership"
new_identity = Identity.find_by(email_address: new_email)
@@ -42,11 +42,11 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
code = IdentityProvider::Simple.send_magic_link(identity.email_address)
code = IdentityProvider::LocalBackend.send_magic_link(identity.email_address)
assert_equal MagicLink::CODE_LENGTH, code.length, "returns code of correct length"
end
code = IdentityProvider::Simple.send_magic_link("nonexistent@example.com")
code = IdentityProvider::LocalBackend.send_magic_link("nonexistent@example.com")
assert_nil code, "returns nil for non-existent email"
end
@@ -54,21 +54,21 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
magic_link = identity.send_magic_link
token = IdentityProvider::Simple.consume_magic_link(magic_link.code)
token = IdentityProvider::LocalBackend.consume_magic_link(magic_link.code)
assert_equal identity.signed_id, token.id, "returns token for valid code"
assert_not MagicLink.exists?(magic_link.id), "deletes magic link after consumption"
token = IdentityProvider::Simple.consume_magic_link("invalid")
token = IdentityProvider::LocalBackend.consume_magic_link("invalid")
assert_nil token, "returns nil for invalid code"
end
test "token_for" do
identity = identities(:kevin)
token = IdentityProvider::Simple.token_for(identity.email_address)
token = IdentityProvider::LocalBackend.token_for(identity.email_address)
assert_equal identity.signed_id, token.id, "returns token for existing email"
token = IdentityProvider::Simple.token_for("nonexistent@example.com")
token = IdentityProvider::LocalBackend.token_for("nonexistent@example.com")
assert_nil token, "returns nil for non-existent email"
end
@@ -76,10 +76,10 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
email = IdentityProvider::Simple.resolve_token(token)
email = IdentityProvider::LocalBackend.resolve_token(token)
assert_equal identity.email_address, email, "returns email address from valid token"
email = IdentityProvider::Simple.resolve_token({ "id" => "invalid" })
email = IdentityProvider::LocalBackend.resolve_token({ "id" => "invalid" })
assert_nil email, "returns nil for invalid token"
end
@@ -87,10 +87,10 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
result = IdentityProvider::Simple.verify_token(token)
result = IdentityProvider::LocalBackend.verify_token(token)
assert_equal identity.signed_id, result.id, "returns token from valid token hash"
result = IdentityProvider::Simple.verify_token({ "id" => "invalid" })
result = IdentityProvider::LocalBackend.verify_token({ "id" => "invalid" })
assert_nil result, "returns nil for invalid token"
end
@@ -98,7 +98,7 @@ class IdentityProvider::SimpleTest < ActiveSupport::TestCase
identity = identities(:kevin)
token = { "id" => identity.signed_id }
tenants = IdentityProvider::Simple.tenants_for(token)
tenants = IdentityProvider::LocalBackend.tenants_for(token)
assert tenants.all? { |t| t.is_a?(IdentityProvider::Tenant) }, "returns Tenant objects"
assert_includes tenants.map(&:id), identity.memberships.first.tenant, "includes identity's tenant"
end
@@ -1,14 +1,26 @@
require "test_helper"
class User::EmailAddressChangeableTest < ActiveSupport::TestCase
test "generate_email_address_change_token" do
test "send_email_address_change_confirmation" do
user = users(:david)
new_email_address = "new@example.com"
token = user.generate_email_address_change_token(to: new_email_address)
assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
user.send_email_address_change_confirmation(new_email_address)
end
assert_not_equal new_email_address, user.reload.email_address
end
test "generate_email_address_change_token" do
user = users(:david)
old_email = user.email_address
new_email = "david.new@37signals.com"
token = user.generate_email_address_change_token(to: new_email, expires_in: 30.minutes)
assert_kind_of String, token
assert_not_equal new_email_address, user.reload.email_address
assert token.present?
end
test "change_email_address_using_token" do
@@ -16,7 +28,7 @@ class User::EmailAddressChangeableTest < ActiveSupport::TestCase
old_email = user.email_address
new_email = "david.new@37signals.com"
token = user.generate_email_address_change_token(from: old_email, to: new_email)
token = user.generate_email_address_change_token(to: new_email, expires_in: 30.minutes)
assert_equal old_email, user.reload.email_address
-14
View File
@@ -12,20 +12,6 @@ class User::IdentifiableTest < ActiveSupport::TestCase
assert_equal "newuser@example.com", user.identity.email_address
end
test "update email address" do
user = users(:david)
old_email = user.email_address
new_email = "david.updated@example.com"
assert_not Identity.find_by(email_address: new_email)
user.update!(email_address: new_email)
new_identity = Identity.find_by(email_address: new_email)
assert new_identity.present?
assert new_identity.memberships.exists?(tenant: user.tenant)
end
test "destroy" do
user = User.create!(name: "Bob")
+31 -1
View File
@@ -43,7 +43,37 @@ class SmokeTest < ApplicationSystemTestCase
private
def sign_in_as(user)
visit session_transfer_url(user.transfer_id)
# Visit app first to establish domain for cookies
visit root_url
identity = Identity.find_or_create_by!(email_address: user.email_address)
identity.link_to(user.tenant)
session = user.sessions.create!(user_agent: "Test", ip_address: "127.0.0.1")
secret_key_base = Rails.application.secret_key_base
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
secret = key_generator.generate_key("signed cookie")
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
identity_token = IdentityProvider::Token.new(identity.signed_id, identity.updated_at)
signed_identity_token = verifier.generate(identity_token.to_h)
signed_session_token = verifier.generate(session.signed_id)
# Set cookies in browser
Capybara.current_session.driver.browser.manage.add_cookie(
name: "identity_token",
value: signed_identity_token,
path: "/"
)
Capybara.current_session.driver.browser.manage.add_cookie(
name: "session_token",
value: signed_session_token,
path: "/#{user.tenant}"
)
visit root_url
assert_selector "h1", text: "Activity"
end
+1 -1
View File
@@ -66,4 +66,4 @@ unless Rails.application.config.x.oss_config
load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__)
end
IdentityProvider.backend = IdentityProvider::Simple
IdentityProvider.backend = IdentityProvider::LocalBackend