Update tests for the Internal API
This commit is contained in:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
module IdentityProvider::Simple
|
||||
module IdentityProvider::LocalBackend
|
||||
extend self
|
||||
|
||||
def link(email_address:, to:)
|
||||
+9
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+19
-19
@@ -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
|
||||
@@ -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!"
|
||||
+15
-15
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user