mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-12 23:57:42 +00:00
496 lines
16 KiB
Ruby
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
|