Commit 5c89f5e6 authored by Alex Kalderimis's avatar Alex Kalderimis

Add connection redaction and connection extension methods

The connection redaction is in preparation for the coming authorization
changes, and the connection methods are important so that we can test
connections as if they are enumerable containers of their items.
parent e344cdc2
# frozen_string_literal: true
module Gitlab
module Graphql
module ConnectionCollectionMethods
extend ActiveSupport::Concern
included do
delegate :to_a, :size, :include?, :empty?, to: :nodes
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Graphql
module ConnectionRedaction
class RedactionState
attr_reader :redactor
attr_reader :redacted_nodes
def redactor=(redactor)
@redactor = redactor
@redacted_nodes = nil
end
def redacted(&block)
@redacted_nodes ||= redactor.present? ? redactor.redact(yield) : yield
end
end
delegate :redactor=, to: :redaction_state
def nodes
redaction_state.redacted { super.to_a }
end
private
def redaction_state
@redaction_state ||= RedactionState.new
end
end
end
end
# frozen_string_literal: true
# We use the Keyset / Stable cursor connection by default for ActiveRecord::Relation.
# However, there are times when that may not be powerful enough (yet), and we
# want to use standard offset pagination.
module Gitlab
module Graphql
module Pagination
class ArrayConnection < ::GraphQL::Pagination::ArrayConnection
prepend ::Gitlab::Graphql::ConnectionRedaction
include ::Gitlab::Graphql::ConnectionCollectionMethods
end
end
end
end
......@@ -12,6 +12,10 @@ module Gitlab
schema.connections.add(
Gitlab::Graphql::ExternallyPaginatedArray,
Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection)
schema.connections.add(
Array,
Gitlab::Graphql::Pagination::ArrayConnection)
end
end
end
......
......@@ -5,6 +5,9 @@ module Gitlab
module Graphql
module Pagination
class ExternallyPaginatedArrayConnection < GraphQL::Pagination::ArrayConnection
include ::Gitlab::Graphql::ConnectionCollectionMethods
prepend ::Gitlab::Graphql::ConnectionRedaction
def start_cursor
items.previous_cursor
end
......
......@@ -31,6 +31,8 @@ module Gitlab
module Keyset
class Connection < GraphQL::Pagination::ActiveRecordRelationConnection
include Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::ConnectionCollectionMethods
prepend ::Gitlab::Graphql::ConnectionRedaction
# rubocop: disable Naming/PredicateName
# https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields
......
......@@ -7,6 +7,8 @@ module Gitlab
module Graphql
module Pagination
class OffsetActiveRecordRelationConnection < GraphQL::Pagination::ActiveRecordRelationConnection
prepend ::Gitlab::Graphql::ConnectionRedaction
include ::Gitlab::Graphql::ConnectionCollectionMethods
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Graphql::Pagination::ArrayConnection do
let(:nodes) { (1..10) }
subject(:connection) { described_class.new(nodes, max_page_size: 100) }
it_behaves_like 'a connection with collection methods'
it_behaves_like 'a redactable connection' do
let(:unwanted) { 5 }
end
end
......@@ -13,6 +13,12 @@ RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection d
described_class.new(all_nodes, { max_page_size: values.size }.merge(arguments))
end
it_behaves_like 'a connection with collection methods'
it_behaves_like 'a redactable connection' do
let(:unwanted) { 3 }
end
describe '#nodes' do
let(:paged_nodes) { connection.nodes }
......
......@@ -21,6 +21,13 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
it_behaves_like 'a connection with collection methods'
it_behaves_like 'a redactable connection' do
let_it_be(:projects) { create_list(:project, 2) }
let(:unwanted) { projects.second }
end
describe '#cursor_for' do
let(:project) { create(:project) }
let(:cursor) { connection.cursor_for(project) }
......
......@@ -6,4 +6,15 @@ RSpec.describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
it 'subclasses from GraphQL::Relay::RelationConnection' do
expect(described_class.superclass).to eq GraphQL::Pagination::ActiveRecordRelationConnection
end
it_behaves_like 'a connection with collection methods' do
let(:connection) { described_class.new(Project.all) }
end
it_behaves_like 'a redactable connection' do
let_it_be(:users) { create_list(:user, 2) }
let(:connection) { described_class.new(User.all, max_page_size: 10) }
let(:unwanted) { users.second }
end
end
# frozen_string_literal: true
# requires:
# - `connection` (no-empty, containing `unwanted` and at least one more item)
# - `unwanted` (single item in collection)
RSpec.shared_examples 'a redactable connection' do
context 'no redactor set' do
it 'contains the unwanted item' do
expect(connection.nodes).to include(unwanted)
end
it 'does not redact more than once' do
connection.nodes
r_state = connection.send(:redaction_state)
expect(r_state.redacted { raise 'Should not be called!' }).to be_present
end
end
let_it_be(:constant_redactor) do
Class.new do
def initialize(remove)
@remove = remove
end
def redact(items)
items - @remove
end
end
end
context 'redactor is set' do
let(:redactor) do
constant_redactor.new([unwanted])
end
before do
connection.redactor = redactor
end
it 'does not contain the unwanted item' do
expect(connection.nodes).not_to include(unwanted)
expect(connection.nodes).not_to be_empty
end
it 'does not redact more than once' do
expect(redactor).to receive(:redact).once.and_call_original
connection.nodes
connection.nodes
connection.nodes
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'a connection with collection methods' do
%i[to_a size include? empty?].each do |method_name|
it "responds to #{method_name}" do
expect(connection).to respond_to(method_name)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment