| @@ -17,7 +17,12 @@ class Post < ApplicationRecord | |||||
| foreign_key: :target_post_id | foreign_key: :target_post_id | ||||
| has_one_attached :thumbnail | has_one_attached :thumbnail | ||||
| before_validation :normalise_url | |||||
| validates :url, presence: true, uniqueness: true | |||||
| validate :validate_original_created_range | validate :validate_original_created_range | ||||
| validate :url_must_be_http_url | |||||
| def as_json options = { } | def as_json options = { } | ||||
| super(options).merge({ thumbnail: thumbnail.attached? ? | super(options).merge({ thumbnail: thumbnail.attached? ? | ||||
| @@ -69,4 +74,33 @@ class Post < ApplicationRecord | |||||
| errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.' | errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.' | ||||
| end | end | ||||
| end | end | ||||
| def url_must_be_http_url | |||||
| begin | |||||
| u = URI.parse(url) | |||||
| rescue URI::InvalidURIError | |||||
| errors.add(:url, 'URL が不正です.') | |||||
| return | |||||
| end | |||||
| if !(u in URI::HTTP) || u.host.blank? | |||||
| errors.add(:url, 'URL が不正です.') | |||||
| return | |||||
| end | |||||
| end | |||||
| def normalise_url | |||||
| return if url.blank? | |||||
| self.url = url.strip | |||||
| u = URI.parse(url) | |||||
| return unless u in URI::HTTP | |||||
| u.host = u.host.downcase if u.host | |||||
| u.path = u.path.sub(/\/\Z/, '') if u.path.present? | |||||
| self.url = u.to_s | |||||
| rescue URI::InvalidURIError | |||||
| ; | |||||
| end | |||||
| end | end | ||||
| @@ -28,4 +28,8 @@ | |||||
| # enabled: "ON" | # enabled: "ON" | ||||
| en: | en: | ||||
| hello: "Hello world" | |||||
| activerecord: | |||||
| errors: | |||||
| messages: | |||||
| record_invalid: "Validation failed: %{errors}" | |||||
| taken: 'イキスギ!' | |||||
| @@ -0,0 +1,6 @@ | |||||
| class AddUniqueIndexToUrlInPosts < ActiveRecord::Migration[7.1] | |||||
| def change | |||||
| change_column :posts, :url, :string, limit: 768 | |||||
| add_index :posts, :url, unique: true, name: 'index_posts_on_url' | |||||
| end | |||||
| end | |||||
| @@ -10,7 +10,7 @@ | |||||
| # | # | ||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||
| ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) do | |||||
| ActiveRecord::Schema[8.0].define(version: 2026_01_18_144400) do | |||||
| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| t.string "name", null: false | t.string "name", null: false | ||||
| t.string "record_type", null: false | t.string "record_type", null: false | ||||
| @@ -85,7 +85,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) do | |||||
| create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| t.string "title" | t.string "title" | ||||
| t.string "url", limit: 2000, null: false | |||||
| t.string "url", limit: 768, null: false | |||||
| t.string "thumbnail_base", limit: 2000 | t.string "thumbnail_base", limit: 2000 | ||||
| t.bigint "parent_id" | t.bigint "parent_id" | ||||
| t.bigint "uploaded_user_id" | t.bigint "uploaded_user_id" | ||||
| @@ -95,6 +95,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) do | |||||
| t.datetime "updated_at", null: false | t.datetime "updated_at", null: false | ||||
| t.index ["parent_id"], name: "index_posts_on_parent_id" | t.index ["parent_id"], name: "index_posts_on_parent_id" | ||||
| t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id" | t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id" | ||||
| t.index ["url"], name: "index_posts_on_url", unique: true | |||||
| end | end | ||||
| create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| @@ -33,14 +33,14 @@ RSpec.describe 'Posts API', type: :request do | |||||
| let!(:hit_post) do | let!(:hit_post) do | ||||
| Post.create!(uploaded_user: user, title: "hello spec world", | Post.create!(uploaded_user: user, title: "hello spec world", | ||||
| url: 'https://example.com/spec').tap do |p| | |||||
| url: 'https://example.com/spec2').tap do |p| | |||||
| PostTag.create!(post: p, tag:) | PostTag.create!(post: p, tag:) | ||||
| end | end | ||||
| end | end | ||||
| let!(:miss_post) do | let!(:miss_post) do | ||||
| Post.create!(uploaded_user: user, title: "unrelated title", | Post.create!(uploaded_user: user, title: "unrelated title", | ||||
| url: 'https://example.com/spec2').tap do |p| | |||||
| url: 'https://example.com/spec3').tap do |p| | |||||
| PostTag.create!(post: p, tag: tag2) | PostTag.create!(post: p, tag: tag2) | ||||
| end | end | ||||
| end | end | ||||
| @@ -158,6 +158,36 @@ RSpec.describe 'Posts API', type: :request do | |||||
| expect(json['tags']).to be_an(Array) | expect(json['tags']).to be_an(Array) | ||||
| expect(json['tags'][0]).to have_key('name') | expect(json['tags'][0]).to have_key('name') | ||||
| end | end | ||||
| context 'when url is blank' do | |||||
| it 'returns 422' do | |||||
| sign_in_as(member) | |||||
| post '/posts', params: { | |||||
| title: 'new post', | |||||
| url: ' ', | |||||
| tags: 'spec_tag', # 既存タグ名を投げる | |||||
| thumbnail: dummy_upload | |||||
| } | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| end | |||||
| end | |||||
| context 'when url is invalid' do | |||||
| it 'returns 422' do | |||||
| sign_in_as(member) | |||||
| post '/posts', params: { | |||||
| title: 'new post', | |||||
| url: 'ぼざクリタグ広場', | |||||
| tags: 'spec_tag', # 既存タグ名を投げる | |||||
| thumbnail: dummy_upload | |||||
| } | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| end | |||||
| end | |||||
| end | end | ||||
| describe 'PUT /posts/:id' do | describe 'PUT /posts/:id' do | ||||
| @@ -8,7 +8,9 @@ RSpec.describe 'Wiki API', type: :request do | |||||
| let!(:tn) { TagName.create!(name: 'spec_wiki_title') } | let!(:tn) { TagName.create!(name: 'spec_wiki_title') } | ||||
| let!(:page) do | let!(:page) do | ||||
| WikiPage.create!(tag_name: tn, created_user: user, updated_user: user) | |||||
| WikiPage.create!(tag_name: tn, created_user: user, updated_user: user).tap do |p| | |||||
| Wiki::Commit.content!(page: p, body: 'init', created_user: user, message: 'init') | |||||
| end | |||||
| end | end | ||||
| describe 'GET /wiki' do | describe 'GET /wiki' do | ||||