Reviewed-on: #397 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #397 でマージされました.
このコミットが含まれているのは:
@@ -0,0 +1,61 @@
|
||||
class UserSettingsController < ApplicationController
|
||||
wrap_parameters false
|
||||
|
||||
def show
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
render json: current_setting.serializable_hash
|
||||
end
|
||||
|
||||
def update
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
raw_attributes = editable_raw_attributes
|
||||
field_errors = validate_raw_attributes(raw_attributes)
|
||||
return render_validation_error fields: field_errors if field_errors.present?
|
||||
|
||||
setting = current_setting
|
||||
setting.assign_attributes(raw_attributes)
|
||||
|
||||
if setting.save
|
||||
render json: setting.serializable_hash, status: :ok
|
||||
else
|
||||
render_validation_error setting
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_setting
|
||||
Setting.find_or_create_by!(user: current_user) do |setting|
|
||||
setting.assign_attributes(Setting.defaults)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
Setting.find_by!(user: current_user)
|
||||
end
|
||||
|
||||
def editable_raw_attributes
|
||||
request.request_parameters.slice(*Setting::EDITABLE_ATTRIBUTES)
|
||||
end
|
||||
|
||||
def validate_raw_attributes raw_attributes
|
||||
raw_attributes.each_with_object({ }) do |(key, value), errors|
|
||||
next if value_matches_type?(key, value)
|
||||
|
||||
errors[key.to_sym] = ['値の型が不正です.']
|
||||
end
|
||||
end
|
||||
|
||||
def value_matches_type? key, value
|
||||
case Setting::TYPE_BY_ATTRIBUTE.fetch(key)
|
||||
when :string
|
||||
value.is_a?(String)
|
||||
when :integer
|
||||
value.is_a?(Integer)
|
||||
when :boolean
|
||||
value == true || value == false
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
class UserThemeSlotsController < ApplicationController
|
||||
wrap_parameters false
|
||||
|
||||
def index
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
render json: current_user
|
||||
.theme_slots
|
||||
.order(:base_theme, :slot_no)
|
||||
.map { |slot| slot.serializable_hash }
|
||||
end
|
||||
|
||||
def update
|
||||
return head :unauthorized unless current_user
|
||||
|
||||
base_theme = params[:base_theme].to_s
|
||||
slot_no = params[:slot_no].to_i
|
||||
tokens = params[:tokens]
|
||||
|
||||
unless UserThemeSlot::BASE_THEMES.include?(base_theme)
|
||||
return render_validation_error fields: { base_theme: ['値が不正です.'] }
|
||||
end
|
||||
unless UserThemeSlot::SLOT_NOS.include?(slot_no)
|
||||
return render_validation_error fields: { slot_no: ['値が不正です.'] }
|
||||
end
|
||||
unless tokens.is_a?(ActionController::Parameters) || tokens.is_a?(Hash)
|
||||
return render_validation_error fields: { tokens: ['JSON object で指定してください.'] }
|
||||
end
|
||||
|
||||
slot = UserThemeSlot.find_or_initialize_by(user: current_user,
|
||||
base_theme:,
|
||||
slot_no:)
|
||||
slot.tokens = tokens.is_a?(ActionController::Parameters) ? tokens.to_unsafe_h : tokens
|
||||
slot.save!
|
||||
|
||||
render json: slot.serializable_hash, status: :ok
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,52 @@
|
||||
class Setting < ApplicationRecord
|
||||
THEMES = ['system', 'light', 'dark'].freeze
|
||||
|
||||
# These remain in the typed settings schema to preserve existing backend
|
||||
# work. The common `/users/settings` page currently surfaces only `theme`.
|
||||
AUTO_FETCH_MODES = ['auto', 'manual', 'off'].freeze
|
||||
WIKI_EDITOR_MODES = ['split', 'write', 'preview'].freeze
|
||||
|
||||
STRING_ATTRIBUTES = [
|
||||
'theme',
|
||||
'auto_fetch_title',
|
||||
'auto_fetch_thumbnail',
|
||||
'wiki_editor_mode',
|
||||
].freeze
|
||||
INTEGER_ATTRIBUTES = [].freeze
|
||||
BOOLEAN_ATTRIBUTES = [].freeze
|
||||
EDITABLE_ATTRIBUTES =
|
||||
(STRING_ATTRIBUTES + INTEGER_ATTRIBUTES + BOOLEAN_ATTRIBUTES).freeze
|
||||
TYPE_BY_ATTRIBUTE = {
|
||||
'theme' => :string,
|
||||
'auto_fetch_title' => :string,
|
||||
'auto_fetch_thumbnail' => :string,
|
||||
'wiki_editor_mode' => :string,
|
||||
}.freeze
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :key, presence: true, length: { maximum: 255 }
|
||||
validates :value, presence: true
|
||||
validates :user_id, uniqueness: true
|
||||
|
||||
validates :theme, inclusion: { in: THEMES }
|
||||
validates :auto_fetch_title, inclusion: { in: AUTO_FETCH_MODES }
|
||||
validates :auto_fetch_thumbnail, inclusion: { in: AUTO_FETCH_MODES }
|
||||
validates :wiki_editor_mode, inclusion: { in: WIKI_EDITOR_MODES }
|
||||
|
||||
def self.defaults
|
||||
{
|
||||
theme: 'system',
|
||||
auto_fetch_title: 'manual',
|
||||
auto_fetch_thumbnail: 'manual',
|
||||
wiki_editor_mode: 'split',
|
||||
}
|
||||
end
|
||||
|
||||
def self.serializable_attributes
|
||||
EDITABLE_ATTRIBUTES.map(&:to_sym)
|
||||
end
|
||||
|
||||
def serializable_hash(options = nil)
|
||||
super({ only: self.class.serializable_attributes }.merge(options || { }))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :created_posts,
|
||||
class_name: 'Post', foreign_key: :uploaded_user_id, dependent: :nullify
|
||||
has_many :settings
|
||||
has_one :setting, dependent: :destroy
|
||||
has_many :theme_slots, class_name: 'UserThemeSlot', dependent: :destroy
|
||||
has_many :user_ips, dependent: :destroy
|
||||
has_many :ip_addresses, through: :user_ips
|
||||
has_many :user_post_views, dependent: :destroy
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class UserThemeSlot < ApplicationRecord
|
||||
BASE_THEMES = ['light', 'dark'].freeze
|
||||
SLOT_NOS = [1, 2, 3].freeze
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :base_theme, presence: true, inclusion: { in: BASE_THEMES }
|
||||
validates :slot_no, presence: true, inclusion: { in: SLOT_NOS }
|
||||
validates :tokens, presence: true
|
||||
validates :user_id, uniqueness: { scope: [:base_theme, :slot_no] }
|
||||
|
||||
validate :tokens_must_be_object
|
||||
|
||||
def serializable_hash(options = nil)
|
||||
hash = { only: [:base_theme, :slot_no, :tokens, :created_at, :updated_at] }
|
||||
super(hash.merge(options || {}))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokens_must_be_object
|
||||
return if tokens.is_a?(Hash)
|
||||
|
||||
errors.add(:tokens, 'は JSON object で指定してください.')
|
||||
end
|
||||
end
|
||||
@@ -81,6 +81,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
get 'users/settings', to: 'user_settings#show'
|
||||
patch 'users/settings', to: 'user_settings#update'
|
||||
get 'users/theme_slots', to: 'user_theme_slots#index'
|
||||
put 'users/theme_slots/:base_theme/:slot_no', to: 'user_theme_slots#update'
|
||||
|
||||
resources :users, only: [:create, :update] do
|
||||
collection do
|
||||
post :verify
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
class RebuildSettingsAsTypedUserSettings < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_foreign_key :settings, :users if foreign_key_exists?(:settings, :users)
|
||||
remove_index :settings, :user_id if index_exists?(:settings, :user_id)
|
||||
|
||||
remove_column :settings, :key, :string if column_exists?(:settings, :key)
|
||||
remove_column :settings, :value, :json if column_exists?(:settings, :value)
|
||||
|
||||
change_column_null :settings, :user_id, false
|
||||
|
||||
add_column :settings, :theme, :string, null: false, default: 'system'
|
||||
add_column :settings,
|
||||
:auto_fetch_title,
|
||||
:string,
|
||||
null: false,
|
||||
default: 'manual'
|
||||
add_column :settings,
|
||||
:auto_fetch_thumbnail,
|
||||
:string,
|
||||
null: false,
|
||||
default: 'manual'
|
||||
add_column :settings,
|
||||
:wiki_editor_mode,
|
||||
:string,
|
||||
null: false,
|
||||
default: 'split'
|
||||
|
||||
add_index :settings, :user_id, unique: true
|
||||
add_foreign_key :settings, :users
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
class CreateUserThemeSlots < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :user_theme_slots do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.string :base_theme, null: false
|
||||
t.integer :slot_no, null: false
|
||||
t.json :tokens, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :user_theme_slots,
|
||||
[:user_id, :base_theme, :slot_no],
|
||||
unique: true,
|
||||
name: 'index_user_theme_slots_on_user_theme_and_slot'
|
||||
add_check_constraint :user_theme_slots,
|
||||
"base_theme IN ('light', 'dark')",
|
||||
name: 'user_theme_slots_base_theme_valid'
|
||||
add_check_constraint :user_theme_slots,
|
||||
'slot_no BETWEEN 1 AND 3',
|
||||
name: 'user_theme_slots_slot_no_valid'
|
||||
end
|
||||
end
|
||||
生成ファイル
+35
-19
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_07_05_000000) do
|
||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -374,11 +374,26 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
|
||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "key", null: false
|
||||
t.json "value", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id"], name: "index_settings_on_user_id"
|
||||
t.string "theme", default: "system", null: false
|
||||
t.string "auto_fetch_title", default: "manual", null: false
|
||||
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
||||
t.string "wiki_editor_mode", default: "split", null: false
|
||||
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
||||
end
|
||||
|
||||
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "wiki_page_id", null: false
|
||||
t.integer "no", null: false
|
||||
t.string "alt_text"
|
||||
t.binary "sha256", limit: 32, null: false
|
||||
t.bigint "created_by_user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
|
||||
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
|
||||
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
|
||||
end
|
||||
|
||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
@@ -426,9 +441,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
t.string "event_type", null: false
|
||||
t.string "name", null: false
|
||||
t.string "category", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.text "aliases", null: false
|
||||
t.text "parent_tag_ids", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "created_by_user_id"
|
||||
t.index ["created_at"], name: "index_tag_versions_on_created_at"
|
||||
@@ -441,10 +456,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "tag_name_id", null: false
|
||||
t.string "category", default: "general", null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "post_count", default: 0, null: false
|
||||
t.datetime "deprecated_at"
|
||||
t.datetime "discarded_at"
|
||||
t.integer "version_no", null: false
|
||||
t.index ["deprecated_at"], name: "index_tags_on_deprecated_at"
|
||||
@@ -566,6 +581,19 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
t.index ["post_id"], name: "index_user_post_views_on_post_id"
|
||||
end
|
||||
|
||||
create_table "user_theme_slots", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "base_theme", null: false
|
||||
t.integer "slot_no", null: false
|
||||
t.json "tokens", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id", "base_theme", "slot_no"], name: "index_user_theme_slots_on_user_theme_and_slot", unique: true
|
||||
t.index ["user_id"], name: "index_user_theme_slots_on_user_id"
|
||||
t.check_constraint "`base_theme` in (_utf8mb4'light',_utf8mb4'dark')", name: "user_theme_slots_base_theme_valid"
|
||||
t.check_constraint "`slot_no` between 1 and 3", name: "user_theme_slots_slot_no_valid"
|
||||
end
|
||||
|
||||
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "inheritance_code", limit: 64, null: false
|
||||
@@ -576,19 +604,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
t.index ["banned_at"], name: "index_users_on_banned_at"
|
||||
end
|
||||
|
||||
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "wiki_page_id", null: false
|
||||
t.integer "no", null: false
|
||||
t.string "alt_text"
|
||||
t.binary "sha256", limit: 32, null: false
|
||||
t.bigint "created_by_user_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
|
||||
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
|
||||
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
|
||||
end
|
||||
|
||||
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "sha256", limit: 64, null: false
|
||||
t.text "body", null: false
|
||||
@@ -738,6 +753,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_26_010000) do
|
||||
add_foreign_key "user_ips", "users"
|
||||
add_foreign_key "user_post_views", "posts"
|
||||
add_foreign_key "user_post_views", "users"
|
||||
add_foreign_key "user_theme_slots", "users"
|
||||
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
|
||||
add_foreign_key "wiki_assets", "wiki_pages"
|
||||
add_foreign_key "wiki_pages", "tag_names"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Setting, type: :model do
|
||||
it 'accepts the default typed user settings' do
|
||||
setting = described_class.new({ user: create(:user) }.merge(described_class.defaults))
|
||||
|
||||
expect(setting).to be_valid
|
||||
end
|
||||
|
||||
it 'requires one settings row per user' do
|
||||
user = create(:user)
|
||||
described_class.create!({ user: }.merge(described_class.defaults))
|
||||
|
||||
duplicate = described_class.new({ user: }.merge(described_class.defaults))
|
||||
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:user_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates enum-like settings columns' do
|
||||
setting = described_class.new(
|
||||
{ user: create(:user),
|
||||
theme: 'neon',
|
||||
auto_fetch_title: 'sometimes',
|
||||
auto_fetch_thumbnail: 'sometimes',
|
||||
wiki_editor_mode: 'sideways' })
|
||||
|
||||
expect(setting).not_to be_valid
|
||||
expect(setting.errors[:theme]).to be_present
|
||||
expect(setting.errors[:auto_fetch_title]).to be_present
|
||||
expect(setting.errors[:auto_fetch_thumbnail]).to be_present
|
||||
expect(setting.errors[:wiki_editor_mode]).to be_present
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UserThemeSlot, type: :model do
|
||||
it 'accepts a theme slot for one user, base theme, and slot number' do
|
||||
slot = described_class.new(user: create(:user),
|
||||
base_theme: 'light',
|
||||
slot_no: 1,
|
||||
tokens: { 'background' => '0 0% 100%' })
|
||||
|
||||
expect(slot).to be_valid
|
||||
end
|
||||
|
||||
it 'requires unique slot numbers per user and base theme' do
|
||||
user = create(:user)
|
||||
described_class.create!(user:,
|
||||
base_theme: 'dark',
|
||||
slot_no: 2,
|
||||
tokens: { 'background' => '222.2 84% 4.9%' })
|
||||
|
||||
duplicate = described_class.new(user:,
|
||||
base_theme: 'dark',
|
||||
slot_no: 2,
|
||||
tokens: { 'background' => '0 0% 100%' })
|
||||
|
||||
expect(duplicate).not_to be_valid
|
||||
expect(duplicate.errors[:user_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates base theme, slot number, and token object shape' do
|
||||
slot = described_class.new(user: create(:user),
|
||||
base_theme: 'system',
|
||||
slot_no: 4,
|
||||
tokens: 'not-object')
|
||||
|
||||
expect(slot).not_to be_valid
|
||||
expect(slot.errors[:base_theme]).to be_present
|
||||
expect(slot.errors[:slot_no]).to be_present
|
||||
expect(slot.errors[:tokens]).to be_present
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'user settings', type: :request do
|
||||
describe 'GET /users/settings' do
|
||||
it 'requires a current user' do
|
||||
sign_out
|
||||
|
||||
get '/users/settings'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns defaults and creates a missing settings row' do
|
||||
user = create(:user)
|
||||
sign_in_as(user)
|
||||
|
||||
expect {
|
||||
get '/users/settings'
|
||||
}.to change(Setting, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to eq(
|
||||
'theme' => 'system',
|
||||
'auto_fetch_title' => 'manual',
|
||||
'auto_fetch_thumbnail' => 'manual',
|
||||
'wiki_editor_mode' => 'split')
|
||||
expect(user.reload.setting).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /users/settings' do
|
||||
it 'requires a current user' do
|
||||
sign_out
|
||||
|
||||
patch '/users/settings', params: { theme: 'dark' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'updates multiple typed settings at once' do
|
||||
user = create(:user)
|
||||
sign_in_as(user)
|
||||
|
||||
patch '/users/settings',
|
||||
params: {
|
||||
theme: 'dark',
|
||||
auto_fetch_title: 'auto',
|
||||
auto_fetch_thumbnail: 'off',
|
||||
wiki_editor_mode: 'preview' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include(
|
||||
'theme' => 'dark',
|
||||
'auto_fetch_title' => 'auto',
|
||||
'auto_fetch_thumbnail' => 'off',
|
||||
'wiki_editor_mode' => 'preview')
|
||||
expect(user.reload.setting).to have_attributes(
|
||||
theme: 'dark',
|
||||
auto_fetch_title: 'auto',
|
||||
auto_fetch_thumbnail: 'off',
|
||||
wiki_editor_mode: 'preview')
|
||||
end
|
||||
|
||||
it 'does not treat Rails parameter wrapping as an editable setting' do
|
||||
user = create(:user)
|
||||
sign_in_as(user)
|
||||
|
||||
patch '/users/settings',
|
||||
params: { theme: 'light', user_setting: { theme: 'dark' } },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(user.reload.setting.theme).to eq('light')
|
||||
end
|
||||
|
||||
it 'returns validation_error for type mismatches' do
|
||||
sign_in_as(create(:user))
|
||||
|
||||
patch '/users/settings', params: { theme: 1 }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json).to include('type' => 'validation_error')
|
||||
expect(json.fetch('errors')).to include(
|
||||
'theme' => ['値の型が不正です.'])
|
||||
end
|
||||
|
||||
it 'returns validation_error for invalid enum values' do
|
||||
sign_in_as(create(:user))
|
||||
|
||||
patch '/users/settings', params: { theme: 'neon' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json).to include('type' => 'validation_error')
|
||||
expect(json.fetch('errors').fetch('theme')).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'user theme slots', type: :request do
|
||||
describe 'GET /users/theme_slots' do
|
||||
it 'requires a current user' do
|
||||
sign_out
|
||||
|
||||
get '/users/theme_slots'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns only the current user slots in stable order' do
|
||||
user = create(:user)
|
||||
other_user = create(:user)
|
||||
sign_in_as(user)
|
||||
UserThemeSlot.create!(user:, base_theme: 'light', slot_no: 2, tokens: { 'a' => 1 })
|
||||
UserThemeSlot.create!(user:, base_theme: 'dark', slot_no: 1, tokens: { 'b' => 2 })
|
||||
UserThemeSlot.create!(user: other_user,
|
||||
base_theme: 'dark',
|
||||
slot_no: 3,
|
||||
tokens: { 'c' => 3 })
|
||||
|
||||
get '/users/theme_slots'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { |slot| [slot.fetch('base_theme'), slot.fetch('slot_no')] }).to eq(
|
||||
[['dark', 1], ['light', 2]])
|
||||
expect(json.map { |slot| slot.fetch('tokens') }).to eq(
|
||||
[{ 'b' => 2 }, { 'a' => 1 }])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /users/theme_slots/:base_theme/:slot_no' do
|
||||
it 'requires a current user' do
|
||||
sign_out
|
||||
|
||||
put '/users/theme_slots/light/1',
|
||||
params: { tokens: { background: '0 0% 100%' } },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates a theme slot' do
|
||||
user = create(:user)
|
||||
sign_in_as(user)
|
||||
|
||||
expect {
|
||||
put '/users/theme_slots/light/1',
|
||||
params: { tokens: { background: '0 0% 100%', tagColours: { nico: '#ffffff' } } },
|
||||
as: :json
|
||||
}.to change(UserThemeSlot, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include('base_theme' => 'light', 'slot_no' => 1)
|
||||
expect(json.fetch('tokens')).to include(
|
||||
'background' => '0 0% 100%',
|
||||
'tagColours' => { 'nico' => '#ffffff' })
|
||||
expect(user.theme_slots.find_by!(base_theme: 'light', slot_no: 1).tokens).to include(
|
||||
'background' => '0 0% 100%')
|
||||
end
|
||||
|
||||
it 'updates an existing theme slot instead of creating a duplicate' do
|
||||
user = create(:user)
|
||||
sign_in_as(user)
|
||||
UserThemeSlot.create!(user:, base_theme: 'dark', slot_no: 2, tokens: { 'old' => true })
|
||||
|
||||
expect {
|
||||
put '/users/theme_slots/dark/2',
|
||||
params: { tokens: { background: '222.2 84% 4.9%' } },
|
||||
as: :json
|
||||
}.not_to change(UserThemeSlot, :count)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(user.theme_slots.find_by!(base_theme: 'dark', slot_no: 2).tokens).to eq(
|
||||
'background' => '222.2 84% 4.9%')
|
||||
end
|
||||
|
||||
it 'rejects invalid path parameters and token shapes' do
|
||||
sign_in_as(create(:user))
|
||||
|
||||
put '/users/theme_slots/system/4',
|
||||
params: { tokens: 'not-object' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'base_theme' => ['値が不正です.'])
|
||||
end
|
||||
|
||||
it 'rejects non-object tokens for valid slots' do
|
||||
sign_in_as(create(:user))
|
||||
|
||||
put '/users/theme_slots/light/1',
|
||||
params: { tokens: 'not-object' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tokens' => ['JSON object で指定してください.'])
|
||||
end
|
||||
end
|
||||
end
|
||||
新しい課題から参照
ユーザをブロックする