--- name: testing description: Write correct and complete RSpec tests for Doorkeeper. Use when adding specs for new features, writing regression tests for bug fixes, or reviewing test coverage for a change. --- # Testing When writing or modifying tests in Doorkeeper, use this skill to ensure tests are correct, complete, and follow project conventions. ## Test Organization | What you're testing | Where to put the spec | |--------------------|----------------------| | OAuth request/response flow (integration) | `spec/requests/flows/` | | Endpoint behavior (HTTP layer) | `spec/requests/endpoints/` | | Controller logic (unit) | `spec/controllers/` | | Model behavior & mixins | `spec/models/doorkeeper/` | | OAuth protocol logic (unit) | `spec/lib/oauth/` | | Configuration | `spec/lib/config_spec.rb` | | Generators | `spec/generators/` | | Routing | `spec/routing/` | | Grape integration | `spec/grape/` | ## Spec Conventions ### Style ```ruby # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::SomeClass do describe "#method_name" do context "when condition is met" do it "does the expected thing" do # arrange, act, assert end end end end ``` ### Require Line All specs use `require "spec_helper"`. The `spec_helper` loads the dummy Rails app, database, factories, and support helpers — it covers both unit and integration needs. > **Note:** A legacy `spec_helper_integration` file exists but is just a compatibility wrapper around `spec_helper`. Do not use it in new specs. ### Factories Factories are in `spec/factories.rb`. Use FactoryBot: ```ruby let(:application) { FactoryBot.create(:application) } let(:access_token) { FactoryBot.create(:access_token, application: application) } let(:access_grant) { FactoryBot.create(:access_grant, application: application) } ``` ### Helper Methods Reuse helpers from `spec/support/helpers/`: - `model_helper.rb` — `client_exists`, `create_resource_owner`, token/grant existence checks - `request_spec_helper.rb` — `json_response`, `should_have_status`, `url_should_have_param`, `basic_auth_header_for_client` - `url_helper.rb` — `token_endpoint_url`, `authorization_endpoint_url`, `refresh_token_endpoint_url` - `config_helper.rb` — `config_is_set` (temporarily override config in a block) - `authorization_request_helper.rb` — `resource_owner_is_authenticated`, `default_scopes_exist` ### Configuration Changes in Tests Use `config_is_set` or the Doorkeeper.configure block (resets after each test): ```ruby before do config_is_set(:access_token_expires_in, 100) end ``` ## Test Patterns ### Request/Flow Specs (most important) ```ruby RSpec.describe "Authorization Code Flow" do let(:application) { FactoryBot.create(:application) } let(:resource_owner) { User.create!(name: "owner", password: "password") } before do default_scopes_exist :public resource_owner_is_authenticated resource_owner end it "issues an access token" do visit authorization_endpoint_url(client: application) click_on "Authorize" code = current_params["code"] post token_endpoint_url, params: { grant_type: "authorization_code", code: code, redirect_uri: application.redirect_uri, client_id: application.uid, client_secret: application.secret, } expect(response).to have_http_status(:ok) expect(json_response["access_token"]).to be_present end end ``` ### OAuth Unit Specs ```ruby RSpec.describe Doorkeeper::OAuth::AuthorizationCodeRequest do subject(:request) do described_class.new(server, grant, client, params) end let(:server) do double :server, access_token_expires_in: 2.days, refresh_token_enabled?: false, custom_access_token_expires_in: lambda { |context| context.grant_type == Doorkeeper::OAuth::AUTHORIZATION_CODE ? 1234 : nil } end let(:resource_owner) { FactoryBot.create :resource_owner } let(:grant) do FactoryBot.create :access_grant, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end let(:client) { grant.application } let(:redirect_uri) { client.redirect_uri } let(:params) { { redirect_uri: redirect_uri } } before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end describe "#authorize" do it "issues a new token for the client" do expect { request.authorize }.to change { client.reload.access_tokens.count }.by(1) end it "revokes the grant" do expect { request.authorize }.to(change { grant.reload.accessible? }) end end describe "#validate" do it "requires the grant to be accessible" do grant.revoke request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "requires the client" do request = described_class.new(server, grant, nil, params) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end end end ``` ## What to Test ### For new features: 1. Happy path — the feature works as intended 2. Error cases — invalid input, missing parameters, unauthorized access 3. Edge cases — nil values, empty strings, boundary conditions 4. Configuration interaction — does it respect relevant config options? 5. Backward compatibility — does existing behavior still pass? ### For bug fixes: 1. Regression test — reproduce the exact bug scenario, verify it's fixed 2. Related edge cases — similar situations that might also be affected ### For security fixes: 1. The vulnerability is no longer exploitable 2. The fix doesn't break legitimate use cases 3. Error responses don't leak information ## Common Pitfalls - **Don't use `sleep`** — use `Timecop.travel` or `Timecop.freeze` instead - **Don't hardcode token values** — let the system generate them - **Don't test private methods directly** — test through the public interface - **Don't forget scopes** — many features are scope-dependent, set them up explicitly - **Use `Timecop`** for time-dependent tests (expiration, token lifetime) ## Verification 1. Run new specs in isolation: `bundle exec rspec spec/path/to/new_spec.rb` 2. Run the full related directory: `bundle exec rspec spec/lib/oauth/` 3. Run full suite: `bundle exec rake spec` 4. Specs must pass in any order (`--order random`)