diff --git a/AGENTS.md b/AGENTS.md index e4704c2..7ab89a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,12 +84,14 @@ cd frontend npm run dev npm run build npm run lint +npm run test +npm run test:run npm run preview ``` `npm run build` runs `tsc -b && vite build`, then `postbuild` runs `node scripts/generate-sitemap.js`. -Do not write or report `npm test` as a repository command unless a `test` script is added to `frontend/package.json`. +`npm run test` runs Vitest in watch mode. Use `npm run test:run` for a non-watch frontend test run. ## Coding style @@ -164,7 +166,7 @@ function PostFormTagsArea ({ tags, setTags }: Props) { - Keep changes scoped to the requested issue. - Do not scan or summarize dependency/generated/runtime directories such as `node_modules`, `dist`, `tmp`, `log`, and `storage` unless explicitly needed. - Before touching wiki, tag, versioning, BAN, IP BAN, or authentication behavior, inspect the related request specs and service objects. -- If frontend code changes, run the existing frontend verification commands that apply: `npm run build` and `npm run lint`. +- If frontend code changes, run the existing frontend verification commands that apply: `npm run build`, `npm run lint`, and `npm run test:run`. - If backend code changes, run the relevant RSpec command; for broad backend changes, run `bundle exec rspec`. - If a verification command cannot be run or fails, report the exact command and failure. diff --git a/backend/app/controllers/application_controller.rb b/backend/app/controllers/application_controller.rb index 39a4465..42adc75 100644 --- a/backend/app/controllers/application_controller.rb +++ b/backend/app/controllers/application_controller.rb @@ -28,19 +28,17 @@ class ApplicationController < ActionController::API end end - def render_bad_request message = 'リクエストが不正です.', field: nil, code: :bad_request - render_error(:bad_request, message, field:, code:) + def render_bad_request message = 'リクエストが不正です.' + render json: { type: 'bad_request', + message:, + errors: { }, + base_errors: [message] }, + status: :bad_request end - def render_unprocessable_entity message = '入力を確認してください.', field: nil, code: :invalid - render_error(:unprocessable_entity, message, field:, code:) - end - - def render_error status, message, field: nil, code: status - error = { code: code.to_s, message: } - error[:field] = field.to_s if field.present? - - render json: { errors: [error] }, status: + def render_unprocessable_entity message = '入力を確認してください.', field: nil + render_validation_error(fields: field ? { field => [message] } : { }, + base: field ? [] : [message]) end def render_record_invalid error diff --git a/backend/app/controllers/deerjikists_controller.rb b/backend/app/controllers/deerjikists_controller.rb index 7a9cb75..9302370 100644 --- a/backend/app/controllers/deerjikists_controller.rb +++ b/backend/app/controllers/deerjikists_controller.rb @@ -2,8 +2,8 @@ class DeerjikistsController < ApplicationController def show platform = params[:platform].to_s.strip code = params[:code].to_s.strip - return render_bad_request('platform は必須です.', field: :platform) if platform.blank? - return render_bad_request('code は必須です.', field: :code) if code.blank? + return render_bad_request('platform は必須です.') if platform.blank? + return render_bad_request('code は必須です.') if code.blank? deerjikist = Deerjikist .joins(:tag) @@ -23,9 +23,9 @@ class DeerjikistsController < ApplicationController platform = params[:platform].to_s.strip code = params[:code].to_s.strip tag_id = params[:tag_id].to_i - return render_bad_request('platform は必須です.', field: :platform) if platform.blank? - return render_bad_request('code は必須です.', field: :code) if code.blank? - return render_bad_request('tag_id が不正です.', field: :tag_id) if tag_id <= 0 + return render_bad_request('platform は必須です.') if platform.blank? + return render_bad_request('code は必須です.') if code.blank? + return render_bad_request('tag_id が不正です.') if tag_id <= 0 deerjikist = Deerjikist.find_or_initialize_by(platform:, code:).tap do |d| d.tag_id = tag_id @@ -41,8 +41,8 @@ class DeerjikistsController < ApplicationController platform = params[:platform].to_s.strip code = params[:code].to_s.strip - return render_bad_request('platform は必須です.', field: :platform) if platform.blank? - return render_bad_request('code は必須です.', field: :code) if code.blank? + return render_bad_request('platform は必須です.') if platform.blank? + return render_bad_request('code は必須です.') if code.blank? Deerjikist.find([platform, code]).destroy! diff --git a/backend/app/controllers/materials_controller.rb b/backend/app/controllers/materials_controller.rb index 0fc5a9c..2a86b77 100644 --- a/backend/app/controllers/materials_controller.rb +++ b/backend/app/controllers/materials_controller.rb @@ -40,8 +40,11 @@ class MaterialsController < ApplicationController tag_name_raw = params[:tag].to_s.strip file = params[:file] url = params[:url].to_s.strip.presence - return render_bad_request('タグは必須です.', field: :tag) if tag_name_raw.blank? - return render_bad_request('ファイルまたは URL は必須です.') if file.blank? && url.blank? + return render_unprocessable_entity('タグは必須です.', field: :tag) if tag_name_raw.blank? + if file.blank? && url.blank? + return render_validation_error fields: { file: ['ファイルまたは URL は必須です.'], + url: ['ファイルまたは URL は必須です.'] } + end tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw) tag = tag_name.tag @@ -69,8 +72,11 @@ class MaterialsController < ApplicationController tag_name_raw = params[:tag].to_s.strip file = params[:file] url = params[:url].to_s.strip.presence - return render_bad_request('タグは必須です.', field: :tag) if tag_name_raw.blank? - return render_bad_request('ファイルまたは URL は必須です.') if file.blank? && url.blank? + return render_unprocessable_entity('タグは必須です.', field: :tag) if tag_name_raw.blank? + if file.blank? && url.blank? + return render_validation_error fields: { file: ['ファイルまたは URL は必須です.'], + url: ['ファイルまたは URL は必須です.'] } + end tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw) tag = tag_name.tag diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index 55c54a4..002b66f 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -30,13 +30,13 @@ class NicoTagsController < ApplicationController id = params[:id].to_i tag = Tag.find(id) - return render_bad_request('ニコニコ・タグを指定してください.', field: :id) unless tag.nico? + return render_bad_request('ニコニコ・タグを指定してください.') unless tag.nico? linked_tag_names = params[:tags].to_s.split linked_tags = Tag.normalise_tags!(linked_tag_names, with_tagme: false, with_no_deerjikist: false) if linked_tags.any? { |t| t.nico? } - return render_bad_request('ニコニコ・タグ同士は連携できません.', field: :tags) + return render_unprocessable_entity('ニコニコ・タグ同士は連携できません.', field: :tags) end ApplicationRecord.transaction do diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index dedfe51..976578d 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -192,7 +192,7 @@ class PostsController < ApplicationController return render_bad_request('force と merge は同時に指定できません.') if force && merge base_version_no = parse_base_version_no - return render_bad_request('base_version_no は必須です.', field: :base_version_no) if !(force) && !(base_version_no) + return render_bad_request('base_version_no は必須です.') if !(force) && !(base_version_no) title = params[:title].presence tag_names = params[:tags].to_s.split @@ -671,9 +671,9 @@ class PostsController < ApplicationController end def render_post_form_record_invalid record - if e.record.is_a?(TagName) || e.record.is_a?(Tag) - render_validation_error fields: { tags: e.record.errors.full_messages.map { |message| - "タグ名 “#{ e.record.name }”: #{ message }" + if record.is_a?(TagName) || record.is_a?(Tag) + render_validation_error fields: { tags: record.errors.full_messages.map { |message| + "タグ名 “#{ record.name }”: #{ message }" } } else render_validation_error record diff --git a/backend/app/controllers/preview_controller.rb b/backend/app/controllers/preview_controller.rb index 24d2a2d..5486838 100644 --- a/backend/app/controllers/preview_controller.rb +++ b/backend/app/controllers/preview_controller.rb @@ -4,7 +4,7 @@ class PreviewController < ApplicationController return head :unauthorized unless current_user url = params[:url] - return render_bad_request('URL は必須です.', field: :url) unless url.present? + return render_bad_request('URL は必須です.') unless url.present? unless url.start_with?(/http(s)?:\/\//) url = 'http://' + url @@ -16,7 +16,7 @@ class PreviewController < ApplicationController render json: { title: title } rescue => e - render_bad_request(e.message, field: :url) + render_bad_request(e.message) end def thumbnail @@ -25,7 +25,7 @@ class PreviewController < ApplicationController return head :unauthorized unless current_user url = params[:url] - return render_bad_request('URL は必須です.', field: :url) if url.blank? + return render_bad_request('URL は必須です.') if url.blank? unless url.start_with?(/http(s)?:\/\//) url = 'http://' + url @@ -40,8 +40,11 @@ class PreviewController < ApplicationController File.delete(path) rescue nil send_file image.path, type: 'image/png', disposition: 'inline' else - render_error(:internal_server_error, 'サムネールを生成できませんでした.', - code: :thumbnail_generation_failed) + render json: { type: 'internal_server_error', + message: 'サムネールを生成できませんでした.', + errors: { }, + base_errors: ['サムネールを生成できませんでした.'] }, + status: :internal_server_error end end end diff --git a/backend/app/controllers/tag_children_controller.rb b/backend/app/controllers/tag_children_controller.rb index ee4300f..cfdde30 100644 --- a/backend/app/controllers/tag_children_controller.rb +++ b/backend/app/controllers/tag_children_controller.rb @@ -5,8 +5,8 @@ class TagChildrenController < ApplicationController parent_id = params[:parent_id] child_id = params[:child_id] - return render_bad_request('parent_id は必須です.', field: :parent_id) if parent_id.blank? - return render_bad_request('child_id は必須です.', field: :child_id) if child_id.blank? + return render_bad_request('parent_id は必須です.') if parent_id.blank? + return render_bad_request('child_id は必須です.') if child_id.blank? parent = Tag.find(parent_id) child = Tag.find(child_id) @@ -28,8 +28,8 @@ class TagChildrenController < ApplicationController parent_id = params[:parent_id] child_id = params[:child_id] - return render_bad_request('parent_id は必須です.', field: :parent_id) if parent_id.blank? - return render_bad_request('child_id は必須です.', field: :child_id) if child_id.blank? + return render_bad_request('parent_id は必須です.') if parent_id.blank? + return render_bad_request('child_id は必須です.') if child_id.blank? parent = Tag.find(parent_id) child = Tag.find(child_id) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 89911de..30d822b 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -168,7 +168,7 @@ class TagsController < ApplicationController def show_by_name name = params[:name].to_s.strip - return render_bad_request('name は必須です.', field: :name) if name.blank? + return render_bad_request('name は必須です.') if name.blank? tag = Tag.joins(:tag_name) .includes(:tag_name, :materials, tag_name: :wiki_page) @@ -192,7 +192,7 @@ class TagsController < ApplicationController def deerjikists_by_name name = params[:name].to_s.strip - return render_bad_request('name は必須です.', field: :name) if name.blank? + return render_bad_request('name は必須です.') if name.blank? tag = Tag.joins(:tag_name) .includes(:tag_name, tag_name: :wiki_page) @@ -214,21 +214,24 @@ class TagsController < ApplicationController ApplicationRecord.transaction do tag.deerjikists = [] - params[:_json].each do - platform = _1[:platform] - code = normalise_deerjikist_code(platform, _1[:code]) + params[:_json].each.with_index do |item, i| + platform = item[:platform] + code = normalise_deerjikist_code(platform, item[:code]) deerjikist = Deerjikist.find_or_initialize_by(platform:, code:) deerjikist.tag = tag - deerjikist.save! + render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save + raise ActiveRecord::Rollback if performed? end end + return if performed? + render json: DeerjikistRepr.many(tag.reload.deerjikists) end def materials_by_name name = params[:name].to_s.strip - return render_bad_request('name は必須です.', field: :name) if name.blank? + return render_bad_request('name は必須です.') if name.blank? tag = Tag.joins(:tag_name) .includes(:tag_name, :materials, tag_name: :wiki_page) @@ -435,4 +438,23 @@ class TagsController < ApplicationController rescue nil end + + def render_deerjikist_form_record_invalid deerjikist, index + fields = { } + + deerjikist.errors.each do |error| + field = + case error.attribute + when :platform, :code + "deerjikists.#{ index }.#{ error.attribute }" + else + :deerjikists + end + + fields[field] ||= [] + fields[field] << error.full_message + end + + render_validation_error fields: + end end diff --git a/backend/app/controllers/users_controller.rb b/backend/app/controllers/users_controller.rb index 1a6bc75..95ae677 100644 --- a/backend/app/controllers/users_controller.rb +++ b/backend/app/controllers/users_controller.rb @@ -42,12 +42,12 @@ class UsersController < ApplicationController return head :unauthorized if user&.id != params[:id].to_i name = params[:name] - return render_bad_request('名前は必須です.', field: :name) if name.blank? + return render_unprocessable_entity('名前は必須です.', field: :name) if name.blank? if user.update(name:) render json: user.slice(:id, :name, :inheritance_code, :role), status: :ok else - render_validation_errors user + render_validation_error user end end diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index ebf1c13..818ae02 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -46,7 +46,7 @@ class WikiPagesController < ApplicationController def diff id = params[:id] - return render_bad_request('id は必須です.', field: :id) if id.blank? + return render_bad_request('id は必須です.') if id.blank? from = params[:from].presence to = params[:to].presence @@ -103,7 +103,7 @@ class WikiPagesController < ApplicationController render json: WikiPageRepr.base(page), status: :created rescue ActiveRecord::RecordInvalid => e - render_validation_errors e.record + render_validation_error e.record rescue ActiveRecord::RecordNotUnique render_record_not_unique end