# frozen_string_literal: true

require 'spec_helper'

RSpec.describe GraphqlChannel, feature_category: :api do
  let_it_be(:merge_request) { create(:merge_request) }
  let_it_be(:user) { create(:user, developer_of: merge_request.project) }
  let_it_be(:read_api_token) { create(:personal_access_token, scopes: ['read_api'], user: user) }
  let_it_be(:read_user_token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
  let_it_be(:read_api_and_read_user_token) do
    create(:personal_access_token, scopes: %w[read_user read_api], user: user)
  end

  let_it_be(:expired_token) { create(:personal_access_token, :expired, scopes: %w[read_api], user: user) }
  let_it_be(:revoked_token) { create(:personal_access_token, :revoked, scopes: %w[read_api], user: user) }

  describe '#subscribed' do
    let(:query) do
      <<~GRAPHQL
      subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
        mergeRequestReviewersUpdated(issuableId: $issuableId) {
          ... on MergeRequest { id title }
        }
      }
      GRAPHQL
    end

    let(:subscribe_params) do
      {
        query: query,
        variables: { issuableId: merge_request.to_global_id }
      }
    end

    before do
      stub_action_cable_connection current_user: user
    end

    it 'subscribes to the given graphql subscription' do
      subscribe(subscribe_params)

      expect(subscription).to be_confirmed
      expect(subscription.streams).to include(/graphql-event::mergeRequestReviewersUpdated:issuableId/)
    end

    context 'with a personal access token' do
      let(:app_context) { Gitlab::ApplicationContext.current }

      before do
        stub_action_cable_connection current_user: user, access_token: access_token
      end

      context 'with an api scoped personal access token' do
        let(:access_token) { read_api_token }

        it 'subscribes to the given graphql subscription' do
          subscribe(subscribe_params)

          expect(subscription).to be_confirmed
          expect(subscription.streams).to include(/graphql-event::mergeRequestReviewersUpdated:issuableId/)
          expect(app_context.keys).not_to include('meta.auth_fail_reason', 'meta.auth_fail_token_id')
        end
      end

      context 'with a read_user personal access token' do
        let(:access_token) { read_user_token }

        it 'does not subscribe to the given graphql subscription' do
          subscribe(subscribe_params)

          expect(subscription).not_to be_confirmed
          expect(app_context['meta.auth_fail_reason']).to eq('insufficient_scope')
          expect(app_context['meta.auth_fail_token_id']).to eq("PersonalAccessToken/#{access_token.id}")
        end
      end

      context 'with a read_api and read_user personal access token' do
        let(:access_token) { read_api_and_read_user_token }

        it 'subscribes to the given graphql subscription' do
          subscribe(subscribe_params)

          expect(subscription).to be_confirmed
          expect(subscription.streams).to include(/graphql-event::mergeRequestReviewersUpdated:issuableId/)
          expect(app_context.keys).not_to include('meta.auth_fail_reason', 'meta.auth_fail_token_id')
        end
      end

      context 'with an expired read_user personal access token' do
        let(:access_token) { expired_token }

        it 'does not subscribe to the given graphql subscription' do
          subscribe(subscribe_params)

          expect(subscription).not_to be_confirmed
          expect(app_context['meta.auth_fail_reason']).to eq('token_expired')
          expect(app_context['meta.auth_fail_token_id']).to eq("PersonalAccessToken/#{access_token.id}")
        end
      end

      context 'with a revoked read_user personal access token' do
        let(:access_token) { revoked_token }

        it 'does not subscribe to the given graphql subscription' do
          subscribe(subscribe_params)

          expect(subscription).not_to be_confirmed
          expect(app_context['meta.auth_fail_reason']).to eq('token_revoked')
          expect(app_context['meta.auth_fail_token_id']).to eq("PersonalAccessToken/#{access_token.id}")
        end
      end
    end

    describe 'metrics' do
      before do
        stub_action_cable_connection current_user: create(:user)
      end

      it 'does not track unauthorized subscriptions as errors' do
        expect(Gitlab::Metrics::RailsSlis.graphql_query_apdex).not_to receive(:increment)

        expect(Gitlab::Metrics::RailsSlis.graphql_query_error_rate).to receive(:increment).with({
          labels: anything,
          error: false
        })

        subscribe(subscribe_params)

        expect(transmissions.first['result']).to match(a_hash_including(
          'errors' => [
            a_hash_including(
              'message' => 'Unauthorized subscription'
            )
          ]
        ))
      end
    end

    describe 'context' do
      let_it_be(:organization) { create(:organization) }
      let(:graphql_result) do
        instance_double(GraphQL::Query::Result, to_h: {}, subscription?: false, context: {})
      end

      before do
        stub_action_cable_connection current_organization: organization
      end

      it 'includes current_organization context' do
        expect(GitlabSchema).to receive(:execute).with(
          anything,
          hash_including(
            context: hash_including(current_organization: organization)
          )
        ).and_return(graphql_result)

        subscribe(subscribe_params)
      end
    end
  end
end
