170 行
7.8 KiB
Ruby
170 行
7.8 KiB
Ruby
require 'rails_helper'
|
|
require 'csv'
|
|
|
|
RSpec.describe 'Counts masked pivot contracts' do
|
|
let(:admin) { create(:user, :admin) }
|
|
let(:member) { create(:user, :member) }
|
|
let(:guest) { create(:user, :guest) }
|
|
let(:params) do
|
|
{ 'resource' => 'posts', 'definition' => 'history', 'mode' => 'summary',
|
|
'locale' => 'ja', 'execution_at' => '2025-01-01T00:00:00Z',
|
|
'timezone' => 'UTC', 'rows' => 'tags', 'cols' => 'event_type', 'sort' => '' }
|
|
end
|
|
let!(:tags) do
|
|
['A', 'B', 'C'].map do |name|
|
|
tag = create(:tag, primary_name: name)
|
|
TagVersion.create!(tag:, version_no: 1, event_type: 'create', name:,
|
|
category: 'general', aliases: '', parent_tag_ids: '',
|
|
names: { ja: { name:, script_code: 'Jpan', auto_generated: false, aliases: [] } },
|
|
created_at: Time.utc(2023))
|
|
tag
|
|
end
|
|
end
|
|
|
|
before do
|
|
[[0, 1], [0], [1, 2]].each_with_index do |indexes, index|
|
|
post = create(:post)
|
|
snapshot = indexes.map { { tag_id: tags[_1].id, version_no: 1, sections: [] } }
|
|
PostVersion.create!(post:, version_no: 1, event_type: 'create',
|
|
title: "hidden-title-#{ index }", url: post.url, tags: '', tags_json: snapshot,
|
|
parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 1))
|
|
next if index == 2
|
|
|
|
PostVersion.create!(post:, version_no: 2, event_type: 'discard',
|
|
title: "private-discard-#{ index }", url: post.url, tags: '', tags_json: [],
|
|
parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 2))
|
|
end
|
|
end
|
|
|
|
def result(user, extra = {})
|
|
HistoryAccess.set(user:) { CountsTableQuery.new(params.merge(extra)).call }
|
|
end
|
|
|
|
def grand(result)
|
|
result.fetch(:pivot_totals).find { _1[:row].empty? && _1[:column].empty? }.fetch(:count)
|
|
end
|
|
|
|
it 'preserves five tag occurrences after merging private tags and across pages' do
|
|
[nil, guest, member, admin].each do |user|
|
|
page = result(user)
|
|
expect(grand(page)).to eq(5)
|
|
expect(page[:cells].sum { _1[:count] }).to eq(5)
|
|
expect(page).not_to have_key(:excluded_count)
|
|
paged = result(user, 'limit' => '1')
|
|
expect(grand(paged)).to eq(5)
|
|
expect(result(user, 'limit' => '1', 'page' => '2')[:group_count])
|
|
.to eq(page[:group_count])
|
|
end
|
|
public_page = result(nil)
|
|
hidden = public_page[:cells].select { _1[:values].first == CountsAxes::MASKED_VALUE }
|
|
expect(hidden.sum { _1[:count] }).to eq(3)
|
|
expect(public_page[:axis_labels]['a0']).not_to have_key("internal:#{ tags.first.id }")
|
|
expect(public_page.to_json).not_to include('hidden-title', 'private-discard')
|
|
end
|
|
|
|
it 'retains distinct membership when tags are not a row axis' do
|
|
[nil, admin].each do |user|
|
|
expect(grand(result(user, 'rows' => 'event_type', 'cols' => 'tags'))).to eq(3)
|
|
expect(result(user, 'rows' => '', 'cols' => '')[:cells].sole[:count]).to eq(5)
|
|
details = result(user, 'mode' => 'details', 'rows' => '', 'cols' => '')
|
|
expect(details[:count]).to eq(user == admin ? 5 : 1)
|
|
end
|
|
end
|
|
|
|
it 'uses changed-record membership for totals and hides changes to private predecessors' do
|
|
post = PostVersion.order(:id).last.post
|
|
PostVersion.create!(post:, version_no: 2, event_type: 'update', title: 'changed-public-title',
|
|
url: post.url, tags: '',
|
|
tags_json: [{ tag_id: tags.last.id, version_no: 1, sections: [] }],
|
|
parent_post_ids: '', created_at: Time.utc(2024, 3))
|
|
restored = PostVersion.order(:id).first.post
|
|
PostVersion.create!(post: restored, version_no: 3, event_type: 'restore',
|
|
title: 'restored-public-title', url: restored.url, tags: '',
|
|
tags_json: [{ tag_id: tags.first.id, version_no: 1, sections: [] }], parent_post_ids: '',
|
|
created_at: Time.utc(2024, 4))
|
|
[nil, admin].each do |user|
|
|
page = result(user, 'rows' => 'tags changed_field', 'cols' => 'event_type')
|
|
expect(grand(page)).to eq(page[:cells].sum { _1[:count] })
|
|
expect(page[:cells]).not_to be_empty
|
|
if user.nil?
|
|
expect(page[:cells].any? { _1[:values][1] == CountsAxes::MASKED_VALUE }).to be(true)
|
|
end
|
|
expect(page.to_json).not_to include('hidden-title', 'private-discard')
|
|
end
|
|
end
|
|
|
|
it 'counts distinct changed records when tags are not additive row tuples' do
|
|
[nil, admin].each do |user|
|
|
page = result(user, 'rows' => 'changed_field', 'cols' => 'event_type')
|
|
expect(grand(page)).to eq(2)
|
|
if user.nil?
|
|
expect(page[:cells].map { _1[:values].first }.uniq).to eq([CountsAxes::MASKED_VALUE])
|
|
end
|
|
# Changed discard snapshots have no tags: no full cell contributes here.
|
|
empty = result(user, 'rows' => 'changed_field', 'cols' => 'tags')
|
|
expect(empty[:cells]).to be_empty
|
|
expect(grand(empty)).to eq(0)
|
|
end
|
|
end
|
|
|
|
it 'keeps NULL as a public data value rather than treating it as a rollup' do
|
|
post = create(:post)
|
|
PostVersion.create!(post:, version_no: 1, event_type: 'create', title: 'public bot',
|
|
url: post.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 5))
|
|
page = result(nil, 'rows' => 'user', 'cols' => 'event_type')
|
|
expect(page[:cells].select { _1[:values].first.nil? }.sum { _1[:count] }).to eq(1)
|
|
expect(grand(page)).to eq(6)
|
|
expect(page[:row_headers]).to include(hash_including(values: [nil], kind: 'data'))
|
|
end
|
|
|
|
it 'sorts public numeric values numerically alongside a masked group' do
|
|
post = PostVersion.order(:id).last.post
|
|
[2, 10].each do |number|
|
|
PostVersion.create!(post:, version_no: number, event_type: 'update',
|
|
title: 'public numeric sort', url: post.url, tags: '', tags_json: [],
|
|
parent_post_ids: '', created_at: Time.utc(2024, 5))
|
|
end
|
|
page = result(nil, 'rows' => 'version_no', 'cols' => '')
|
|
values = page[:row_headers].select { _1[:kind] == 'data' }.map { _1[:values].first }
|
|
expect(values.reject { _1 == CountsAxes::MASKED_VALUE }.map(&:to_i)).to eq([1, 2, 10])
|
|
expect(values.last).to eq(CountsAxes::MASKED_VALUE)
|
|
end
|
|
|
|
it 'rejects private predicates and supplied measure coordinates before SQL labels escape' do
|
|
probes = [{ 'resource_id' => PostVersion.first.post_id.to_s },
|
|
{ 'tags' => 'A' }, { 'tag_refs.0.name' => 'A', 'tag_refs.0.tag_id' => tags.first.id.to_s },
|
|
{ 'conditions.0.field' => 'title', 'conditions.0.operator' => 'eq',
|
|
'conditions.0.value' => 'hidden-title-0' },
|
|
{ 'drill.0.field' => 'tags', 'drill.0.value' => "internal:#{ tags.first.id }" },
|
|
{ 'cols' => 'tags', 'column_order.0.value' => CountsAxes::MASKED_VALUE }]
|
|
HistoryAccess.set(user: nil) do
|
|
probes.each do |probe|
|
|
expect { CountsTableQuery.new(params.merge(probe)) }
|
|
.to raise_error(CountsAxes::PrivateCondition)
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'keeps public column sorting usable while guessed private coordinates match nothing' do
|
|
first = result(nil, 'rows' => 'event_type', 'cols' => 'tags')
|
|
probe = result(nil, 'rows' => 'event_type', 'cols' => 'tags',
|
|
'column_order.0.value' => "internal:#{ tags.first.id }")
|
|
expect(probe[:column_values]).to eq(first[:column_values])
|
|
expect(probe[:cells]).to eq(first[:cells])
|
|
expect(probe[:column_values_editable]).to be(false)
|
|
expect(probe.to_json).not_to include("internal:#{ tags.first.id }")
|
|
end
|
|
|
|
it 'exports masked coordinates and safe CSV labels from the same pivot' do
|
|
HistoryAccess.set(user: nil) do
|
|
payload = params.merge('format' => 'json')
|
|
json = JSON.parse(CountsTableExport.new(payload).body.to_a.join)
|
|
expect(json['data'].sum { _1['kind'] == 'data' ? _1['count'] : 0 }).to eq(5)
|
|
expect(json.to_json).not_to include('hidden-title', 'private-discard')
|
|
csv = CountsTableExport.new(payload.merge('format' => 'csv')).body.to_a.join
|
|
expect(csv).to include('********')
|
|
expect(csv).not_to include(CountsAxes::MASKED_VALUE, 'hidden-title', 'private-discard')
|
|
end
|
|
end
|
|
end
|