diff --git a/backend/spec/factories/ip_addresses.rb b/backend/spec/factories/ip_addresses.rb new file mode 100644 index 0000000..6ffc363 --- /dev/null +++ b/backend/spec/factories/ip_addresses.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :ip_address do + ip_address { IPAddr.new('203.0.113.10').hton } + banned_at { nil } + + trait :banned do + banned_at { Time.current } + end + end +end diff --git a/backend/spec/factories/users.rb b/backend/spec/factories/users.rb index f7db70a..9225a23 100644 --- a/backend/spec/factories/users.rb +++ b/backend/spec/factories/users.rb @@ -1,15 +1,24 @@ FactoryBot.define do factory :user do - name { "test-user" } + name { nil } inheritance_code { SecureRandom.uuid } - role { "guest" } + role { 'guest' } + banned_at { nil } + + trait :guest do + role { 'guest' } + end trait :member do - role { "member" } + role { 'member' } end trait :admin do role { 'admin' } end + + trait :banned do + banned_at { Time.current } + end end end diff --git a/backend/spec/requests/users_spec.rb b/backend/spec/requests/users_spec.rb index 1f28e95..67c556f 100644 --- a/backend/spec/requests/users_spec.rb +++ b/backend/spec/requests/users_spec.rb @@ -1,109 +1,266 @@ -require "rails_helper" +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) + { 'X-Transfer-Code' => user.inheritance_code } + 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) -RSpec.describe "Users", type: :request do - describe "POST /users" do - it "creates guest user and returns code" do - post "/users" expect(response).to have_http_status(:created) - expect(json["code"]).to be_present - expect(json["user"]["role"]).to eq("guest") + 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 - sign_out - post "/users/code/renew" + 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") } + 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) - it "returns 401 when current_user id mismatch" do - sign_in_as(create(:user)) - put "/users/#{user.id}", params: { name: "new-name" } expect(response).to have_http_status(:unauthorized) end - it "returns 400 when name is blank" do - sign_in_as(user) - put "/users/#{user.id}", params: { name: " " } + it 'returns 400 when name is blank' do + put "/users/#{user.id}", + params: { name: ' ' }, + headers: auth_headers(user) + expect(response).to have_http_status(:bad_request) end - it "updates name and returns 201 with user slice" do - sign_in_as(user) - put "/users/#{user.id}", params: { name: "new-name" } + 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") + expect(json['id']).to eq(user.id) + expect(json['name']).to eq('new-name') user.reload - expect(user.name).to eq("new-name") + 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" } + 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) + expect(json['valid']).to eq(false) end - it "creates IpAddress and UserIp, and returns valid:true with user slice" do - user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest") + 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 - # request.remote_ip を固定 - allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10") + 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 } + 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") + 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') - # ついでに IpAddress もできてることを確認(ipの保存形式がバイナリでも count で見れる) - expect(IpAddress.count).to be >= 1 + 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+ip (does not create duplicate UserIp)" do - user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest") - allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10") + 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 } + post '/users/verify', params: { code: user.inheritance_code } expect(response).to have_http_status(:ok) expect { - post "/users/verify", params: { code: user.inheritance_code } + 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) + 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 "GET /users/me" do - it "returns 404 when code not found" do - get "/users/me", params: { code: "nope" } + describe 'GET /users/me' do + it 'returns 404 when code not found' do + get '/users/me', params: { code: 'nope' } + expect(response).to have_http_status(:not_found) 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 } + 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 } 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") + 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 diff --git a/backend/spec/support/test_records.rb b/backend/spec/support/test_records.rb index 350b8da..b6a7cd5 100644 --- a/backend/spec/support/test_records.rb +++ b/backend/spec/support/test_records.rb @@ -2,14 +2,12 @@ module TestRecords def create_member_user! User.create!(name: 'spec user', inheritance_code: SecureRandom.hex(16), - role: 'member', - banned: false) + role: 'member') end def create_admin_user! User.create!(name: 'spec admin', inheritance_code: SecureRandom.hex(16), - role: 'admin', - banned: false) + role: 'admin') end end