Browse Source

feat: URL 正規化(#208) (#226)

#208

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/226
pull/234/head
みてるぞ 3 weeks ago
parent
commit
86209dcc84
6 changed files with 83 additions and 6 deletions
  1. +34
    -0
      backend/app/models/post.rb
  2. +5
    -1
      backend/config/locales/en.yml
  3. +6
    -0
      backend/db/migrate/20260118144400_add_unique_index_to_url_in_posts.rb
  4. +3
    -2
      backend/db/schema.rb
  5. +32
    -2
      backend/spec/requests/posts_spec.rb
  6. +3
    -1
      backend/spec/requests/wiki_spec.rb

+ 34
- 0
backend/app/models/post.rb View File

@@ -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

+ 5
- 1
backend/config/locales/en.yml View File

@@ -28,4 +28,8 @@
# enabled: "ON" # enabled: "ON"


en: en:
hello: "Hello world"
activerecord:
errors:
messages:
record_invalid: "Validation failed: %{errors}"
taken: 'イキスギ!'

+ 6
- 0
backend/db/migrate/20260118144400_add_unique_index_to_url_in_posts.rb View File

@@ -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

+ 3
- 2
backend/db/schema.rb View File

@@ -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|


+ 32
- 2
backend/spec/requests/posts_spec.rb View File

@@ -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


+ 3
- 1
backend/spec/requests/wiki_spec.rb View File

@@ -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


Loading…
Cancel
Save