197 行
8.9 KiB
Ruby
197 行
8.9 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe 'Counts masked search', type: :request do
|
|
let!(:tag) { create(:tag, primary_name: '秘密のタグ') }
|
|
let(:actor) { create(:user, :member) }
|
|
let(:admin) { create(:user, :admin) }
|
|
let(:base) do
|
|
{ 'resource' => 'tags', 'definition' => 'history', 'mode' => 'summary',
|
|
'locale' => 'ja', 'execution_at' => '2025-01-01T00:00:00Z',
|
|
'timezone' => 'UTC', 'rows' => '', 'cols' => '', 'sort' => '' }
|
|
end
|
|
|
|
before do
|
|
[1, 2].each do |number|
|
|
TagVersion.create!(tag:, version_no: number,
|
|
event_type: number == 1 ? 'create' : 'discard', name: '秘密のタグ',
|
|
category: 'general', aliases: '秘密の別名', parent_tag_ids: '',
|
|
names: { ja: { name: '秘密のタグ', script_code: 'Jpan', auto_generated: false,
|
|
aliases: [{ name: '秘密の別名', script_code: 'Jpan' }] } },
|
|
created_by_user: actor, created_at: Time.utc(2023, number))
|
|
end
|
|
end
|
|
|
|
def query(extra = {})
|
|
get '/counts/query', params: base.merge(extra)
|
|
expect(response).to have_http_status(:ok), extra.inspect
|
|
response.parsed_body
|
|
end
|
|
|
|
def predicate(field, operator, value, index = 0)
|
|
prefix = "conditions.#{ index }"
|
|
result = { 'conditions' => '', "#{ prefix }.field" => field,
|
|
"#{ prefix }.operator" => operator }
|
|
if ['in', 'any', 'all', 'none', 'not_in'].include?(operator)
|
|
result.merge!("#{ prefix }.values" => '1', 'condition_values' => '',
|
|
"condition_values.#{ index }.condition" => index.to_s,
|
|
"condition_values.#{ index }.value_type" => 'string',
|
|
"condition_values.#{ index }.value" => value)
|
|
else
|
|
result["#{ prefix }.value"] = value
|
|
end
|
|
result
|
|
end
|
|
|
|
it 'rejects every name and alias predicate on private history' do
|
|
['name', 'name_ja', 'names.name', 'aliases.name', 'names.aliases.name'].each do |field|
|
|
secret = field.include?('aliases') ? '秘密の別名' : '秘密のタグ'
|
|
multiple = !['name', 'name_ja'].include?(field)
|
|
operators = multiple ? ['any', 'all', 'none'] : ['in', 'not_in']
|
|
['eq', 'ne', 'like', *operators].each do |operator|
|
|
[secret, '********', '%', ''].each do |value|
|
|
expected = (value == '' && ['eq', 'ne', 'like'].include?(operator)) ? 2 : 0
|
|
expect(query(predicate(field, operator, value))['count']).to eq(expected)
|
|
end
|
|
end
|
|
blank = predicate(field, 'eq', '').merge('conditions.0.blank' => '1')
|
|
expect(query(blank)['count']).to eq(0)
|
|
end
|
|
end
|
|
|
|
it 'ignores missing and empty scalar values unless blank is checked' do
|
|
['name', 'name_ja', 'names.name', 'aliases.name'].each do |field|
|
|
['eq', 'ne', 'like'].each do |operator|
|
|
condition = predicate(field, operator, '')
|
|
[condition, condition.except('conditions.0.value')].each do |empty|
|
|
expect(query(empty)['count']).to eq(2)
|
|
expect(query(empty.merge('conditions.0.blank' => '0'))['count']).to eq(2)
|
|
expect(query(empty.merge('conditions.0.blank' => '1'))['count']).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'keeps details totals unchanged for inactive private predicates' do
|
|
settings = predicate('name', 'ne', '').merge('mode' => 'details')
|
|
result = query(settings)
|
|
expect(result['count']).to eq(0)
|
|
expect(result['versions']).to be_empty
|
|
expect(result['output_count']).to eq(2)
|
|
active = query(settings.merge('conditions.0.blank' => '1'))
|
|
expect(active['output_count']).to eq(0)
|
|
end
|
|
|
|
it 'ignores missing sets but preserves explicit empty set semantics' do
|
|
{ 'name' => ['in', 'not_in'], 'aliases.name' => ['any', 'all', 'none'] }.each do
|
|
|field, operators|
|
|
operators.each do |operator|
|
|
condition = { 'conditions' => '', 'conditions.0.field' => field,
|
|
'conditions.0.operator' => operator }
|
|
expect(query(condition)['count']).to eq(2)
|
|
empty = condition.merge('conditions.0.values' => '1', 'condition_values' => '')
|
|
expected = ['in', 'any'].include?(operator) ? 0 : 2
|
|
expect(query(empty)['count']).to eq(expected)
|
|
expect(query(condition.merge('conditions.0.blank' => '1'))['count']).to eq(0)
|
|
# An explicit empty string element is a real value, not an unspecified set.
|
|
expect(query(predicate(field, operator, ''))['count']).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'ignores empty private predicates regardless of their position' do
|
|
first = predicate('name', 'ne', '')
|
|
.merge(predicate('resource_id', 'eq', tag.id.to_s, 1))
|
|
last = predicate('resource_id', 'eq', tag.id.to_s)
|
|
.merge(predicate('aliases.name', 'like', '', 1))
|
|
expect(query(first)['count']).to eq(2)
|
|
expect(query(last)['count']).to eq(2)
|
|
active = predicate('name', 'ne', 'unrelated')
|
|
.merge(predicate('aliases.name', 'like', '', 1))
|
|
expect(query(active)['count']).to eq(0)
|
|
end
|
|
|
|
it 'searches public literal markers and treats axis predicates as explicit' do
|
|
public_tag = create(:tag, primary_name: '********')
|
|
TagVersion.create!(tag: public_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))
|
|
expect(query['count']).to eq(3)
|
|
['eq', 'ne', 'like', 'in', 'not_in'].each do |operator|
|
|
expected = ['ne', 'not_in'].include?(operator) ? 0 : 1
|
|
expect(query(predicate('name', operator, '********'))['count']).to eq(expected)
|
|
end
|
|
blank = predicate('aliases.name', 'eq', '').merge('conditions.0.blank' => '1')
|
|
expect(query(blank)['count']).to eq(1)
|
|
['********', '', nil].each do |value|
|
|
condition = { 'conditions' => '', 'conditions.0.field' => 'name',
|
|
'conditions.0.operator' => 'axis_eq',
|
|
'conditions.0.value_type' => value.nil? ? 'null' : 'string' }
|
|
condition['conditions.0.value'] = value unless value.nil?
|
|
expected = value == '********' ? 1 : 0
|
|
expect(query(condition)['count']).to eq(expected)
|
|
drill = { 'drill' => '', 'drill.0.field' => 'name',
|
|
'drill.0.value_type' => value.nil? ? 'null' : 'string' }
|
|
drill['drill.0.value'] = value unless value.nil?
|
|
expect(query(drill)['count']).to eq(expected)
|
|
end
|
|
end
|
|
|
|
it 'keeps public identity and timeline predicates searchable' do
|
|
expect(query['count']).to eq(2)
|
|
values = { 'resource_id' => tag.id.to_s, 'version_no' => '2',
|
|
'event_type' => 'discard', 'user' => actor.id.to_s,
|
|
'created_at' => '2023-02-01T00:00:00Z' }
|
|
values.each do |field, value|
|
|
expected = ['resource_id', 'user'].include?(field) ? 2 : 1
|
|
expect(query(predicate(field, 'eq', value))['count']).to eq(expected)
|
|
end
|
|
condition = predicate('name', 'ne', '秘密のタグ')
|
|
.merge(predicate('event_type', 'eq', 'discard', 1))
|
|
expect(query(condition)['count']).to eq(0)
|
|
expect(query(predicate('category', 'eq', '********'))['count']).to eq(0)
|
|
expect(query(predicate('category', 'eq', 'general'))['count']).to eq(0)
|
|
end
|
|
|
|
it 'keeps public predicates available after combining multiple conditions' do
|
|
condition = predicate('resource_id', 'eq', tag.id.to_s)
|
|
.merge(predicate('event_type', 'eq', 'discard', 1))
|
|
expect(query(condition)['count']).to eq(1)
|
|
condition.merge!(predicate('name', 'ne', 'unrelated', 2))
|
|
expect(query(condition)['count']).to eq(0)
|
|
end
|
|
|
|
it 'keeps real name searches for administrators and excludes private details' do
|
|
condition = predicate('name', 'eq', '秘密のタグ')
|
|
details = query(condition.merge('mode' => 'details'))
|
|
expect(details['versions']).to be_empty
|
|
expect(details['output_count']).to eq(0)
|
|
masked = query(predicate('name', 'eq', '********').merge('mode' => 'details'))
|
|
expect(masked['versions']).to be_empty
|
|
expect(masked['output_count']).to eq(0)
|
|
sign_in_as(admin)
|
|
expect(query(condition)['count']).to eq(2)
|
|
expect(query(condition.merge('mode' => 'details'))['versions'].length).to eq(2)
|
|
end
|
|
|
|
it 'rejects private names reached through a post tag snapshot' do
|
|
post = create(:post)
|
|
PostVersion.create!(post:, version_no: 1, event_type: 'create', title: 'post',
|
|
url: post.url, tags: '', parent_post_ids: '',
|
|
tags_json: [{ tag_id: tag.id, version_no: 1, name: '秘密のタグ',
|
|
category: 'general', sections: [] }],
|
|
created_at: Time.utc(2024))
|
|
['tags.name', 'tags.names.name', 'tags.name_ja'].each do |field|
|
|
['eq', 'ne', 'like', 'any', 'all', 'none'].each do |operator|
|
|
['秘密のタグ', '********', '%'].each do |value|
|
|
settings = predicate(field, operator, value).merge('resource' => 'posts')
|
|
expect(query(settings)['count']).to eq(0)
|
|
end
|
|
end
|
|
blank = predicate(field, 'eq', '').merge('resource' => 'posts',
|
|
'conditions.0.blank' => '1')
|
|
expect(query(blank)['count']).to eq(0)
|
|
end
|
|
end
|
|
end
|