Merge remote-tracking branch 'origin/main' into feature/047

This commit is contained in:
2026-05-04 16:24:34 +09:00
21 changed files with 774 additions and 128 deletions
@@ -1,14 +1,16 @@
class ApplicationController < ActionController::API
before_action :reject_banned_ip_address!
before_action :authenticate_user
before_action :reject_banned_user!
def current_user
@current_user
end
def current_user = @current_user
private
def authenticate_user
code = request.headers['X-Transfer-Code'] || request.headers['HTTP_X_TRANSFER_CODE']
return if code.blank?
@current_user = User.find_by(inheritance_code: code)
end
@@ -22,4 +24,17 @@ class ApplicationController < ActionController::API
s.in?(['', '1', 'true', 'on', 'yes'])
end
end
def reject_banned_ip_address!
ip_address = IpAddress.find_by(ip_address: IPAddr.new(request.remote_ip).hton)
return unless ip_address&.banned?
head :forbidden
end
def reject_banned_user!
return unless current_user&.banned?
head :forbidden
end
end
+48 -2
View File
@@ -1,3 +1,7 @@
require 'net/http'
require 'uri'
class TagsController < ApplicationController
def index
post_id = params[:post]
@@ -182,7 +186,8 @@ class TagsController < ApplicationController
.find_by(id: params[:id])
return head :not_found unless tag
render json: DeerjikistRepr.many(tag.deerjikists)
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end
def deerjikists_by_name
@@ -194,7 +199,31 @@ class TagsController < ApplicationController
.find_by(tag_names: { name: })
return head :not_found unless tag
render json: DeerjikistRepr.many(tag.deerjikists)
render json: { tag: TagRepr.base(tag),
deerjikists: DeerjikistRepr.many(tag.deerjikists) }
end
def update_deerjikists
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(id: params[:id])
return head :not_found unless tag
ApplicationRecord.transaction do
tag.deerjikists = []
params[:_json].each do
platform = _1[:platform]
code = normalise_deerjikist_code(platform, _1[:code])
deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
deerjikist.tag = tag
deerjikist.save!
end
end
render json: DeerjikistRepr.many(tag.reload.deerjikists)
end
def materials_by_name
@@ -391,4 +420,21 @@ class TagsController < ApplicationController
TagImplication.create!(tag:, parent_tag:)
end
end
def normalise_deerjikist_code platform, code
return code if platform != 'youtube' || code[0] != '@'
url = "https://www.youtube.com/#{ code }"
html = Net::HTTP.get(URI(url))
canonical = html[
/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})"/,
1]
return canonical if canonical
html[/"channelId":"(UC[a-zA-Z0-9_-]{22})"/, 1] || html[/\bUC[a-zA-Z0-9_-]{22}\b/]
rescue
nil
end
end
+1 -5
View File
@@ -1,9 +1,6 @@
class UsersController < ApplicationController
def create
return head :unprocessable_entity if request.remote_ip.blank?
user = nil
User.transaction do
user = User.create!(inheritance_code: SecureRandom.uuid, role: :guest)
attach_ip_address!(user)
@@ -17,8 +14,7 @@ class UsersController < ApplicationController
def verify
user = User.find_by(inheritance_code: params[:code])
return render json: { valid: false } unless user
return head :unprocessable_entity if request.remote_ip.blank?
return head :forbidden if user.banned?
attach_ip_address!(user)
+3 -7
View File
@@ -4,11 +4,7 @@ class IpAddress < ApplicationRecord
has_many :user_ips, dependent: :destroy
has_many :users, through: :user_ips
def banned? = banned_at?
def banned = banned?
def banned= value
bool = ActiveModel::Type::Boolean.new.cast(value)
self.banned_at = bool ? banned_at || Time.current : nil
end
def banned? = banned_at.present?
def ban! = banned? || update!(banned_at: Time.current)
def unban! = update!(banned_at: nil)
end
+2
View File
@@ -79,6 +79,8 @@ class Tag < ApplicationRecord
def material_id = materials.first&.id
def has_deerjikists = deerjikists.present?
def self.tagme = find_or_create_by_tag_name!('タグ希望', category: :meta)
def self.bot = find_or_create_by_tag_name!('bot操作', category: :meta)
def self.no_deerjikist = find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
+5 -8
View File
@@ -17,14 +17,11 @@ class User < ApplicationRecord
has_many :updated_wiki_pages,
class_name: 'WikiPage', foreign_key: :updated_user_id, dependent: :nullify
def banned? = banned_at?
def banned = banned?
def banned= value
bool = ActiveModel::Type::Boolean.new.cast(value)
self.banned_at = bool ? (banned_at || Time.current) : nil
end
def viewed?(post) = user_post_views.exists?(post_id: post.id)
def gte_member? = member? || admin?
def banned? = banned_at.present?
def ban! = banned? || update!(banned_at: Time.current)
def unban! = update!(banned_at: nil)
end
+1 -1
View File
@@ -3,7 +3,7 @@
module TagRepr
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at],
methods: [:name, :has_wiki, :material_id] }.freeze
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
module_function
+1
View File
@@ -24,6 +24,7 @@ Rails.application.routes.draw do
patch '', action: :update
get :deerjikists
put :deerjikists, action: :update_deerjikists
end
end
@@ -0,0 +1,16 @@
class RenameBannedToBannedAtInUsersAndIpAddresses < ActiveRecord::Migration[8.0]
def up
[:users, :ip_addresses].each do
add_column _1, :banned_at, :datetime, after: :banned
add_index _1, :banned_at
remove_column _1, :banned
end
end
def down
[:ip_addresses, :users].each do
add_column _1, :banned, :boolean, null: false, default: false, after: :banned_at
remove_column _1, :banned_at
end
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
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
+227 -17
View File
@@ -8,23 +8,35 @@ RSpec.describe 'Tags deerjikists API', type: :request do
let!(:tag) { create(:tag, category: :deerjikist) }
let(:member) { create(:user, :member) }
let(:guest) { create(:user, role: :guest) }
before do
# show_by_name / deerjikists_by_name 用に名前を固定
tag.tag_name.update!(name: 'deerjika')
end
describe 'GET /tags/:id/deerjikists' do
subject(:do_request) do
get "/tags/#{ tag_id }/deerjikists"
get "/tags/#{tag_id}/deerjikists"
end
let(:tag_id) { tag.id }
context 'when tag exists and has no deerjikists' do
it 'returns 200 and empty array' do
it 'returns 200 with tag and empty deerjikists array' do
do_request
expect(response).to have_http_status(:ok)
expect(json).to eq([])
expect(json).to be_a(Hash)
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(false)
expect(json['deerjikists']).to eq([])
end
end
@@ -34,17 +46,27 @@ RSpec.describe 'Tags deerjikists API', type: :request do
Deerjikist.create!(platform: platform2, code: code2, tag: tag)
end
it 'returns 200 and deerjikists array' do
it 'returns 200 with tag and deerjikists array' do
do_request
expect(response).to have_http_status(:ok)
expect(json).to be_a(Array)
expect(json.size).to eq(2)
expect(json).to be_a(Hash)
expect(json.map { |h| [h['platform'], h['code']] }).to contain_exactly(
[platform1, code1],
[platform2, code2],
)
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(true)
expect(json['deerjikists']).to be_a(Array)
expect(json['deerjikists'].size).to eq(2)
expect(json['deerjikists'].map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end
@@ -53,6 +75,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end
@@ -60,7 +83,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
describe 'GET /tags/name/:name/deerjikists' do
subject(:do_request) do
get "/tags/name/#{ name }/deerjikists"
get "/tags/name/#{name}/deerjikists"
end
let(:name) { 'deerjika' }
@@ -70,6 +93,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
it 'returns 400' do
do_request
expect(response).to have_http_status(:bad_request)
end
end
@@ -79,23 +103,209 @@ RSpec.describe 'Tags deerjikists API', type: :request do
it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end
context 'when tag exists and has no deerjikists' do
it 'returns 200 with tag and empty deerjikists array' do
do_request
expect(response).to have_http_status(:ok)
expect(json).to be_a(Hash)
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(false)
expect(json['deerjikists']).to eq([])
end
end
context 'when tag exists and has deerjikists' do
before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end
it 'returns 200 and deerjikists array' do
it 'returns 200 with tag and deerjikists array' do
do_request
expect(response).to have_http_status(:ok)
expect(json).to be_a(Array)
expect(json.size).to eq(1)
expect(json[0]['platform']).to eq(platform1)
expect(json[0]['code']).to eq(code1)
expect(json).to be_a(Hash)
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag.id)
expect(json['tag']['name']).to eq('deerjika')
expect(json['tag']['category']).to eq('deerjikist')
expect(json['tag']['has_deerjikists']).to eq(true)
expect(json['deerjikists']).to be_a(Array)
expect(json['deerjikists'].size).to eq(1)
expect(json['deerjikists'][0]['platform']).to eq(platform1)
expect(json['deerjikists'][0]['code']).to eq(code1)
end
end
end
describe 'PUT /tags/:id/deerjikists' do
subject(:do_request) do
put "/tags/#{tag_id}/deerjikists", params: payload, as: :json
end
let(:tag_id) { tag.id }
let(:payload) do
[
{ platform: platform1, code: code1 },
{ platform: platform2, code: code2 },
]
end
context 'when not logged in' do
it 'returns 401' do
do_request
expect(response).to have_http_status(:unauthorized)
end
end
context 'when logged in but not member' do
before do
sign_in_as guest
end
it 'returns 403' do
do_request
expect(response).to have_http_status(:forbidden)
end
end
context 'when tag does not exist' do
let(:tag_id) { 9_999_999 }
before do
sign_in_as member
end
it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end
context 'when logged in as member' do
before do
sign_in_as member
end
context 'when tag has no deerjikists' do
it 'creates deerjikists and returns deerjikists array' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(0).to(2)
expect(response).to have_http_status(:ok)
expect(json).to be_a(Array)
expect(json.map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end
context 'when tag already has deerjikists' do
before do
Deerjikist.create!(platform: platform1, code: 'old-code-1', tag: tag)
Deerjikist.create!(platform: platform2, code: 'old-code-2', tag: tag)
end
it 'replaces deerjikists and returns deerjikists array' do
do_request
expect(response).to have_http_status(:ok)
expect(Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
expect(Deerjikist.exists?(platform: platform1, code: 'old-code-1')).to eq(false)
expect(Deerjikist.exists?(platform: platform2, code: 'old-code-2')).to eq(false)
expect(json).to be_a(Array)
expect(json.map { |h| [h['platform'], h['code']] })
.to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end
context 'when payload is empty array' do
let(:payload) { [] }
before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
Deerjikist.create!(platform: platform2, code: code2, tag: tag)
end
it 'clears deerjikists and returns empty array' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(2).to(0)
expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
end
context 'when youtube code is handle' do
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
let(:payload) do
[
{ platform: 'youtube', code: '@deerjika' },
]
end
before do
allow(Net::HTTP).to receive(:get).and_return(
%(<link rel="canonical" href="https://www.youtube.com/channel/#{channel_id}">),
)
end
it 'normalises youtube handle to channel id' do
expect {
do_request
}.to change { Deerjikist.where(tag: tag).count }.from(0).to(1)
expect(response).to have_http_status(:ok)
expect(Net::HTTP).to have_received(:get)
expect(Deerjikist.exists?(platform: 'youtube', code: channel_id, tag: tag))
.to eq(true)
expect(json).to be_a(Array)
expect(json.size).to eq(1)
expect(json[0]['platform']).to eq('youtube')
expect(json[0]['code']).to eq(channel_id)
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(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')
# request.remote_ip を固定
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("203.0.113.10")
IpAddress.create!(
ip_address: IPAddr.new(remote_ip).hton,
banned_at: Time.current
)
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)
.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
+2 -4
View File
@@ -2,14 +2,12 @@ module TestRecords
def create_member_user!
User.create!(name: 'spec user',
inheritance_code: SecureRandom.hex(16),
role: 'member',
banned_at: nil)
role: 'member')
end
def create_admin_user!
User.create!(name: 'spec admin',
inheritance_code: SecureRandom.hex(16),
role: 'admin',
banned_at: nil)
role: 'admin')
end
end