このコミットが含まれているのは:
2026-06-08 12:44:45 +09:00
コミット 543f051f8f
12個のファイルの変更70行の追加64行の削除
+11 -4
ファイルの表示
@@ -47,6 +47,10 @@ bundle exec rspec
If a command cannot be run or fails, report the exact command and failure.
Do not create, modify, or run tests unless the user explicitly asks for test
work. When the user asks for tests, keep working and rerun them until they
pass or the remaining failure is clearly blocked.
## Rails structure
- `app/controllers`: API controllers.
@@ -116,7 +120,8 @@ service, representation, and spec.
- `User#banned?` and `IpAddress#banned?` check `banned_at.present?`.
- Do not weaken BAN or IP BAN behavior.
- If changing request authentication or controller before actions, add or
update request specs covering banned users and banned IP addresses.
update request specs covering banned users and banned IP addresses only when
the user explicitly asks for tests.
## RSpec
@@ -130,8 +135,9 @@ service, representation, and spec.
- `AuthHelper#sign_in_as(user)` stubs
`ApplicationController#current_user`; use it when matching existing
request spec style.
- Add or update request specs for API behavior changes, especially status
codes, permissions, response shape, and version conflict behavior.
- Add or update request specs for API behavior changes only when the user
explicitly asks for tests, especially status codes, permissions, response
shape, and version conflict behavior.
## Migrations
@@ -164,7 +170,8 @@ service, representation, and spec.
the record `version_no`.
- Do not update versioned records without considering whether a version snapshot must be created.
- For optimistic concurrency paths, preserve `base_version_no`, `force`, and
`merge` semantics and cover conflicts in request specs.
`merge` semantics. Cover conflicts in request specs only when the user
explicitly asks for tests.
## Domain cautions
+2 -2
ファイルの表示
@@ -1,6 +1,6 @@
class GekanatorGamesController < ApplicationController
def create
return head :unauthorized unless current_user
return head :not_found unless current_user&.admin?
guessed_post_id = params.require(:guessed_post_id)
correct_post_id = params[:correct_post_id].presence
@@ -11,7 +11,7 @@ class GekanatorGamesController < ApplicationController
guessed_post_id:,
correct_post_id:,
won: correct_post_id.present? && guessed_post_id.to_i == correct_post_id.to_i,
question_count: params.require(:question_count),
question_count: answers.length,
answers:)
if game.save
+2 -2
ファイルの表示
@@ -1,9 +1,9 @@
class CreateGekanatorGames < ActiveRecord::Migration[8.0]
def change
create_table :gekanator_games do |t|
t.references :user, foreign_key: true
t.references :user, null: false, foreign_key: true
t.references :guessed_post, null: false, foreign_key: { to_table: :posts }
t.references :correct_post, foreign_key: { to_table: :posts }
t.references :correct_post, null: false, foreign_key: { to_table: :posts }
t.boolean :won, null: false
t.integer :question_count, null: false
t.json :answers, null: false
@@ -1,17 +0,0 @@
class RequireGekanatorGameUserAndCorrectPost < ActiveRecord::Migration[8.0]
def up
execute <<~SQL.squish
UPDATE gekanator_games
SET correct_post_id = guessed_post_id
WHERE correct_post_id IS NULL
SQL
change_column_null :gekanator_games, :user_id, false
change_column_null :gekanator_games, :correct_post_id, false
end
def down
change_column_null :gekanator_games, :correct_post_id, true
change_column_null :gekanator_games, :user_id, true
end
end
生成ファイル
+1 -20
ファイルの表示
@@ -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_08_000000) do
ActiveRecord::Schema[8.0].define(version: 2026_06_07_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
@@ -152,19 +152,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do
t.index ["target_post_id"], name: "index_post_similarities_on_target_post_id"
end
create_table "post_tag_sections", primary_key: ["post_id", "tag_id", "begin_ms"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "post_id", null: false
t.bigint "tag_id", null: false
t.integer "begin_ms", null: false
t.integer "end_ms", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["post_id", "begin_ms"], name: "idx_post_tag_sections_post_id_begin_ms"
t.index ["tag_id"], name: "fk_rails_8be3847903"
t.check_constraint "`begin_ms` < `end_ms`", name: "chk_post_tag_sections_end_ms_after_begin_ms"
t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural"
end
create_table "post_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "post_id", null: false
t.bigint "tag_id", null: false
@@ -215,11 +202,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do
t.datetime "original_created_before"
t.datetime "updated_at", null: false
t.integer "version_no", null: false
t.integer "video_ms"
t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id"
t.index ["url"], name: "index_posts_on_url", unique: true
t.index ["video_ms", "id"], name: "idx_posts_video_ms_id"
t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_posts_video_ms_positive"
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
end
@@ -370,7 +354,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at"
t.index ["theatre_id", "expires_at"], name: "idx_on_theatre_id_skip_expires_at_4c8de1dd42"
t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at"
t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id"
t.index ["user_id"], name: "index_theatre_watching_users_on_user_id"
@@ -530,8 +513,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_08_000000) do
add_foreign_key "post_implications", "posts", column: "parent_post_id"
add_foreign_key "post_similarities", "posts"
add_foreign_key "post_similarities", "posts", column: "target_post_id"
add_foreign_key "post_tag_sections", "posts"
add_foreign_key "post_tag_sections", "tags"
add_foreign_key "post_tags", "posts"
add_foreign_key "post_tags", "tags"
add_foreign_key "post_tags", "users", column: "created_user_id"
+20 -8
ファイルの表示
@@ -1,31 +1,32 @@
require 'rails_helper'
RSpec.describe 'Gekanator games API', type: :request do
let!(:admin) { create_admin_user! }
let!(:user) { create_member_user! }
let!(:guessed_post) { Post.create!(title: 'guess', url: 'https://example.com/guess') }
let!(:correct_post) { Post.create!(title: 'correct', url: 'https://example.com/correct') }
describe 'POST /gekanator/games' do
it 'stores a won game' do
sign_in_as user
sign_in_as admin
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: guessed_post.id,
question_count: 3,
answers: [{ question_id: 'tag:1', answer: 'yes' }] }
expect(response).to have_http_status(:created)
game = GekanatorGame.find(json['id'])
expect(game.user).to eq(user)
expect(game.user).to eq(admin)
expect(game.guessed_post).to eq(guessed_post)
expect(game.correct_post).to eq(guessed_post)
expect(game.won).to eq(true)
expect(game.question_count).to eq(1)
expect(game.answers).to eq([{ 'question_id' => 'tag:1', 'answer' => 'yes' }])
end
it 'stores a lost game with the correct post' do
sign_in_as user
sign_in_as admin
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
@@ -37,10 +38,11 @@ RSpec.describe 'Gekanator games API', type: :request do
game = GekanatorGame.find(json['id'])
expect(game.correct_post).to eq(correct_post)
expect(game.won).to eq(false)
expect(game.question_count).to eq(1)
end
it 'rejects a game without the correct post' do
sign_in_as user
sign_in_as admin
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
@@ -50,14 +52,24 @@ RSpec.describe 'Gekanator games API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'requires a user' do
it 'returns not found without an admin user' do
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: guessed_post.id,
question_count: 1,
answers: [] }
expect(response).to have_http_status(:unauthorized)
expect(response).to have_http_status(:not_found)
end
it 'returns not found for a non-admin user' do
sign_in_as user
post '/gekanator/games', params: {
guessed_post_id: guessed_post.id,
correct_post_id: guessed_post.id,
answers: [{ question_id: 'tag:1', answer: 'yes' }] }
expect(response).to have_http_status(:not_found)
end
end
end