378 行
12 KiB
Ruby
378 行
12 KiB
Ruby
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
|