This commit is contained in:
@@ -18,7 +18,7 @@ class MaterialsController < ApplicationController
|
|||||||
count = q.count
|
count = q.count
|
||||||
materials = q.order(created_at: :desc, id: :desc).limit(limit).offset(offset)
|
materials = q.order(created_at: :desc, id: :desc).limit(limit).offset(offset)
|
||||||
|
|
||||||
render json: { materials: materials.map { |m| material_json(m) }, count: count }
|
render json: { materials: MaterialRepr.many(materials), count: count }
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -29,11 +29,7 @@ class MaterialsController < ApplicationController
|
|||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
return head :not_found unless material
|
return head :not_found unless material
|
||||||
|
|
||||||
render json: material.as_json(methods: [:content_type]).merge(
|
render json: MaterialRepr.base(material)
|
||||||
file: if material.file.attached?
|
|
||||||
rails_storage_proxy_url(material.file, only_path: false)
|
|
||||||
end,
|
|
||||||
tag: TagRepr.base(material.tag))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -54,7 +50,7 @@ class MaterialsController < ApplicationController
|
|||||||
material.file.attach(file)
|
material.file.attach(file)
|
||||||
|
|
||||||
if material.save
|
if material.save
|
||||||
render json: material_json(material), status: :created
|
render json: MaterialRepr.base(material), status: :created
|
||||||
else
|
else
|
||||||
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -80,15 +76,11 @@ class MaterialsController < ApplicationController
|
|||||||
if file
|
if file
|
||||||
material.file.attach(file)
|
material.file.attach(file)
|
||||||
else
|
else
|
||||||
material.file.purge(file)
|
material.file.purge
|
||||||
end
|
end
|
||||||
|
|
||||||
if material.save
|
if material.save
|
||||||
render json: material.as_json(methods: [:content_type]).merge(
|
render json: MaterialRepr.base(material)
|
||||||
file: if material.file.attached?
|
|
||||||
rails_storage_proxy_url(material.file, only_path: false)
|
|
||||||
end,
|
|
||||||
tag: TagRepr.base(material.tag))
|
|
||||||
else
|
else
|
||||||
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -104,15 +96,4 @@ class MaterialsController < ApplicationController
|
|||||||
material.discard
|
material.discard
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def material_json(material)
|
|
||||||
MaterialRepr.base(material).merge(
|
|
||||||
'filename' => material.file.attached? ? material.file.filename.to_s : nil,
|
|
||||||
'byte_size' => material.file.attached? ? material.file.byte_size : nil,
|
|
||||||
'content_type' => material.file.attached? ? material.file.content_type : nil,
|
|
||||||
'url' => material.file.attached? ? url_for(material.file) : nil
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class TagsController < ApplicationController
|
|||||||
tag = Tag.joins(:tag_name)
|
tag = Tag.joins(:tag_name)
|
||||||
.includes(:tag_name, tag_name: :wiki_page)
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
.find_by(tag_names: { name: })
|
.find_by(tag_names: { name: })
|
||||||
return :not_found unless tag
|
return head :not_found unless tag
|
||||||
|
|
||||||
render json: build_tag_children(tag)
|
render json: build_tag_children(tag)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,13 +2,19 @@
|
|||||||
|
|
||||||
|
|
||||||
module MaterialRepr
|
module MaterialRepr
|
||||||
BASE = { only: [:id, :url, :parent_id, :created_at, :updated_at],
|
BASE = { methods: [:content_type],
|
||||||
include: { created_by_user: UserRepr::BASE, tag: TagRepr::BASE } }.freeze
|
include: { created_by_user: UserRepr::BASE,
|
||||||
|
updated_by_user: UserRepr::BASE } }.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def base(material)
|
def base(material)
|
||||||
material.as_json(BASE)
|
material.as_json(BASE).merge(
|
||||||
|
file: if material.file.attached?
|
||||||
|
Rails.application.routes.url_helpers.rails_storage_proxy_url(
|
||||||
|
material.file, only_path: false)
|
||||||
|
end,
|
||||||
|
tag: TagRepr.base(material.tag))
|
||||||
end
|
end
|
||||||
|
|
||||||
def many(materials)
|
def many(materials)
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ Rails.application.configure do
|
|||||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||||
# config.asset_host = "http://assets.example.com"
|
# config.asset_host = "http://assets.example.com"
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# TODO: オブスト契約したら :r2 に変更する.
|
||||||
config.active_storage.service = :r2
|
config.active_storage.service = :local
|
||||||
|
|
||||||
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||||
config.assume_ssl = true
|
config.assume_ssl = true
|
||||||
|
|||||||
@@ -50,4 +50,6 @@ Rails.application.configure do
|
|||||||
|
|
||||||
# Raise error when a before_action's only/except options reference missing actions.
|
# Raise error when a before_action's only/except options reference missing actions.
|
||||||
config.action_controller.raise_on_missing_callback_actions = true
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
|
|
||||||
|
Rails.application.routes.default_url_options[:host] = 'www.example.com'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Materials API', type: :request do
|
||||||
|
let!(:member_user) { create(:user, :member) }
|
||||||
|
let!(:guest_user) { create(:user) }
|
||||||
|
|
||||||
|
def dummy_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
|
||||||
|
Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
def response_materials
|
||||||
|
json.fetch('materials')
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_material(tag:, user:, parent: nil, file: dummy_upload, url: nil)
|
||||||
|
Material.new(tag:, parent:, url:, created_by_user: user, updated_by_user: user).tap do |material|
|
||||||
|
material.file.attach(file) if file
|
||||||
|
material.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /materials' do
|
||||||
|
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material) }
|
||||||
|
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material) }
|
||||||
|
|
||||||
|
let!(:material_a) do
|
||||||
|
build_material(tag: tag_a, user: member_user, file: dummy_upload(filename: 'a.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:material_b) do
|
||||||
|
build_material(tag: tag_b, user: member_user, parent: material_a, file: dummy_upload(filename: 'b.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
old_time = Time.zone.local(2026, 3, 29, 1, 0, 0)
|
||||||
|
new_time = Time.zone.local(2026, 3, 29, 2, 0, 0)
|
||||||
|
|
||||||
|
material_a.update_columns(created_at: old_time, updated_at: old_time)
|
||||||
|
material_b.update_columns(created_at: new_time, updated_at: new_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns materials with count and metadata' do
|
||||||
|
get '/materials'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('materials', 'count')
|
||||||
|
expect(response_materials).to be_an(Array)
|
||||||
|
expect(json['count']).to eq(2)
|
||||||
|
|
||||||
|
row = response_materials.find { |m| m['id'] == material_b.id }
|
||||||
|
expect(row).to be_present
|
||||||
|
expect(row['tag']).to include(
|
||||||
|
'id' => tag_b.id,
|
||||||
|
'name' => 'material_index_b',
|
||||||
|
'category' => 'material'
|
||||||
|
)
|
||||||
|
expect(row['created_by_user']).to include(
|
||||||
|
'id' => member_user.id,
|
||||||
|
'name' => member_user.name
|
||||||
|
)
|
||||||
|
expect(row['content_type']).to eq('image/png')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters materials by tag_id' do
|
||||||
|
get '/materials', params: { tag_id: material_a.tag_id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json['count']).to eq(1)
|
||||||
|
expect(response_materials.map { |m| m['id'] }).to eq([material_a.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters materials by parent_id' do
|
||||||
|
get '/materials', params: { parent_id: material_a.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json['count']).to eq(1)
|
||||||
|
expect(response_materials.map { |m| m['id'] }).to eq([material_b.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'paginates and keeps total count' do
|
||||||
|
get '/materials', params: { page: 2, limit: 1 }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json['count']).to eq(2)
|
||||||
|
expect(response_materials.size).to eq(1)
|
||||||
|
expect(response_materials.first['id']).to eq(material_a.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'normalises invalid page and limit' do
|
||||||
|
get '/materials', params: { page: 0, limit: 0 }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json['count']).to eq(2)
|
||||||
|
expect(response_materials.size).to eq(1)
|
||||||
|
expect(response_materials.first['id']).to eq(material_b.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /materials/:id' do
|
||||||
|
let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material) }
|
||||||
|
let!(:material) do
|
||||||
|
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a material with file, tag, and content_type' do
|
||||||
|
get "/materials/#{ material.id }"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include(
|
||||||
|
'id' => material.id,
|
||||||
|
'content_type' => 'image/png'
|
||||||
|
)
|
||||||
|
expect(json['file']).to be_present
|
||||||
|
expect(json['tag']).to include(
|
||||||
|
'id' => tag.id,
|
||||||
|
'name' => 'material_show',
|
||||||
|
'category' => 'material'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when material does not exist' do
|
||||||
|
get '/materials/999999999'
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /materials' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
before { sign_out }
|
||||||
|
|
||||||
|
it 'returns 401' do
|
||||||
|
post '/materials', params: {
|
||||||
|
tag: 'material_create_unauthorized',
|
||||||
|
file: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
before { sign_in_as(guest_user) }
|
||||||
|
|
||||||
|
it 'returns 400 when tag is blank' do
|
||||||
|
post '/materials', params: { tag: ' ', file: dummy_upload }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 400 when both file and url are blank' do
|
||||||
|
post '/materials', params: { tag: 'material_create_blank' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a material with an attached file' do
|
||||||
|
expect do
|
||||||
|
post '/materials', params: {
|
||||||
|
tag: 'material_create_new',
|
||||||
|
file: dummy_upload(filename: 'created.png')
|
||||||
|
}
|
||||||
|
end.to change(Material, :count).by(1)
|
||||||
|
.and change(Tag, :count).by(1)
|
||||||
|
.and change(TagName, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
material = Material.order(:id).last
|
||||||
|
expect(material.tag.name).to eq('material_create_new')
|
||||||
|
expect(material.tag.category).to eq('material')
|
||||||
|
expect(material.created_by_user).to eq(guest_user)
|
||||||
|
expect(material.updated_by_user).to eq(guest_user)
|
||||||
|
expect(material.file.attached?).to be(true)
|
||||||
|
|
||||||
|
expect(json['id']).to eq(material.id)
|
||||||
|
expect(json.dig('tag', 'name')).to eq('material_create_new')
|
||||||
|
expect(json['content_type']).to eq('image/png')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 when the existing tag is not material/character' do
|
||||||
|
general_tag_name = TagName.create!(name: 'material_create_general_tag')
|
||||||
|
Tag.create!(tag_name: general_tag_name, category: :general)
|
||||||
|
|
||||||
|
post '/materials', params: {
|
||||||
|
tag: 'material_create_general_tag',
|
||||||
|
file: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'persists url-only material' do
|
||||||
|
expect do
|
||||||
|
post '/materials', params: {
|
||||||
|
tag: 'material_create_url_only',
|
||||||
|
url: 'https://example.com/material-source'
|
||||||
|
}
|
||||||
|
end.to change(Material, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
material = Material.order(:id).last
|
||||||
|
expect(material.tag.name).to eq('material_create_url_only')
|
||||||
|
expect(material.url).to eq('https://example.com/material-source')
|
||||||
|
expect(material.file.attached?).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the original url for url-only material' do
|
||||||
|
post '/materials', params: {
|
||||||
|
tag: 'material_create_url_only_response',
|
||||||
|
url: 'https://example.com/material-source'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
expect(json['url']).to eq('https://example.com/material-source')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /materials/:id' do
|
||||||
|
let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material) }
|
||||||
|
let!(:material) do
|
||||||
|
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not logged in' do
|
||||||
|
before { sign_out }
|
||||||
|
|
||||||
|
it 'returns 401' do
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: 'material_update_new',
|
||||||
|
file: dummy_upload(filename: 'new.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in but not member' do
|
||||||
|
before { sign_in_as(guest_user) }
|
||||||
|
|
||||||
|
it 'returns 403' do
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: 'material_update_new',
|
||||||
|
file: dummy_upload(filename: 'new.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when member' do
|
||||||
|
before { sign_in_as(member_user) }
|
||||||
|
|
||||||
|
it 'returns 404 when material does not exist' do
|
||||||
|
put '/materials/999999999', params: {
|
||||||
|
tag: 'material_update_missing',
|
||||||
|
file: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 400 when tag is blank' do
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: ' ',
|
||||||
|
file: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 400 when both file and url are blank' do
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: 'material_update_no_payload'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates tag, url, file, and updated_by_user' do
|
||||||
|
old_blob_id = material.file.blob.id
|
||||||
|
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: 'material_update_new',
|
||||||
|
url: 'https://example.com/updated-source',
|
||||||
|
file: dummy_upload(filename: 'updated.jpg', type: 'image/jpeg')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
material.reload
|
||||||
|
expect(material.tag.name).to eq('material_update_new')
|
||||||
|
expect(material.tag.category).to eq('material')
|
||||||
|
expect(material.url).to eq('https://example.com/updated-source')
|
||||||
|
expect(material.updated_by_user).to eq(member_user)
|
||||||
|
expect(material.file.attached?).to be(true)
|
||||||
|
expect(material.file.blob.id).not_to eq(old_blob_id)
|
||||||
|
expect(material.file.blob.filename.to_s).to eq('updated.jpg')
|
||||||
|
expect(material.file.blob.content_type).to eq('image/jpeg')
|
||||||
|
|
||||||
|
expect(json['id']).to eq(material.id)
|
||||||
|
expect(json['file']).to be_present
|
||||||
|
expect(json['content_type']).to eq('image/jpeg')
|
||||||
|
expect(json.dig('tag', 'name')).to eq('material_update_new')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'purges the existing file when file is omitted and url is provided' do
|
||||||
|
old_blob_id = material.file.blob.id
|
||||||
|
|
||||||
|
put "/materials/#{ material.id }", params: {
|
||||||
|
tag: 'material_update_remove_file',
|
||||||
|
url: 'https://example.com/updated-source'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
material.reload
|
||||||
|
expect(material.tag.name).to eq('material_update_remove_file')
|
||||||
|
expect(material.url).to eq('https://example.com/updated-source')
|
||||||
|
expect(material.updated_by_user).to eq(member_user)
|
||||||
|
expect(material.file.attached?).to be(false)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ActiveStorage::Blob.where(id: old_blob_id).exists?
|
||||||
|
).to be(false)
|
||||||
|
|
||||||
|
expect(json['id']).to eq(material.id)
|
||||||
|
expect(json['file']).to be_nil
|
||||||
|
expect(json['content_type']).to be_nil
|
||||||
|
expect(json.dig('tag', 'name')).to eq('material_update_remove_file')
|
||||||
|
expect(json['url']).to eq('https://example.com/updated-source')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /materials/:id' do
|
||||||
|
let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material) }
|
||||||
|
let!(:material) do
|
||||||
|
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not logged in' do
|
||||||
|
before { sign_out }
|
||||||
|
|
||||||
|
it 'returns 401' do
|
||||||
|
delete "/materials/#{ material.id }"
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in but not member' do
|
||||||
|
before { sign_in_as(guest_user) }
|
||||||
|
|
||||||
|
it 'returns 403' do
|
||||||
|
delete "/materials/#{ material.id }"
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when member' do
|
||||||
|
before { sign_in_as(member_user) }
|
||||||
|
|
||||||
|
it 'returns 404 when material does not exist' do
|
||||||
|
delete '/materials/999999999'
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'discards the material and returns 204' do
|
||||||
|
delete "/materials/#{ material.id }"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
expect(Material.find_by(id: material.id)).to be_nil
|
||||||
|
expect(Material.with_discarded.find(material.id)).to be_discarded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,6 +19,17 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
response_tags.map { |t| t.fetch('name') }
|
response_tags.map { |t| t.fetch('name') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dummy_material_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
|
||||||
|
Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_material(tag, user:, filename: 'dummy.png', type: 'image/png', url: nil)
|
||||||
|
Material.new(tag:, url:, created_by_user: user, updated_by_user: user).tap do |material|
|
||||||
|
material.file.attach(dummy_material_upload(filename:, type:)) if filename
|
||||||
|
material.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /tags' do
|
describe 'GET /tags' do
|
||||||
it 'returns tags with count and metadata' do
|
it 'returns tags with count and metadata' do
|
||||||
get '/tags'
|
get '/tags'
|
||||||
@@ -359,4 +370,144 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET /tags/with-depth' do
|
||||||
|
let!(:root_meme) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_a_root_meme'), category: :meme)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:root_material) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_b_root_material'), category: :material)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:hidden_general_root) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_hidden_general_root'), category: :general)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:child_character) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_child_character'), category: :character)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:grandchild_material) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_grandchild_material'), category: :material)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:child_general) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'depth_child_general'), category: :general)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
TagImplication.create!(parent_tag: root_meme, tag: child_character)
|
||||||
|
TagImplication.create!(parent_tag: child_character, tag: grandchild_material)
|
||||||
|
TagImplication.create!(parent_tag: root_material, tag: child_general)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only visible root tags and visible has_children flags' do
|
||||||
|
get '/tags/with-depth'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.map { |t| t['name'] }).to eq([
|
||||||
|
'depth_a_root_meme',
|
||||||
|
'depth_b_root_material'
|
||||||
|
])
|
||||||
|
|
||||||
|
meme_row = json.find { |t| t['name'] == 'depth_a_root_meme' }
|
||||||
|
material_row = json.find { |t| t['name'] == 'depth_b_root_material' }
|
||||||
|
|
||||||
|
expect(meme_row['has_children']).to eq(true)
|
||||||
|
expect(meme_row['children']).to eq([])
|
||||||
|
|
||||||
|
expect(material_row['has_children']).to eq(false)
|
||||||
|
expect(material_row['children']).to eq([])
|
||||||
|
|
||||||
|
expect(json.map { |t| t['name'] }).not_to include('depth_hidden_general_root')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns children of the specified parent' do
|
||||||
|
get '/tags/with-depth', params: { parent: root_meme.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.map { |t| t['name'] }).to eq(['depth_child_character'])
|
||||||
|
|
||||||
|
row = json.first
|
||||||
|
expect(row['category']).to eq('character')
|
||||||
|
expect(row['has_children']).to eq(true)
|
||||||
|
expect(row['children']).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /tags/name/:name/materials' do
|
||||||
|
let!(:material_user) { create_member_user! }
|
||||||
|
|
||||||
|
let!(:root_tag) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'materials_root'), category: :material)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:child_a_tag) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'materials_child_a'), category: :material)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:child_b_tag) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'materials_child_b'), category: :character)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:grandchild_tag) do
|
||||||
|
Tag.create!(tag_name: TagName.create!(name: 'materials_grandchild'), category: :material)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:root_material) do
|
||||||
|
create_material(root_tag, user: material_user, filename: 'root.png')
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:child_a_material) do
|
||||||
|
create_material(child_a_tag, user: material_user, filename: 'child_a.png')
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:grandchild_material) do
|
||||||
|
create_material(grandchild_tag, user: material_user, filename: 'grandchild.png')
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
TagImplication.create!(parent_tag: root_tag, tag: child_b_tag)
|
||||||
|
TagImplication.create!(parent_tag: root_tag, tag: child_a_tag)
|
||||||
|
TagImplication.create!(parent_tag: child_a_tag, tag: grandchild_tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a tag tree with nested materials sorted by child name' do
|
||||||
|
get "/tags/name/#{ CGI.escape(root_tag.name) }/materials"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
expect(json).to include(
|
||||||
|
'id' => root_tag.id,
|
||||||
|
'name' => 'materials_root',
|
||||||
|
'category' => 'material'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(json['material']).to be_present
|
||||||
|
expect(json.dig('material', 'id')).to eq(root_material.id)
|
||||||
|
expect(json.dig('material', 'file')).to be_present
|
||||||
|
expect(json.dig('material', 'content_type')).to eq('image/png')
|
||||||
|
|
||||||
|
expect(json['children'].map { |t| t['name'] }).to eq([
|
||||||
|
'materials_child_a',
|
||||||
|
'materials_child_b'
|
||||||
|
])
|
||||||
|
|
||||||
|
child_a = json['children'].find { |t| t['name'] == 'materials_child_a' }
|
||||||
|
child_b = json['children'].find { |t| t['name'] == 'materials_child_b' }
|
||||||
|
|
||||||
|
expect(child_a.dig('material', 'id')).to eq(child_a_material.id)
|
||||||
|
expect(child_a['children'].map { |t| t['name'] }).to eq(['materials_grandchild'])
|
||||||
|
expect(child_a.dig('children', 0, 'material', 'id')).to eq(grandchild_material.id)
|
||||||
|
|
||||||
|
expect(child_b['material']).to be_nil
|
||||||
|
expect(child_b['children']).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when the tag does not exist' do
|
||||||
|
get '/tags/name/no_such_tag_12345/materials'
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user