Compare commits

...

2 Commits

Author SHA1 Message Date
みてるぞ ffc023dad3 #327 2026-05-04 16:19:22 +09:00
みてるぞ c1af29617f #327 2026-05-04 15:42:17 +09:00
7 changed files with 239 additions and 78 deletions
@@ -1,8 +1,5 @@
class UsersController < ApplicationController class UsersController < ApplicationController
def create def create
return head :unprocessable_entity if request.remote_ip.blank?
return head :forbidden if ip_address_banned?
user = nil user = nil
User.transaction do User.transaction do
user = User.create!(inheritance_code: SecureRandom.uuid, role: :guest) user = User.create!(inheritance_code: SecureRandom.uuid, role: :guest)
@@ -15,9 +12,6 @@ class UsersController < ApplicationController
end end
def verify def verify
return head :unprocessable_entity if request.remote_ip.blank?
return head :forbidden if ip_address_banned?
user = User.find_by(inheritance_code: params[:code]) user = User.find_by(inheritance_code: params[:code])
return render json: { valid: false } unless user return render json: { valid: false } unless user
return head :forbidden if user.banned? return head :forbidden if user.banned?
@@ -65,11 +59,4 @@ class UsersController < ApplicationController
UserIp.create_or_find_by!(user:, ip_address:) UserIp.create_or_find_by!(user:, ip_address:)
end end
def ip_address_banned?
ip_address = IpAddress.find_by(ip_address: IPAddr.new(request.remote_ip).hton)
return false unless ip_address
ip_address.banned?
end
end end
+1 -1
View File
@@ -5,6 +5,6 @@ class IpAddress < ApplicationRecord
has_many :users, through: :user_ips has_many :users, through: :user_ips
def banned? = banned_at.present? def banned? = banned_at.present?
def ban! = banned? or update!(banned_at: Time.current) def ban! = banned? || update!(banned_at: Time.current)
def unban! = update!(banned_at: nil) def unban! = update!(banned_at: nil)
end end
+1 -1
View File
@@ -22,6 +22,6 @@ class User < ApplicationRecord
def gte_member? = member? || admin? def gte_member? = member? || admin?
def banned? = banned_at.present? def banned? = banned_at.present?
def ban! = banned? or update!(banned_at: Time.current) def ban! = banned? || update!(banned_at: Time.current)
def unban! = update!(banned_at: nil) def unban! = update!(banned_at: nil)
end end
+10
View File
@@ -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
+12 -3
View File
@@ -1,15 +1,24 @@
FactoryBot.define do FactoryBot.define do
factory :user do factory :user do
name { "test-user" } name { nil }
inheritance_code { SecureRandom.uuid } inheritance_code { SecureRandom.uuid }
role { "guest" } role { 'guest' }
banned_at { nil }
trait :guest do
role { 'guest' }
end
trait :member do trait :member do
role { "member" } role { 'member' }
end end
trait :admin do trait :admin do
role { 'admin' } role { 'admin' }
end end
trait :banned do
banned_at { Time.current }
end
end end
end end
+213 -56
View File
@@ -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(response).to have_http_status(:created)
expect(json["code"]).to be_present expect(json['code']).to be_present
expect(json["user"]["role"]).to eq("guest") 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
end end
describe "POST /users/code/renew" do describe 'POST /users/code/renew' do
it "returns 401 when not logged in" do it 'returns 401 when not logged in' do
sign_out post '/users/code/renew'
post "/users/code/renew"
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end 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 end
describe "PUT /users/:id" do describe 'PUT /users/:id' do
let(:user) { create(:user, name: "old-name", role: "guest") } 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) expect(response).to have_http_status(:unauthorized)
end end
it "returns 400 when name is blank" do it 'returns 400 when name is blank' do
sign_in_as(user) put "/users/#{user.id}",
put "/users/#{user.id}", params: { name: " " } params: { name: ' ' },
headers: auth_headers(user)
expect(response).to have_http_status(:bad_request) expect(response).to have_http_status(:bad_request)
end end
it "updates name and returns 201 with user slice" do it 'updates name and returns user slice' do
sign_in_as(user) put "/users/#{user.id}",
put "/users/#{user.id}", params: { name: "new-name" } params: { name: 'new-name' },
headers: auth_headers(user)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json["id"]).to eq(user.id) expect(json['id']).to eq(user.id)
expect(json["name"]).to eq("new-name") expect(json['name']).to eq('new-name')
user.reload 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
end end
describe "POST /users/verify" do describe 'POST /users/verify' do
it "returns valid:false when code not found" do it 'returns valid:false when code not found' do
post "/users/verify", params: { code: "nope" } post '/users/verify', params: { code: 'nope' }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json["valid"]).to eq(false) expect(json['valid']).to eq(false)
end end
it "creates IpAddress and UserIp, and returns valid:true with user slice" do it 'returns 403 when current IP address is banned' do
user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest") user = create(:user, inheritance_code: SecureRandom.uuid, role: 'guest')
# request.remote_ip を固定 IpAddress.create!(
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10") ip_address: IPAddr.new(remote_ip).hton,
banned_at: Time.current
)
expect { 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(: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) }.to change(UserIp, :count).by(1)
.and change(IpAddress, :count).by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json["valid"]).to eq(true) expect(json['valid']).to eq(true)
expect(json["user"]["id"]).to eq(user.id) expect(json['user']['id']).to eq(user.id)
expect(json["user"]["inheritance_code"]).to eq(user.inheritance_code) expect(json['user']['inheritance_code']).to eq(user.inheritance_code)
expect(json["user"]["role"]).to eq("guest") expect(json['user']['role']).to eq('guest')
# ついでに IpAddress もできてることを確認(ipの保存形式がバイナリでも count で見れる) ip_address = IpAddress.find_by(ip_address: IPAddr.new(remote_ip).hton)
expect(IpAddress.count).to be >= 1 expect(ip_address).to be_present
expect(UserIp.exists?(user:, ip_address:)).to eq(true)
end end
it "is idempotent for same user+ip (does not create duplicate UserIp)" do it 'is idempotent for same user and same IP address' do
user = create(:user, inheritance_code: SecureRandom.uuid, role: "guest") 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")
post "/users/verify", params: { code: user.inheritance_code } post '/users/verify', params: { code: user.inheritance_code }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect { expect {
post "/users/verify", params: { code: user.inheritance_code } post '/users/verify', params: { code: user.inheritance_code }
}.not_to change(UserIp, :count) }.not_to change(UserIp, :count)
expect(response).to have_http_status(:ok) 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
end end
describe "GET /users/me" do describe 'GET /users/me' do
it "returns 404 when code not found" do it 'returns 404 when code not found' do
get "/users/me", params: { code: "nope" } get '/users/me', params: { code: 'nope' }
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
it "returns user slice when found" do it 'returns user slice when found' do
user = create(:user, inheritance_code: SecureRandom.uuid, name: "me", role: "guest") user = create(:user, inheritance_code: SecureRandom.uuid, name: 'me', role: 'guest')
get "/users/me", params: { code: user.inheritance_code }
get '/users/me', params: { code: user.inheritance_code }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json["id"]).to eq(user.id) expect(json['id']).to eq(user.id)
expect(json["name"]).to eq("me") expect(json['name']).to eq('me')
expect(json["inheritance_code"]).to eq(user.inheritance_code) expect(json['inheritance_code']).to eq(user.inheritance_code)
expect(json["role"]).to eq("guest") 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 end
end end
+2 -4
View File
@@ -2,14 +2,12 @@ module TestRecords
def create_member_user! def create_member_user!
User.create!(name: 'spec user', User.create!(name: 'spec user',
inheritance_code: SecureRandom.hex(16), inheritance_code: SecureRandom.hex(16),
role: 'member', role: 'member')
banned: false)
end end
def create_admin_user! def create_admin_user!
User.create!(name: 'spec admin', User.create!(name: 'spec admin',
inheritance_code: SecureRandom.hex(16), inheritance_code: SecureRandom.hex(16),
role: 'admin', role: 'admin')
banned: false)
end end
end end