diff --git a/app/interactors/datapass_webhook/create_or_prolong_token.rb b/app/interactors/datapass_webhook/create_or_prolong_token.rb index d1c7530cc..e2be25d77 100644 --- a/app/interactors/datapass_webhook/create_or_prolong_token.rb +++ b/app/interactors/datapass_webhook/create_or_prolong_token.rb @@ -7,10 +7,11 @@ def call return if %w[approve validate].exclude?(context.event) return if context.modalities.exclude?('params') + affect_scopes_to_authorization_request token = create_or_prolong_token if token.persisted? - affect_scopes(token) + copy_scopes_to_token(token) context.token_id = token.id else context.fail!(message: 'Fail to create token') @@ -44,8 +45,12 @@ def create_token ) end - def affect_scopes(token) - token.update!(scopes:) + def affect_scopes_to_authorization_request + authorization_request.update!(scopes:) + end + + def copy_scopes_to_token(token) + token.update!(scopes: authorization_request.scopes) end def token_already_exists? diff --git a/app/interactors/datapass_webhook/extract_mailjet_variables.rb b/app/interactors/datapass_webhook/extract_mailjet_variables.rb index fa527591e..b578ed3de 100644 --- a/app/interactors/datapass_webhook/extract_mailjet_variables.rb +++ b/app/interactors/datapass_webhook/extract_mailjet_variables.rb @@ -44,11 +44,11 @@ def add_token_scopes end def token_present? - authorization_request.token.present? + authorization_request.scopes.present? end def token_roles - @token_roles ||= authorization_request.token.scopes + @token_roles ||= authorization_request.scopes end def events_from_instructor diff --git a/app/lib/seeds.rb b/app/lib/seeds.rb index 42d327d46..5068ba0d7 100644 --- a/app/lib/seeds.rb +++ b/app/lib/seeds.rb @@ -12,6 +12,7 @@ def perform create_data_for_api_particulier create_data_shared create_audit_notifications + create_oauth2_test_data end def flushdb @@ -31,6 +32,38 @@ def create_scopes(api) YAML.load_file(Rails.root.join("config/data/scopes/#{api}.yml")) end + def create_oauth2_test_data + ar = create_authorization_request( + external_id: '9001', + intitule: 'Test OAuth2 Authorization Request', + status: 'validated', + api: 'entreprise', + siret: '13002526500013', + scopes: %w[unites_legales_etablissements_insee associations_djepva], + validated_at: Time.current, + first_submitted_at: Time.current + ) + + Token.create!( + authorization_request: ar, + iat: Time.current.to_i, + exp: 18.months.from_now.to_i, + version: '1.0', + scopes: ar.scopes + ) + + editor = Editor.find_by!(name: 'UMAD Corp') + oauth_app = OAuthApplication.create!( + name: "OAuth - #{editor.name}", + owner: editor, + uid: 'oauth-test-client-id', + secret: 'oauth-test-client-secret' + ) + editor.update!(oauth_application: oauth_app) + + EditorDelegation.create!(editor:, authorization_request: ar) + end + private def create_data_for_api_entreprise diff --git a/app/mailers/api_entreprise/authorization_request_mailer.rb b/app/mailers/api_entreprise/authorization_request_mailer.rb index 45314b437..944c7d187 100644 --- a/app/mailers/api_entreprise/authorization_request_mailer.rb +++ b/app/mailers/api_entreprise/authorization_request_mailer.rb @@ -22,7 +22,7 @@ class APIEntreprise::AuthorizationRequestMailer < APIEntrepriseMailer send('define_method', method) do |args| @all_scopes = I18n.t('api_entreprise.tokens.token.scope') @authorization_request = args[:authorization_request] - @authorization_request_scopes = @authorization_request.token.scopes.map(&:to_sym) if @authorization_request.token.present? + @authorization_request_scopes = @authorization_request.scopes.map(&:to_sym) if @authorization_request.scopes.present? @authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request) @full_name_demandeur = @authorization_request.demandeur.full_name diff --git a/app/mailers/api_particulier/authorization_request_mailer.rb b/app/mailers/api_particulier/authorization_request_mailer.rb index be01a15ba..8621b4f85 100644 --- a/app/mailers/api_particulier/authorization_request_mailer.rb +++ b/app/mailers/api_particulier/authorization_request_mailer.rb @@ -18,7 +18,7 @@ class APIParticulier::AuthorizationRequestMailer < APIParticulierMailer send('define_method', method) do |args| @all_scopes = I18n.t('api_particulier.tokens.token.scope') @authorization_request = args[:authorization_request] - @authorization_request_scopes = @authorization_request.token.scopes.map(&:to_sym) if @authorization_request.token.present? + @authorization_request_scopes = @authorization_request.scopes.map(&:to_sym) if @authorization_request.scopes.present? @authorization_request_datapass_url = datapass_authorization_request_url(@authorization_request) @full_name_demandeur = @authorization_request.demandeur.full_name diff --git a/app/models/authorization_request.rb b/app/models/authorization_request.rb index 8ce9e46ca..fa74d6d41 100644 --- a/app/models/authorization_request.rb +++ b/app/models/authorization_request.rb @@ -6,6 +6,10 @@ class AuthorizationRequest < ApplicationRecord optional: true, dependent: nil + belongs_to :oauth_application, optional: true + + has_many :editor_delegations, dependent: :destroy + has_many :user_authorization_request_roles, dependent: :destroy do def for_user(user) where(user:) @@ -104,7 +108,6 @@ def archive! def revoke! token&.update!(blacklisted_at: Time.zone.now) - update!(status: 'revoked') end @@ -112,4 +115,12 @@ def prolong_token_expecting_updates? token&.last_prolong_token_wizard.present? && token.last_prolong_token_wizard.requires_update? end + + def generate_oauth_credentials! + return oauth_application if oauth_application.present? + + OAuthApplication.create!(name: "OAuth - #{intitule || external_id}", owner: self).tap { update!(oauth_application: it) } + end + + def oauth_scopes = scopes end diff --git a/app/models/editor.rb b/app/models/editor.rb index 14545a1b0..be837f081 100644 --- a/app/models/editor.rb +++ b/app/models/editor.rb @@ -1,6 +1,11 @@ class Editor < ApplicationRecord - has_many :users, - dependent: :nullify + belongs_to :oauth_application, optional: true + + has_many :users, dependent: :nullify + has_many :editor_delegations, dependent: :destroy + has_many :delegated_authorization_requests, + through: :editor_delegations, + source: :authorization_request validates :name, presence: true @@ -9,4 +14,16 @@ def authorization_requests(api:) .where(api:) .where(demarche: form_uids) end + + def generate_oauth_credentials! + return oauth_application if oauth_application.present? + + oauth_app = OAuthApplication.create!(name: "OAuth - #{name}", owner: self) + update!(oauth_application: oauth_app) + oauth_app + end + + def can_access_authorization_request?(authorization_request) + editor_delegations.active.exists?(authorization_request:) + end end diff --git a/app/models/editor_delegation.rb b/app/models/editor_delegation.rb new file mode 100644 index 000000000..0e34ee2b8 --- /dev/null +++ b/app/models/editor_delegation.rb @@ -0,0 +1,19 @@ +class EditorDelegation < ApplicationRecord + belongs_to :editor + belongs_to :authorization_request + + scope :active, -> { where(revoked_at: nil) } + scope :revoked, -> { where.not(revoked_at: nil) } + + def revoke! + update!(revoked_at: Time.current) + end + + def revoked? + revoked_at.present? + end + + def active? + !revoked? + end +end diff --git a/app/models/oauth_application.rb b/app/models/oauth_application.rb new file mode 100644 index 000000000..20055c25d --- /dev/null +++ b/app/models/oauth_application.rb @@ -0,0 +1,19 @@ +class OAuthApplication < ApplicationRecord + belongs_to :owner, polymorphic: true, optional: true + + has_many :editors, dependent: :nullify + has_many :authorization_requests, dependent: :nullify + + validates :name, presence: true + validates :uid, presence: true, uniqueness: true + validates :secret, presence: true + + before_validation :generate_credentials, on: :create + + private + + def generate_credentials + self.uid ||= SecureRandom.hex(32) + self.secret ||= SecureRandom.hex(64) + end +end diff --git a/db/migrate/20260122100000_create_oauth_applications.rb b/db/migrate/20260122100000_create_oauth_applications.rb new file mode 100644 index 000000000..cc221b034 --- /dev/null +++ b/db/migrate/20260122100000_create_oauth_applications.rb @@ -0,0 +1,17 @@ +class CreateOAuthApplications < ActiveRecord::Migration[8.1] + def change + create_table :oauth_applications, id: :uuid, default: -> { 'gen_random_uuid()' } do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.string :scopes, default: '', null: false + t.string :owner_type + t.uuid :owner_id + + t.timestamps + end + + add_index :oauth_applications, :uid, unique: true + add_index :oauth_applications, %i[owner_type owner_id] + end +end diff --git a/db/migrate/20260122100001_create_editor_delegations.rb b/db/migrate/20260122100001_create_editor_delegations.rb new file mode 100644 index 000000000..4c0f8d6c3 --- /dev/null +++ b/db/migrate/20260122100001_create_editor_delegations.rb @@ -0,0 +1,16 @@ +class CreateEditorDelegations < ActiveRecord::Migration[8.1] + def change + create_table :editor_delegations, id: :uuid, default: -> { 'gen_random_uuid()' } do |t| + t.references :editor, null: false, foreign_key: true, type: :uuid + t.references :authorization_request, null: false, foreign_key: true, type: :uuid + t.datetime :revoked_at + + t.timestamps + end + + add_index :editor_delegations, %i[editor_id authorization_request_id], + unique: true, + where: 'revoked_at IS NULL', + name: 'index_active_editor_delegations_unique' + end +end diff --git a/db/migrate/20260122100002_add_oauth_application_to_editors.rb b/db/migrate/20260122100002_add_oauth_application_to_editors.rb new file mode 100644 index 000000000..62ee1685a --- /dev/null +++ b/db/migrate/20260122100002_add_oauth_application_to_editors.rb @@ -0,0 +1,7 @@ +class AddOAuthApplicationToEditors < ActiveRecord::Migration[8.1] + disable_ddl_transaction! + + def change + add_reference :editors, :oauth_application, type: :uuid, index: { algorithm: :concurrently } + end +end diff --git a/db/migrate/20260122100003_add_oauth_application_to_authorization_requests.rb b/db/migrate/20260122100003_add_oauth_application_to_authorization_requests.rb new file mode 100644 index 000000000..17d022c48 --- /dev/null +++ b/db/migrate/20260122100003_add_oauth_application_to_authorization_requests.rb @@ -0,0 +1,7 @@ +class AddOAuthApplicationToAuthorizationRequests < ActiveRecord::Migration[8.1] + disable_ddl_transaction! + + def change + add_reference :authorization_requests, :oauth_application, type: :uuid, index: { algorithm: :concurrently } + end +end diff --git a/db/migrate/20260123100000_add_scopes_to_authorization_requests.rb b/db/migrate/20260123100000_add_scopes_to_authorization_requests.rb new file mode 100644 index 000000000..9e9e2b065 --- /dev/null +++ b/db/migrate/20260123100000_add_scopes_to_authorization_requests.rb @@ -0,0 +1,8 @@ +class AddScopesToAuthorizationRequests < ActiveRecord::Migration[8.1] + disable_ddl_transaction! + + def change + add_column :authorization_requests, :scopes, :jsonb, default: [], null: false + add_index :authorization_requests, :scopes, using: :gin, algorithm: :concurrently + end +end diff --git a/db/migrate/20260123100001_backfill_scopes_on_authorization_requests.rb b/db/migrate/20260123100001_backfill_scopes_on_authorization_requests.rb new file mode 100644 index 000000000..db4d3d6de --- /dev/null +++ b/db/migrate/20260123100001_backfill_scopes_on_authorization_requests.rb @@ -0,0 +1,15 @@ +class BackfillScopesOnAuthorizationRequests < ActiveRecord::Migration[8.1] + disable_ddl_transaction! + + def up + AuthorizationRequest.find_each do |ar| + next if ar.token.blank? + + ar.update!(scopes: ar.token.scopes) + end + end + + def down + AuthorizationRequest.update_all(scopes: []) # rubocop:disable Rails/SkipsModelValidations + end +end diff --git a/db/schema.rb b/db/schema.rb index 03571da00..a807dcb53 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,13 +10,13 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_14_112301) do +ActiveRecord::Schema[8.1].define(version: 2026_01_23_100001) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" - create_table "access_logs", id: false, force: false, if_not_exists: true do |t| + create_table "access_logs", id: false, force: :cascade do |t| t.string "path", null: false t.uuid "request_id", null: false t.timestamptz "timestamp", null: false @@ -45,12 +45,27 @@ t.datetime "first_submitted_at", precision: nil t.string "intitule" t.datetime "last_update", precision: nil + t.uuid "oauth_application_id" t.string "previous_external_id" t.uuid "public_id" + t.jsonb "scopes", default: [], null: false t.string "siret" t.string "status" t.datetime "validated_at", precision: nil t.index ["external_id"], name: "index_authorization_requests_on_external_id", unique: true, where: "(external_id IS NOT NULL)" + t.index ["oauth_application_id"], name: "index_authorization_requests_on_oauth_application_id" + t.index ["scopes"], name: "index_authorization_requests_on_scopes", using: :gin + end + + create_table "editor_delegations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "authorization_request_id", null: false + t.datetime "created_at", null: false + t.uuid "editor_id", null: false + t.datetime "revoked_at" + t.datetime "updated_at", null: false + t.index ["authorization_request_id"], name: "index_editor_delegations_on_authorization_request_id" + t.index ["editor_id", "authorization_request_id"], name: "index_active_editor_delegations_unique", unique: true, where: "(revoked_at IS NULL)" + t.index ["editor_id"], name: "index_editor_delegations_on_editor_id" end create_table "editors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -58,7 +73,9 @@ t.datetime "created_at", null: false t.string "form_uids", default: [], array: true t.string "name", null: false + t.uuid "oauth_application_id" t.datetime "updated_at", null: false + t.index ["oauth_application_id"], name: "index_editors_on_oauth_application_id" end create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -160,6 +177,19 @@ t.index ["token_id"], name: "index_magic_links_on_token_id" end + create_table "oauth_applications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.string "name", null: false + t.uuid "owner_id" + t.string "owner_type" + t.string "scopes", default: "", null: false + t.string "secret", null: false + t.string "uid", null: false + t.datetime "updated_at", null: false + t.index ["owner_type", "owner_id"], name: "index_oauth_applications_on_owner_type_and_owner_id" + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + create_table "organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.jsonb "insee_payload", default: {} @@ -228,6 +258,8 @@ t.index ["email"], name: "index_users_on_email", unique: true end + add_foreign_key "editor_delegations", "authorization_requests" + add_foreign_key "editor_delegations", "editors" add_foreign_key "magic_links", "tokens" add_foreign_key "prolong_token_wizards", "tokens" add_foreign_key "user_authorization_request_roles", "authorization_requests" diff --git a/lib/tasks/oauth.rake b/lib/tasks/oauth.rake new file mode 100644 index 000000000..81c8e7cf9 --- /dev/null +++ b/lib/tasks/oauth.rake @@ -0,0 +1,50 @@ +namespace :oauth do + desc "Create OAuth app for an owner oauth:create_app\\['Editor','UUID'\\] or oauth:create_app\\['AuthorizationRequest','UUID'\\]" + task :create_app, %i[owner_type owner_id] => :environment do |_, args| + owner = args.owner_type.constantize.find(args.owner_id) + oauth_app = owner.generate_oauth_credentials! + + puts 'OAuth Application created:' + puts " Client ID: #{oauth_app.uid}" + puts " Client Secret: #{oauth_app.secret}" + end + + desc "Create delegation for editor oauth:create_delegation\\['EDITOR_UUID','AR_UUID'\\]" + task :create_delegation, %i[editor_id authorization_request_id] => :environment do |_, args| + editor = Editor.find(args.editor_id) + authorization_request = AuthorizationRequest.find(args.authorization_request_id) + + delegation = EditorDelegation.create!( + editor:, + authorization_request: + ) + + puts "Delegation created: #{delegation.id}" + end + + desc "Revoke a delegation oauth:revoke_delegation\\['DELEGATION_UUID'\\]" + task :revoke_delegation, [:delegation_id] => :environment do |_, args| + delegation = EditorDelegation.find(args.delegation_id) + delegation.revoke! + + puts "Delegation #{delegation.id} revoked" + end + + desc 'List all OAuth applications' + task list_apps: :environment do + OAuthApplication.includes(:owner).find_each do |app| + puts "#{app.id} - #{app.name} (#{app.owner_type}: #{app.owner&.id})" + end + end + + desc "List all delegations for an editor oauth:list_delegations\\['EDITOR_UUID'\\]" + task :list_delegations, [:editor_id] => :environment do |_, args| + editor = Editor.find(args.editor_id) + + editor.editor_delegations.includes(:authorization_request).find_each do |delegation| + status = delegation.active? ? 'active' : 'revoked' + ar = delegation.authorization_request + puts "#{delegation.id} - AR: #{ar.external_id} (#{status})" + end + end +end diff --git a/spec/factories/authorization_requests.rb b/spec/factories/authorization_requests.rb index cf903b2c0..71c868c6a 100644 --- a/spec/factories/authorization_requests.rb +++ b/spec/factories/authorization_requests.rb @@ -7,6 +7,7 @@ api { 'entreprise' } siret { '13002526500013' } public_id { SecureRandom.uuid } + scopes { [] } trait :without_external_id do external_id { nil } @@ -31,6 +32,8 @@ end trait :with_tokens do + scopes { ['entreprises'] } + tokens do [ build(:token) diff --git a/spec/factories/editor_delegations.rb b/spec/factories/editor_delegations.rb new file mode 100644 index 000000000..588ec3a02 --- /dev/null +++ b/spec/factories/editor_delegations.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :editor_delegation do + editor + authorization_request + + trait :revoked do + revoked_at { Time.current } + end + end +end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb new file mode 100644 index 000000000..0b39fad59 --- /dev/null +++ b/spec/factories/oauth_applications.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :oauth_application do + name { 'Test OAuth Application' } + + trait :for_editor do + owner factory: %i[editor] + end + + trait :for_authorization_request do + owner factory: %i[authorization_request] + end + end +end diff --git a/spec/interactors/datapass_webhook/create_or_prolong_token_spec.rb b/spec/interactors/datapass_webhook/create_or_prolong_token_spec.rb index 2a38ec49a..8f9d619c2 100644 --- a/spec/interactors/datapass_webhook/create_or_prolong_token_spec.rb +++ b/spec/interactors/datapass_webhook/create_or_prolong_token_spec.rb @@ -54,6 +54,7 @@ expect(token.exp).to eq(18.months.from_now.to_i) expect(token.iat).to eq(Time.zone.now.to_i) expect(token.scopes.sort).to eq(%w[associations entreprises]) + expect(authorization_request.reload.scopes.sort).to eq(%w[associations entreprises]) end context 'when there is some scopes starting with open_data_' do diff --git a/spec/interactors/datapass_webhook/extract_mailjet_variables_spec.rb b/spec/interactors/datapass_webhook/extract_mailjet_variables_spec.rb index fd4cc2302..5fd68387f 100644 --- a/spec/interactors/datapass_webhook/extract_mailjet_variables_spec.rb +++ b/spec/interactors/datapass_webhook/extract_mailjet_variables_spec.rb @@ -26,11 +26,9 @@ expect(subject.mailjet_variables['token_scopes']).to be_nil end - context 'when authorization request has a token' do - let!(:token) { create(:token, authorization_request:) } - + context 'when authorization request has scopes' do before do - token.update!(scopes: %w[entreprises liasse_fiscale]) + authorization_request.update!(scopes: %w[entreprises liasse_fiscale]) end it 'sets token_scopes with these values' do diff --git a/spec/models/authorization_request_spec.rb b/spec/models/authorization_request_spec.rb index bc216474f..0dc3bfa2e 100644 --- a/spec/models/authorization_request_spec.rb +++ b/spec/models/authorization_request_spec.rb @@ -176,4 +176,41 @@ it { is_expected.to include(*archived_authorization_request) } end end + + describe '#generate_oauth_credentials!' do + subject(:authorization_request) { create(:authorization_request, intitule: 'Test Intitule') } + + it 'creates an oauth_application' do + expect { authorization_request.generate_oauth_credentials! } + .to change(OAuthApplication, :count).by(1) + end + + it 'associates the oauth_application with the authorization_request' do + oauth_app = authorization_request.generate_oauth_credentials! + + expect(authorization_request.reload.oauth_application).to eq(oauth_app) + expect(oauth_app.owner).to eq(authorization_request) + end + + it 'returns existing oauth_application if already present' do + existing_app = authorization_request.generate_oauth_credentials! + + expect(authorization_request.generate_oauth_credentials!).to eq(existing_app) + expect(OAuthApplication.count).to eq(1) + end + end + + describe '#oauth_scopes' do + let(:authorization_request) { create(:authorization_request) } + + it 'returns empty array when no scopes' do + expect(authorization_request.oauth_scopes).to eq([]) + end + + it 'returns scopes from authorization_request' do + authorization_request.update!(scopes: %w[entreprises etablissements]) + + expect(authorization_request.oauth_scopes).to eq(%w[entreprises etablissements]) + end + end end diff --git a/spec/models/editor_delegation_spec.rb b/spec/models/editor_delegation_spec.rb new file mode 100644 index 000000000..8b07ecae2 --- /dev/null +++ b/spec/models/editor_delegation_spec.rb @@ -0,0 +1,66 @@ +RSpec.describe EditorDelegation do + it 'has a valid factory' do + expect(build(:editor_delegation)).to be_valid + end + + describe 'associations' do + it { is_expected.to belong_to(:editor) } + it { is_expected.to belong_to(:authorization_request) } + end + + describe 'scopes' do + let!(:active_delegation) { create(:editor_delegation) } + let!(:revoked_delegation) { create(:editor_delegation, :revoked) } + + describe '.active' do + it 'returns only active delegations' do + expect(described_class.active).to contain_exactly(active_delegation) + end + end + + describe '.revoked' do + it 'returns only revoked delegations' do + expect(described_class.revoked).to contain_exactly(revoked_delegation) + end + end + end + + describe '#revoke!' do + subject(:delegation) { create(:editor_delegation) } + + it 'sets revoked_at timestamp' do + delegation.revoke! + + expect(delegation.revoked_at).to be_present + expect(delegation.revoked_at).to be_within(1.second).of(Time.current) + end + end + + describe '#revoked?' do + it 'returns false when revoked_at is nil' do + delegation = build(:editor_delegation, revoked_at: nil) + + expect(delegation).not_to be_revoked + end + + it 'returns true when revoked_at is set' do + delegation = build(:editor_delegation, revoked_at: Time.current) + + expect(delegation).to be_revoked + end + end + + describe '#active?' do + it 'returns true when revoked_at is nil' do + delegation = build(:editor_delegation, revoked_at: nil) + + expect(delegation).to be_active + end + + it 'returns false when revoked_at is set' do + delegation = build(:editor_delegation, revoked_at: Time.current) + + expect(delegation).not_to be_active + end + end +end diff --git a/spec/models/editor_spec.rb b/spec/models/editor_spec.rb index d534ba956..6842efea1 100644 --- a/spec/models/editor_spec.rb +++ b/spec/models/editor_spec.rb @@ -26,4 +26,48 @@ it { is_expected.to match_array(valid_authorization_requests) } it { is_expected.to be_a(ActiveRecord::Relation) } end + + describe '#generate_oauth_credentials!' do + subject(:editor) { create(:editor) } + + it 'creates an oauth_application' do + expect { editor.generate_oauth_credentials! } + .to change(OAuthApplication, :count).by(1) + end + + it 'associates the oauth_application with the editor' do + oauth_app = editor.generate_oauth_credentials! + + expect(editor.reload.oauth_application).to eq(oauth_app) + expect(oauth_app.owner).to eq(editor) + end + + it 'returns existing oauth_application if already present' do + existing_app = editor.generate_oauth_credentials! + + expect(editor.generate_oauth_credentials!).to eq(existing_app) + expect(OAuthApplication.count).to eq(1) + end + end + + describe '#can_access_authorization_request?' do + let(:editor) { create(:editor) } + let(:authorization_request) { create(:authorization_request) } + + it 'returns true when active delegation exists' do + create(:editor_delegation, editor:, authorization_request:) + + expect(editor.can_access_authorization_request?(authorization_request)).to be true + end + + it 'returns false when no delegation exists' do + expect(editor.can_access_authorization_request?(authorization_request)).to be false + end + + it 'returns false when delegation is revoked' do + create(:editor_delegation, :revoked, editor:, authorization_request:) + + expect(editor.can_access_authorization_request?(authorization_request)).to be false + end + end end diff --git a/spec/models/o_auth_application_spec.rb b/spec/models/o_auth_application_spec.rb new file mode 100644 index 000000000..de04be6c6 --- /dev/null +++ b/spec/models/o_auth_application_spec.rb @@ -0,0 +1,62 @@ +RSpec.describe OAuthApplication do + it 'has a valid factory' do + expect(build(:oauth_application)).to be_valid + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:name) } + + it 'validates uid uniqueness' do + existing = create(:oauth_application) + duplicate = build(:oauth_application, uid: existing.uid) + + expect(duplicate).not_to be_valid + expect(duplicate.errors[:uid]).to be_present + end + end + + describe 'credential generation' do + subject(:oauth_application) { build(:oauth_application) } + + it 'generates uid before validation' do + oauth_application.valid? + + expect(oauth_application.uid).to be_present + expect(oauth_application.uid.length).to eq(64) + end + + it 'generates secret before validation' do + oauth_application.valid? + + expect(oauth_application.secret).to be_present + expect(oauth_application.secret.length).to eq(128) + end + + it 'does not override existing credentials' do + oauth_application.uid = 'custom_uid' + oauth_application.secret = 'custom_secret' + oauth_application.valid? + + expect(oauth_application.uid).to eq('custom_uid') + expect(oauth_application.secret).to eq('custom_secret') + end + end + + describe 'polymorphic owner' do + it 'can belong to an editor' do + editor = create(:editor) + oauth_application = create(:oauth_application, owner: editor) + + expect(oauth_application.owner).to eq(editor) + expect(oauth_application.owner_type).to eq('Editor') + end + + it 'can belong to an authorization_request' do + authorization_request = create(:authorization_request) + oauth_application = create(:oauth_application, owner: authorization_request) + + expect(oauth_application.owner).to eq(authorization_request) + expect(oauth_application.owner_type).to eq('AuthorizationRequest') + end + end +end