Files
gitlab-foss/spec/requests/oauth/dynamic_registrations_controller_spec.rb
2025-07-24 15:22:16 +00:00

496 lines
16 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Oauth::DynamicRegistrationsController, feature_category: :system_access do
let(:oauth_registration_path) { Gitlab::Routing.url_helpers.oauth_register_path }
let(:valid_request_body) do
{
client_name: 'Test Application',
redirect_uris: ['http://example.com/callback'],
scope: 'mcp'
}
end
let(:headers) { { 'Content-Type' => 'application/json' } }
RSpec.shared_examples 'creates application successfully' do
before do
create_registration
end
it 'returns created status' do
expect(response).to have_gitlab_http_status(:created)
end
end
RSpec.shared_examples 'rejects request with bad_request' do
let(:initial_count) { Doorkeeper::Application.count }
before do
initial_count
end
it 'returns bad request status' do
create_registration
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'does not create application' do
expect { create_registration }.not_to change { Doorkeeper::Application.count }
end
end
describe 'POST /oauth/register' do
subject(:create_registration) do
post oauth_registration_path, params: request_body.to_json, headers: headers
end
context 'when feature flag is enabled' do
context 'with valid parameters' do
let(:request_body) { valid_request_body }
it 'creates a new OAuth application' do
expect { create_registration }.to change { Doorkeeper::Application.count }.by(1)
end
it_behaves_like 'creates application successfully'
it 'returns correct JSON response structure' do
create_registration
application = Doorkeeper::Application.last.reload
expect(response.parsed_body).to include(
'client_id' => application.uid,
'client_id_issued_at' => application.created_at.to_i,
'redirect_uris' => ['http://example.com/callback'],
'grant_types' => ['authorization_code'],
'client_name' => '[Unverified Dynamic Application] Test Application',
'scope' => 'mcp',
'dynamic' => true
)
end
it 'creates application with correct attributes' do
create_registration
application = Doorkeeper::Application.last
expect(application).to have_attributes(
name: '[Unverified Dynamic Application] Test Application',
redirect_uri: 'http://example.com/callback',
confidential: false,
dynamic: true
)
expect(application.scopes.to_s).to eq('mcp')
end
it 'sets content type to JSON' do
create_registration
expect(response.content_type).to include('application/json')
end
context 'when validating application attributes' do
before do
create_registration
end
let(:created_application) { Doorkeeper::Application.last }
it 'generates unique client_id and client_secret' do
expect(created_application.uid).to be_present
expect(created_application.uid).to be_a(String)
end
end
it 'sets client_id_issued_at to current time' do
freeze_time do
create_registration
expect(response.parsed_body['client_id_issued_at']).to eq(Time.current.to_i)
end
end
end
context 'with redirect URI variations' do
context 'with multiple redirect URIs as array' do
let(:request_body) do
valid_request_body.merge(
redirect_uris: %w[http://example.com/callback http://example.com/callback2]
)
end
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'creates application with multiple redirect URIs' do
expect(created_application.redirect_uri).to eq("http://example.com/callback\nhttp://example.com/callback2")
end
it 'returns multiple redirect URIs in response' do
expect(response.parsed_body['redirect_uris'])
.to eq(%w[http://example.com/callback http://example.com/callback2])
end
end
context 'with redirect URI as string' do
let(:request_body) do
valid_request_body.merge(redirect_uris: 'http://example.com/callback')
end
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'creates application with single redirect URI' do
expect(created_application.redirect_uri).to eq('http://example.com/callback')
end
it 'returns single redirect URI as array in response' do
expect(response.parsed_body['redirect_uris']).to eq(['http://example.com/callback'])
end
end
context 'with missing redirect_uris' do
let(:request_body) { valid_request_body.except(:redirect_uris) }
it_behaves_like 'rejects request with bad_request'
end
context 'with empty redirect_uris array' do
let(:request_body) { valid_request_body.merge(redirect_uris: []) }
it_behaves_like 'rejects request with bad_request'
end
context 'with invalid redirect URI' do
let(:request_body) { valid_request_body.merge(redirect_uris: ['invalid-uri']) }
it_behaves_like 'rejects request with bad_request'
end
context 'with HTTPS redirect URIs' do
let(:request_body) do
valid_request_body.merge(redirect_uris: ['https://secure.example.com/callback'])
end
it_behaves_like 'creates application successfully'
end
context 'with localhost redirect URIs' do
let(:request_body) do
valid_request_body.merge(redirect_uris: ['http://localhost:3000/callback'])
end
it_behaves_like 'creates application successfully'
end
context 'with custom scheme redirect URIs' do
let(:request_body) do
valid_request_body.merge(redirect_uris: ['myapp://callback'])
end
it_behaves_like 'creates application successfully'
end
end
context 'with scope variations' do
context 'with no scope provided' do
let(:request_body) { valid_request_body.except(:scope) }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'uses default mcp scope' do
expect(created_application.scopes.to_s).to eq('mcp')
end
it 'returns default scope in response' do
expect(response.parsed_body['scope']).to eq('mcp')
end
end
context 'with empty scope' do
let(:request_body) { valid_request_body.merge(scope: '') }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'uses default mcp scope' do
expect(created_application.scopes.to_s).to eq('mcp')
end
end
context 'with nil scope' do
let(:request_body) { valid_request_body.merge(scope: nil) }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'uses default mcp scope' do
expect(created_application.scopes.to_s).to eq('mcp')
end
end
context 'with mcp scope' do
let(:request_body) { valid_request_body.merge(scope: 'mcp') }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'uses provided scope' do
expect(created_application.scopes.to_s).to eq('mcp')
end
end
context 'with different scope' do
let(:request_body) { valid_request_body.merge(scope: 'api') }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'uses default mcp scope' do
expect(created_application.scopes.to_s).to eq('mcp')
end
end
end
context 'with client_name variations' do
context 'with minimal valid parameters' do
let(:request_body) do
{
client_name: 'Minimal App',
redirect_uris: ['http://example.com/callback']
}
end
it 'creates application successfully' do
create_registration
expect(response).to have_gitlab_http_status(:created)
application = Doorkeeper::Application.last
expect(application.name).to eq('[Unverified Dynamic Application] Minimal App')
expect(application.redirect_uri).to eq('http://example.com/callback')
expect(application.scopes.to_s).to eq('mcp')
end
end
context 'with very long client name' do
let(:request_body) { valid_request_body.merge(client_name: 'A' * 1000) }
it_behaves_like 'rejects request with bad_request'
end
context 'with special characters in client name' do
let(:request_body) { valid_request_body.merge(client_name: 'Test App & Co. (v1.0)') }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'handles special characters correctly' do
expect(created_application.name).to eq('[Unverified Dynamic Application] Test App & Co. (v1.0)')
end
end
context 'with Unicode characters in client name' do
let(:request_body) { valid_request_body.merge(client_name: 'Test 应用程序 🚀') }
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'handles Unicode characters correctly' do
expect(created_application.name).to eq('[Unverified Dynamic Application] Test 应用程序 🚀')
end
end
context 'with empty client name' do
let(:request_body) { valid_request_body.merge(client_name: '') }
it_behaves_like 'rejects request with bad_request'
it 'returns error response' do
create_registration
expect(response.parsed_body).to include(
'error' => 'invalid_client_metadata'
)
expect(response.parsed_body['error_description']).to be_present
end
end
context 'with nil client name' do
let(:request_body) { valid_request_body.merge(client_name: nil) }
it_behaves_like 'rejects request with bad_request'
end
end
context 'with parameter filtering' do
context 'with disallowed parameters' do
let(:request_body) do
valid_request_body.merge(
confidential: false,
dynamic: false,
uid: 'custom_uid',
secret: 'custom_secret',
created_at: 1.day.ago.to_i,
updated_at: 1.day.ago.to_i
)
end
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'filters out disallowed parameters' do
expect(created_application.name).to eq('[Unverified Dynamic Application] Test Application')
expect(created_application.confidential).to be false # Should always be true
expect(created_application.dynamic).to be true # Should always be true
expect(created_application.uid).not_to eq('custom_uid')
expect(created_application.secret).not_to eq('custom_secret')
end
end
end
context 'with extra unknown parameters' do
let(:request_body) do
valid_request_body.merge(
unknown_param: 'value',
another_param: 'another_value'
)
end
let(:created_application) { Doorkeeper::Application.last }
before do
create_registration
end
it 'ignores unknown parameters' do
expect(response).to have_gitlab_http_status(:created)
expect(created_application.name).to eq('[Unverified Dynamic Application] Test Application')
end
end
context 'with request body format issues' do
context 'with invalid JSON' do
let(:request_body) { '{ invalid json' }
it 'returns bad request status' do
post oauth_registration_path, params: request_body, headers: headers
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'does not create application' do
expect do
post oauth_registration_path, params: request_body, headers: headers
end.not_to change { Doorkeeper::Application.count }
end
end
context 'with empty request body' do
let(:request_body) { {} }
it_behaves_like 'rejects request with bad_request'
end
end
context 'with database validation failures' do
context 'with duplicate application name' do
let(:request_body) { valid_request_body.merge(client_name: 'Duplicate App') }
before do
create(:application, name: 'Duplicate App')
end
it 'creates application successfully' do
create_registration
expect(response).to have_gitlab_http_status(:created)
end
it 'allows duplicate names' do
expect { create_registration }.to change { Doorkeeper::Application.count }.by(1)
end
end
context 'with extremely long redirect URI' do
let(:request_body) do
valid_request_body.merge(redirect_uris: ["http://example.com/callback#{'a' * 10000}"])
end
it 'handles long redirect URIs appropriately' do
create_registration
expect(response).to have_gitlab_http_status(:created).or have_gitlab_http_status(:bad_request)
end
end
end
context 'with application creation edge cases' do
context 'when application fails to save' do
let(:request_body) { valid_request_body }
before do
allow_next_instance_of(Doorkeeper::Application) do |application|
errors = ActiveModel::Errors.new(application)
allow(errors).to receive(:full_messages).and_return(['Name is invalid'])
allow(application).to receive_messages(persisted?: false, errors: errors)
end
end
it 'returns bad request status' do
create_registration
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns error response with validation messages' do
create_registration
expect(response.parsed_body).to include(
'error' => 'invalid_client_metadata',
'error_description' => 'Name is invalid'
)
end
end
end
end
context 'when feature flag is disabled' do
let(:request_body) { valid_request_body }
before do
stub_feature_flags(oauth_dynamic_client_registration: false)
end
it 'returns 404' do
create_registration
expect(response).to have_gitlab_http_status(:not_found)
end
it 'does not create application' do
expect { create_registration }.not_to change { Doorkeeper::Application.count }
end
end
end
end