require 'rails_helper' RSpec.describe 'Users', type: :request do let(:remote_ip) { '203.0.113.10' } before do allow_any_instance_of(ActionDispatch::Request) .to receive(:remote_ip) .and_return(remote_ip) end def auth_headers(user) 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) end it 'returns public user fields without recording a visit' do user = create(:user, name: 'SSR user') expect { get '/users/session', headers: auth_headers(user) }.not_to change(UserIp, :count) expect(response).to have_http_status(:ok) expect(json).to eq('id' => user.id, 'name' => user.name, 'role' => user.role) expect(response.body).not_to include(user.inheritance_code) end end describe 'POST /users' do it 'creates guest user, IpAddress and UserIp, and returns code' do expect { post '/users' }.to change(User, :count).by(1) .and change(IpAddress, :count).by(1) .and change(UserIp, :count).by(1) expect(response).to have_http_status(:created) expect(json['code']).to be_present expect(json['user']['role']).to eq('guest') user = User.last ip_address = IpAddress.find_by(ip_address: IPAddr.new(remote_ip).hton) expect(user.role).to eq('guest') expect(ip_address).to be_present expect(UserIp.exists?(user:, ip_address:)).to eq(true) end it 'returns 403 and does not create user when current IP address is banned' do IpAddress.create!( ip_address: IPAddr.new(remote_ip).hton, banned_at: Time.current ) expect { post '/users' }.not_to change(User, :count) expect(response).to have_http_status(:forbidden) expect(UserIp.count).to eq(0) end end describe 'POST /users/code/renew' do it 'returns 401 when not logged in' do post '/users/code/renew' expect(response).to have_http_status(:unauthorized) end it 'returns 403 when current user is banned' do user = create(:user, :banned) post '/users/code/renew', headers: auth_headers(user) expect(response).to have_http_status(:forbidden) end it 'returns 403 when current IP address is banned' do user = create(:user) IpAddress.create!( ip_address: IPAddr.new(remote_ip).hton, banned_at: Time.current ) post '/users/code/renew', headers: auth_headers(user) expect(response).to have_http_status(:forbidden) end end describe 'PUT /users/:id' do let(:user) { create(:user, name: 'old-name', role: 'guest') } it 'returns 401 when current_user id mismatch' do other_user = create(:user) put "/users/#{user.id}", params: { name: 'new-name' }, headers: auth_headers(other_user) expect(response).to have_http_status(:unauthorized) end it 'returns 422 when name is blank' do put "/users/#{user.id}", params: { name: ' ' }, headers: auth_headers(user) expect(response).to have_http_status(:unprocessable_entity) expect(json.fetch('errors')).to include( 'name' => ['名前は必須です.']) end it 'updates name and returns user slice' do put "/users/#{user.id}", params: { name: 'new-name' }, headers: auth_headers(user) expect(response).to have_http_status(:ok) expect(json['id']).to eq(user.id) expect(json['name']).to eq('new-name') user.reload expect(user.name).to eq('new-name') end it 'returns 403 when current user is banned' do user.update!(banned_at: Time.current) put "/users/#{user.id}", params: { name: 'new-name' }, headers: auth_headers(user) expect(response).to have_http_status(:forbidden) user.reload expect(user.name).to eq('old-name') end it 'returns 403 when current IP address is banned' do IpAddress.create!( ip_address: IPAddr.new(remote_ip).hton, banned_at: Time.current ) put "/users/#{user.id}", params: { name: 'new-name' }, headers: auth_headers(user) expect(response).to have_http_status(:forbidden) user.reload expect(user.name).to eq('old-name') end end describe 'POST /users/verify' do it 'returns valid:false when code not found' do post '/users/verify', params: { code: 'nope' } expect(response).to have_http_status(:ok) expect(json['valid']).to eq(false) end it 'returns 403 when current IP address is banned' do user = create(:user, inheritance_code: SecureRandom.uuid, role: 'guest') IpAddress.create!( ip_address: IPAddr.new(remote_ip).hton, banned_at: Time.current ) expect { post '/users/verify', params: { code: user.inheritance_code } }.not_to change(UserIp, :count) expect(response).to have_http_status(:forbidden) end it 'returns 403 when verified user is banned' do user = create( :user, :banned, inheritance_code: SecureRandom.uuid, role: 'guest' ) expect { post '/users/verify', params: { code: user.inheritance_code } }.not_to change(UserIp, :count) expect(response).to have_http_status(:forbidden) end it 'creates IpAddress and UserIp, and returns valid:true with user slice' do user = create(:user, inheritance_code: SecureRandom.uuid, role: 'guest') expect { post '/users/verify', params: { code: user.inheritance_code } }.to change(UserIp, :count).by(1) .and change(IpAddress, :count).by(1) expect(response).to have_http_status(:ok) expect(json['valid']).to eq(true) expect(json['user']['id']).to eq(user.id) expect(json['user']['inheritance_code']).to eq(user.inheritance_code) expect(json['user']['role']).to eq('guest') ip_address = IpAddress.find_by(ip_address: IPAddr.new(remote_ip).hton) expect(ip_address).to be_present expect(UserIp.exists?(user:, ip_address:)).to eq(true) end it 'is idempotent for same user and same IP address' do user = create(:user, inheritance_code: SecureRandom.uuid, role: 'guest') post '/users/verify', params: { code: user.inheritance_code } expect(response).to have_http_status(:ok) expect { post '/users/verify', params: { code: user.inheritance_code } }.not_to change(UserIp, :count) expect(response).to have_http_status(:ok) expect(json['valid']).to eq(true) end it 'creates another UserIp for same user and different IP address' do user = create(:user, inheritance_code: SecureRandom.uuid, role: 'guest') post '/users/verify', params: { code: user.inheritance_code } expect(response).to have_http_status(:ok) allow_any_instance_of(ActionDispatch::Request) .to receive(:remote_ip) .and_return('203.0.113.11') expect { post '/users/verify', params: { code: user.inheritance_code } }.to change(UserIp, :count).by(1) expect(response).to have_http_status(:ok) expect(json['valid']).to eq(true) 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 '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(:unauthorized) end it 'returns user slice when found' do user = create(:user, inheritance_code: SecureRandom.uuid, name: 'me', role: 'guest') get '/users/me', headers: cookie_headers(user) expect(response).to have_http_status(:ok) expect(json['id']).to eq(user.id) expect(json['name']).to eq('me') expect(json['inheritance_code']).to eq(user.inheritance_code) expect(json['role']).to eq('guest') end it 'returns 403 when current IP address is banned' do user = create(:user, inheritance_code: SecureRandom.uuid) IpAddress.create!( ip_address: IPAddr.new(remote_ip).hton, banned_at: Time.current ) get '/users/me', params: { code: user.inheritance_code } expect(response).to have_http_status(:forbidden) end end end