このコミットが含まれているのは:
2026-10-19 23:08:59 +09:00
コミット 4ab13c4244
56個のファイルの変更、1071行の追加、259行の削除
+43
ファイルの表示
@@ -0,0 +1,43 @@
require 'rails_helper'
RSpec.describe 'Post filter identity across locales', type: :request do
include_context 'Ainu locale'
let!(:tag) { create(:tag, primary_name: 'legacy_union_source') }
let!(:external) { create(:external_tag, name: 'shared') }
let!(:internal_post) { create(:post) }
let!(:external_post) { create(:post) }
let!(:other_post) { create(:post) }
before do
# Legacy data can predate the current prohibition on internal nico: names.
primary_tag_name_for(tag, 'ja').update_columns(name: 'nico:shared')
primary_tag_name_for(tag, 'ain').update!(name: 'シロシ', auto_generated: false)
create(:tag_name, :alias, tag:, name: 'union_alias', language_code: 'ja')
PostTag.create!(post: internal_post, tag:)
PostExternalTag.create!(post: external_post, external_tag: external)
end
['nico:shared', 'union_alias', 'not:nico:shared'].each do |literal|
it "preserves the complete match set for #{ literal }" do
get '/posts', params: { locale: 'ja', tags: literal }
expect(response).to have_http_status(:ok)
original_ids = json.fetch('posts').map { _1.fetch('id') }
expected = literal.start_with?('not:') ? [other_post.id] :
[internal_post.id, external_post.id]
expect(original_ids).to match_array(expected)
get '/posts/tag_filters', params: {
locale: 'ja', target_locale: 'ain-Kana', tags: literal }
expect(response).to have_http_status(:ok)
references = json.fetch('references')
expect(references.first).to include('tag_id' => tag.id, 'external_tag_id' => external.id)
get '/posts', params: {
locale: 'ain-Kana', tags: references.map { _1.fetch('name') }.join(' '),
tag_refs: references.to_json }
expect(response).to have_http_status(:ok)
expect(json.fetch('posts').map { _1.fetch('id') }).to match_array(original_ids)
end
end
end
+1 -1
ファイルの表示
@@ -900,7 +900,7 @@ RSpec.describe 'Posts API', type: :request do
query_count =
count_sql_queries do
get "/posts/#{ post_record.id }",
headers: { 'X-Transfer-Code' => user.inheritance_code }
headers: cookie_headers(user)
end
expect(response).to have_http_status(:ok)
+1 -1
ファイルの表示
@@ -4,7 +4,7 @@ RSpec.describe 'Tag description Wiki facade writes', type: :request do
include WikiRecords
let!(:user) { create_member_user! }
let!(:tag) { create(:tag, primary_name: 'description_write') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
def stored_state
[Tag, TagName, TagVersion, TagDescription, TagDescriptionVersion].map do |model|
+3 -3
ファイルの表示
@@ -7,7 +7,7 @@ RSpec.describe 'Tag description native API', type: :request do
let(:editor) { create(:user, :member, name: 'description editor') }
let(:tag_name) { create(:tag_name, name: 'native_description') }
let(:tag) { create(:tag, primary_tag_name: tag_name) }
let(:headers) { { 'X-Transfer-Code' => editor.inheritance_code } }
let(:headers) { cookie_headers(editor) }
let(:endpoint) { "/tags/#{ tag.id }/description" }
def stored_state
@@ -285,7 +285,7 @@ RSpec.describe 'Tag description native API', type: :request do
guest = create(:user, :guest)
before_state = stored_state
native_write(body: 'forbidden',
request_headers: { 'X-Transfer-Code' => guest.inheritance_code })
request_headers: cookie_headers(guest))
expect(response).to have_http_status(:forbidden)
expect(stored_state).to eq(before_state)
end
@@ -294,7 +294,7 @@ RSpec.describe 'Tag description native API', type: :request do
banned = create(:user, :member, :banned)
before_state = stored_state
native_write(body: 'forbidden',
request_headers: { 'X-Transfer-Code' => banned.inheritance_code })
request_headers: cookie_headers(banned))
expect(response).to have_http_status(:forbidden)
expect(stored_state).to eq(before_state)
end
+1 -1
ファイルの表示
@@ -7,7 +7,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
let!(:user) { create_member_user! }
let!(:tag) { create(:tag, primary_name: 'japanese_member') }
let!(:ainu_name) { add_ainu_name(tag, 'ainu_member') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
let(:members) { [{ 'tag_id' => tag.id }] }
let(:placements) do
[{ 'section_name' => 'section', 'description' => nil, 'children' => [
+1 -1
ファイルの表示
@@ -37,7 +37,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
markdown.lines.map(&:chomp).reject(&:empty?))
post '/wiki', params: { title: 'タググループ:再解析', body: },
headers: { 'X-Transfer-Code' => user.inheritance_code }
headers: cookie_headers(user)
expect(response).to have_http_status(:created)
parsed = group_rows('tag_groups', name: '再解析').sole
expect_group_snapshot(parsed.fetch('id'), number: 1, placements:, members:, description:)
+4 -4
ファイルの表示
@@ -7,7 +7,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
let!(:user) { create_member_user! }
let!(:tag) { group_tag('洗澡鹿') }
let!(:other_tag) { group_tag('伊地知ニジカ') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
let(:members) { [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }] }
let(:placements) { [{ 'tag_id' => tag.id, 'children' => [] }] }
@@ -148,7 +148,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
body = group_markdown(page)
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id) do
update_group(body:, request_headers: { 'X-Transfer-Code' => editor.inheritance_code })
update_group(body:, request_headers: cookie_headers(editor))
end
end
end
@@ -212,7 +212,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
it 'records the editor and update time' do
editor = create(:user, :member, name: '編輯者')
travel_to(Time.current + 1.hour) do
update_group(request_headers: { 'X-Transfer-Code' => editor.inheritance_code })
update_group(request_headers: cookie_headers(editor))
expect(response).to have_http_status(:ok)
current = group_rows('tag_groups', id: page.id).sole
version = group_version(page.id, 2)
@@ -271,7 +271,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
when :guest then create(:user, :guest)
when :banned then create(:user, :member, :banned)
end
request_headers = actor ? { 'X-Transfer-Code' => actor.inheritance_code } : { }
request_headers = actor ? cookie_headers(actor) : { }
before_state = group_state
write_group(operation, request_headers)
expect(response).to have_http_status(role ? :forbidden : :unauthorized)
+2 -2
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe 'Tag group write integrity', type: :request do
let!(:user) { create_member_user! }
let!(:tag) { group_tag('洗澡鹿') }
let!(:other_tag) { group_tag('伊地知ニジカ') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
let(:members) { [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }] }
let(:placements) { [{ 'tag_id' => tag.id, 'children' => [] }] }
@@ -141,7 +141,7 @@ RSpec.describe 'Tag group write integrity', type: :request do
end
let!(:id) { seed_group(name: '更新', members:, placements:) }
let!(:editor) { create(:user, :member) }
let(:headers) { { 'X-Transfer-Code' => editor.inheritance_code } }
let(:headers) { cookie_headers(editor) }
it 'keeps all membership and placement audit fields on a name-only edit' do
before_rows = group_state.slice('tag_group_tags', 'tag_group_groups', 'tag_group_placements')
+4 -4
ファイルの表示
@@ -47,7 +47,7 @@ RSpec.describe 'Tag group native API', type: :request do
when :guest then create(:user, :guest)
when :banned then create(:user, :member, :banned)
end
headers = actor ? { 'X-Transfer-Code' => actor.inheritance_code } : { }
headers = actor ? cookie_headers(actor) : { }
before_state = group_state
if operation == :create
post '/tag_groups', params: { }, headers:, as: :json
@@ -61,7 +61,7 @@ RSpec.describe 'Tag group native API', type: :request do
it "rejects a banned IP for native #{ operation }" do
IpAddress.create!(ip_address: IPAddr.new('127.0.0.1').hton, banned_at: Time.current)
headers = { 'X-Transfer-Code' => user.inheritance_code }
headers = cookie_headers(user)
before_state = group_state
if operation == :create
post '/tag_groups', params: { }, headers:, as: :json
@@ -77,7 +77,7 @@ RSpec.describe 'Tag group native API', type: :request do
let!(:tag) { group_tag('掲載タグ') }
let!(:unplaced) { group_tag('未掲載タグ') }
let!(:child_id) { seed_group(name: '子', description: '子の説明') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
let(:members) do
[{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }, { 'tag_id' => unplaced.id }]
end
@@ -300,7 +300,7 @@ RSpec.describe 'Tag group native API', type: :request do
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:) do
put "/tag_groups/#{ group_id }", params: synchronised_payload,
headers: { 'X-Transfer-Code' => editor.inheritance_code }, as: :json
headers: cookie_headers(editor), as: :json
end
end
end
+1 -1
ファイルの表示
@@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe 'Tag resource ID allocation', type: :request do
include WikiRecords
let!(:user) { create_member_user! }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
def create_description name
tag = create(:tag, primary_name: name)
+95 -4
ファイルの表示
@@ -10,10 +10,16 @@ RSpec.describe 'Users', type: :request do
end
def auth_headers(user)
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
describe 'GET /users/session' do
it 'does not accept the retired transfer-code header' do
user = create(:user)
get '/users/session', headers: { 'X-Transfer-Code' => user.inheritance_code }
expect(response).to have_http_status(:unauthorized)
end
it 'requires authentication' do
get '/users/session'
expect(response).to have_http_status(:unauthorized)
@@ -251,17 +257,102 @@ RSpec.describe 'Users', type: :request do
end
end
describe 'Cookie sessions with CSRF enabled', csrf: true do
def csrf_headers
get '/users/csrf'
expect(response).to have_http_status(:ok)
{ 'X-CSRF-Token' => json.fetch('token'), 'Origin' => 'http://www.example.com' }
end
def establish_session(user)
post '/users/verify', params: { code: user.inheritance_code }, headers: csrf_headers
expect(response).to have_http_status(:ok)
expect(json.fetch('valid')).to be(true)
end
it 'rejects a write without a token before creating an account' do
expect { post '/users' }.not_to change(User, :count)
expect(response).to have_http_status(:forbidden)
expect(response.headers['X-CSRF-Invalid']).to eq('1')
end
it 'uses the encrypted persistent Cookie for CSR and SSR session reads' do
user = create(:user)
establish_session(user)
cookie = Array(response.headers['set-cookie']).join
expect(cookie).to include('httponly', 'samesite=lax')
expect(cookie).not_to include(user.inheritance_code)
expires = Time.httpdate(cookie.match(/expires=([^;]+)/i)[1])
expect(expires).to be > 19.years.from_now
get '/users/session'
expect(json.fetch('id')).to eq(user.id)
expect(response.body).not_to include(user.inheritance_code)
get '/users/me'
expect(json.fetch('inheritance_code')).to eq(user.inheritance_code)
expect(response.headers['Cache-Control']).to include('no-store')
end
it 'rejects a cross-origin write with an otherwise valid token' do
headers = csrf_headers.merge('Origin' => 'https://other.invalid')
expect { post '/users', headers: }.not_to change(User, :count)
expect(response).to have_http_status(:forbidden)
end
it 'does not replace an existing Cookie identity during recovery' do
current = create(:user)
old = create(:user)
establish_session(current)
post '/users/verify', params: { code: old.inheritance_code, restore: true },
headers: csrf_headers
expect(json.fetch('user').fetch('id')).to eq(current.id)
get '/users/session'
expect(json.fetch('id')).to eq(current.id)
end
it 'allows explicit transfer and refreshes the active session' do
first = create(:user)
second = create(:user)
establish_session(first)
establish_session(second)
get '/users/session'
expect(json.fetch('id')).to eq(second.id)
end
it 'keeps the renewed session and rejects the previous credential' do
user = create(:user)
establish_session(user)
old_cookie = Array(response.headers['set-cookie']).first.split(';').first
old_code = user.inheritance_code
post '/users/code/renew', headers: csrf_headers
expect(response).to have_http_status(:ok)
expect(json.fetch('code')).not_to eq(old_code)
get '/users/session'
expect(json.fetch('id')).to eq(user.id)
other_browser = ActionDispatch::Integration::Session.new(Rails.application)
other_browser.get('/users/session', headers: { 'Cookie' => old_cookie })
expect(other_browser.response).to have_http_status(:unauthorized)
end
it 'clears browser authentication on session deletion' do
establish_session(create(:user))
delete '/users/session', headers: csrf_headers
expect(response).to have_http_status(:no_content)
get '/users/session'
expect(response).to have_http_status(:unauthorized)
end
end
describe 'GET /users/me' do
it 'returns 404 when code not found' do
it 'requires a session even when a code is supplied in the query' do
get '/users/me', params: { code: 'nope' }
expect(response).to have_http_status(:not_found)
expect(response).to have_http_status(:unauthorized)
end
it 'returns user slice when found' do
user = create(:user, inheritance_code: SecureRandom.uuid, name: 'me', role: 'guest')
get '/users/me', params: { code: user.inheritance_code }
get '/users/me', headers: cookie_headers(user)
expect(response).to have_http_status(:ok)
expect(json['id']).to eq(user.id)
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
let!(:user) { create_member_user! }
def auth_headers user
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
it 'returns 409 when base_revision_id is stale' do
+2 -2
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe 'Wiki creation freeze', type: :request do
include WikiRecords
let!(:user) { create_member_user! }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
context 'existing description write protection' do
@@ -18,7 +18,7 @@ RSpec.describe 'Wiki creation freeze', type: :request do
it "rejects #{ role || 'anonymous' } without changing either history" do
actor = role && create(:user, role == :banned ? :member : role)
actor.update!(banned_at: Time.current) if role == :banned
request_headers = actor ? { 'X-Transfer-Code' => actor.inheritance_code } : { }
request_headers = actor ? cookie_headers(actor) : { }
before_state = group_state
put "/wiki/#{ page.id }", params: { title: tag.name('ja'), body: 'after' },
headers: request_headers
+1 -1
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
let!(:user) { create_member_user! }
let!(:tag) { create(:tag, primary_name: 'japanese_title') }
let!(:ainu_name) { add_ainu_name(tag, 'ainu_title') }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
let(:headers) { cookie_headers(user) }
let!(:japanese) do
TagDescriptionOperation.create!(
Locale.nipponese, tag:, body: 'Japanese body', created_by_user: user)
+3 -3
ファイルの表示
@@ -6,7 +6,7 @@ require 'securerandom'
RSpec.describe 'Wiki API', type: :request do
include WikiRecords
def auth_headers(user)
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
let!(:user) { create_member_user! }
@@ -80,7 +80,7 @@ RSpec.describe 'Wiki API', type: :request do
let(:guest) { create(:user, role: 'guest') }
def auth_headers(user)
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
context 'when not logged in' do
@@ -126,7 +126,7 @@ RSpec.describe 'Wiki API', type: :request do
let(:guest) { create(:user, role: 'guest', inheritance_code: SecureRandom.hex(16)) }
def auth_headers(user)
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
let!(:test_tag_name) { create(:tag, primary_name: 'TestPage').tag_name('ja') }
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe 'Wiki title collision', type: :request do
let!(:user) { create_member_user! }
def auth_headers user
{ 'X-Transfer-Code' => user.inheritance_code }
cookie_headers(user)
end
def create_wiki_page title:, body:
+1 -1
ファイルの表示
@@ -17,7 +17,7 @@ RSpec.describe 'Removed standalone Wiki resources', type: :request do
user
before_state = group_state
post '/wiki', params: { title:, body: 'body' },
headers: { 'X-Transfer-Code' => user.inheritance_code }
headers: cookie_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(group_state).to eq(before_state)
end