ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

379 lines
12 KiB

  1. require 'rails_helper'
  2. RSpec.describe 'Materials API', type: :request do
  3. let!(:member_user) { create(:user, :member) }
  4. let!(:guest_user) { create(:user) }
  5. def dummy_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
  6. Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
  7. end
  8. def response_materials
  9. json.fetch('materials')
  10. end
  11. def build_material(tag:, user:, parent: nil, file: dummy_upload, url: nil)
  12. Material.new(tag:, parent:, url:, created_by_user: user, updated_by_user: user).tap do |material|
  13. material.file.attach(file) if file
  14. material.save!
  15. end
  16. end
  17. describe 'GET /materials' do
  18. let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material) }
  19. let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material) }
  20. let!(:material_a) do
  21. build_material(tag: tag_a, user: member_user, file: dummy_upload(filename: 'a.png'))
  22. end
  23. let!(:material_b) do
  24. build_material(tag: tag_b, user: member_user, parent: material_a, file: dummy_upload(filename: 'b.png'))
  25. end
  26. before do
  27. old_time = Time.zone.local(2026, 3, 29, 1, 0, 0)
  28. new_time = Time.zone.local(2026, 3, 29, 2, 0, 0)
  29. material_a.update_columns(created_at: old_time, updated_at: old_time)
  30. material_b.update_columns(created_at: new_time, updated_at: new_time)
  31. end
  32. it 'returns materials with count and metadata' do
  33. get '/materials'
  34. expect(response).to have_http_status(:ok)
  35. expect(json).to include('materials', 'count')
  36. expect(response_materials).to be_an(Array)
  37. expect(json['count']).to eq(2)
  38. row = response_materials.find { |m| m['id'] == material_b.id }
  39. expect(row).to be_present
  40. expect(row['tag']).to include(
  41. 'id' => tag_b.id,
  42. 'name' => 'material_index_b',
  43. 'category' => 'material'
  44. )
  45. expect(row['created_by_user']).to include(
  46. 'id' => member_user.id,
  47. 'name' => member_user.name
  48. )
  49. expect(row['content_type']).to eq('image/png')
  50. end
  51. it 'filters materials by tag_id' do
  52. get '/materials', params: { tag_id: material_a.tag_id }
  53. expect(response).to have_http_status(:ok)
  54. expect(json['count']).to eq(1)
  55. expect(response_materials.map { |m| m['id'] }).to eq([material_a.id])
  56. end
  57. it 'filters materials by parent_id' do
  58. get '/materials', params: { parent_id: material_a.id }
  59. expect(response).to have_http_status(:ok)
  60. expect(json['count']).to eq(1)
  61. expect(response_materials.map { |m| m['id'] }).to eq([material_b.id])
  62. end
  63. it 'paginates and keeps total count' do
  64. get '/materials', params: { page: 2, limit: 1 }
  65. expect(response).to have_http_status(:ok)
  66. expect(json['count']).to eq(2)
  67. expect(response_materials.size).to eq(1)
  68. expect(response_materials.first['id']).to eq(material_a.id)
  69. end
  70. it 'normalises invalid page and limit' do
  71. get '/materials', params: { page: 0, limit: 0 }
  72. expect(response).to have_http_status(:ok)
  73. expect(json['count']).to eq(2)
  74. expect(response_materials.size).to eq(1)
  75. expect(response_materials.first['id']).to eq(material_b.id)
  76. end
  77. end
  78. describe 'GET /materials/:id' do
  79. let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material) }
  80. let!(:material) do
  81. build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
  82. end
  83. it 'returns a material with file, tag, and content_type' do
  84. get "/materials/#{ material.id }"
  85. expect(response).to have_http_status(:ok)
  86. expect(json).to include(
  87. 'id' => material.id,
  88. 'content_type' => 'image/png'
  89. )
  90. expect(json['file']).to be_present
  91. expect(json['tag']).to include(
  92. 'id' => tag.id,
  93. 'name' => 'material_show',
  94. 'category' => 'material'
  95. )
  96. end
  97. it 'returns 404 when material does not exist' do
  98. get '/materials/999999999'
  99. expect(response).to have_http_status(:not_found)
  100. end
  101. end
  102. describe 'POST /materials' do
  103. context 'when not logged in' do
  104. before { sign_out }
  105. it 'returns 401' do
  106. post '/materials', params: {
  107. tag: 'material_create_unauthorized',
  108. file: dummy_upload
  109. }
  110. expect(response).to have_http_status(:unauthorized)
  111. end
  112. end
  113. context 'when logged in' do
  114. before { sign_in_as(guest_user) }
  115. it 'returns 400 when tag is blank' do
  116. post '/materials', params: { tag: ' ', file: dummy_upload }
  117. expect(response).to have_http_status(:bad_request)
  118. end
  119. it 'returns 400 when both file and url are blank' do
  120. post '/materials', params: { tag: 'material_create_blank' }
  121. expect(response).to have_http_status(:bad_request)
  122. end
  123. it 'creates a material with an attached file' do
  124. expect do
  125. post '/materials', params: {
  126. tag: 'material_create_new',
  127. file: dummy_upload(filename: 'created.png')
  128. }
  129. end.to change(Material, :count).by(1)
  130. .and change(Tag, :count).by(1)
  131. .and change(TagName, :count).by(1)
  132. expect(response).to have_http_status(:created)
  133. material = Material.order(:id).last
  134. expect(material.tag.name).to eq('material_create_new')
  135. expect(material.tag.category).to eq('material')
  136. expect(material.created_by_user).to eq(guest_user)
  137. expect(material.updated_by_user).to eq(guest_user)
  138. expect(material.file.attached?).to be(true)
  139. expect(json['id']).to eq(material.id)
  140. expect(json.dig('tag', 'name')).to eq('material_create_new')
  141. expect(json['content_type']).to eq('image/png')
  142. end
  143. it 'returns 422 when the existing tag is not material/character' do
  144. general_tag_name = TagName.create!(name: 'material_create_general_tag')
  145. Tag.create!(tag_name: general_tag_name, category: :general)
  146. post '/materials', params: {
  147. tag: 'material_create_general_tag',
  148. file: dummy_upload
  149. }
  150. expect(response).to have_http_status(:unprocessable_entity)
  151. end
  152. it 'persists url-only material' do
  153. expect do
  154. post '/materials', params: {
  155. tag: 'material_create_url_only',
  156. url: 'https://example.com/material-source'
  157. }
  158. end.to change(Material, :count).by(1)
  159. expect(response).to have_http_status(:created)
  160. material = Material.order(:id).last
  161. expect(material.tag.name).to eq('material_create_url_only')
  162. expect(material.url).to eq('https://example.com/material-source')
  163. expect(material.file.attached?).to be(false)
  164. end
  165. it 'returns the original url for url-only material' do
  166. post '/materials', params: {
  167. tag: 'material_create_url_only_response',
  168. url: 'https://example.com/material-source'
  169. }
  170. expect(response).to have_http_status(:created)
  171. expect(json['url']).to eq('https://example.com/material-source')
  172. end
  173. end
  174. end
  175. describe 'PUT /materials/:id' do
  176. let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material) }
  177. let!(:material) do
  178. build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
  179. end
  180. context 'when not logged in' do
  181. before { sign_out }
  182. it 'returns 401' do
  183. put "/materials/#{ material.id }", params: {
  184. tag: 'material_update_new',
  185. file: dummy_upload(filename: 'new.png')
  186. }
  187. expect(response).to have_http_status(:unauthorized)
  188. end
  189. end
  190. context 'when logged in but not member' do
  191. before { sign_in_as(guest_user) }
  192. it 'returns 403' do
  193. put "/materials/#{ material.id }", params: {
  194. tag: 'material_update_new',
  195. file: dummy_upload(filename: 'new.png')
  196. }
  197. expect(response).to have_http_status(:forbidden)
  198. end
  199. end
  200. context 'when member' do
  201. before { sign_in_as(member_user) }
  202. it 'returns 404 when material does not exist' do
  203. put '/materials/999999999', params: {
  204. tag: 'material_update_missing',
  205. file: dummy_upload
  206. }
  207. expect(response).to have_http_status(:not_found)
  208. end
  209. it 'returns 400 when tag is blank' do
  210. put "/materials/#{ material.id }", params: {
  211. tag: ' ',
  212. file: dummy_upload
  213. }
  214. expect(response).to have_http_status(:bad_request)
  215. end
  216. it 'returns 400 when both file and url are blank' do
  217. put "/materials/#{ material.id }", params: {
  218. tag: 'material_update_no_payload'
  219. }
  220. expect(response).to have_http_status(:bad_request)
  221. end
  222. it 'updates tag, url, file, and updated_by_user' do
  223. old_blob_id = material.file.blob.id
  224. put "/materials/#{ material.id }", params: {
  225. tag: 'material_update_new',
  226. url: 'https://example.com/updated-source',
  227. file: dummy_upload(filename: 'updated.jpg', type: 'image/jpeg')
  228. }
  229. expect(response).to have_http_status(:ok)
  230. material.reload
  231. expect(material.tag.name).to eq('material_update_new')
  232. expect(material.tag.category).to eq('material')
  233. expect(material.url).to eq('https://example.com/updated-source')
  234. expect(material.updated_by_user).to eq(member_user)
  235. expect(material.file.attached?).to be(true)
  236. expect(material.file.blob.id).not_to eq(old_blob_id)
  237. expect(material.file.blob.filename.to_s).to eq('updated.jpg')
  238. expect(material.file.blob.content_type).to eq('image/jpeg')
  239. expect(json['id']).to eq(material.id)
  240. expect(json['file']).to be_present
  241. expect(json['content_type']).to eq('image/jpeg')
  242. expect(json.dig('tag', 'name')).to eq('material_update_new')
  243. end
  244. it 'purges the existing file when file is omitted and url is provided' do
  245. old_blob_id = material.file.blob.id
  246. put "/materials/#{ material.id }", params: {
  247. tag: 'material_update_remove_file',
  248. url: 'https://example.com/updated-source'
  249. }
  250. expect(response).to have_http_status(:ok)
  251. material.reload
  252. expect(material.tag.name).to eq('material_update_remove_file')
  253. expect(material.url).to eq('https://example.com/updated-source')
  254. expect(material.updated_by_user).to eq(member_user)
  255. expect(material.file.attached?).to be(false)
  256. expect(
  257. ActiveStorage::Blob.where(id: old_blob_id).exists?
  258. ).to be(false)
  259. expect(json['id']).to eq(material.id)
  260. expect(json['file']).to be_nil
  261. expect(json['content_type']).to be_nil
  262. expect(json.dig('tag', 'name')).to eq('material_update_remove_file')
  263. expect(json['url']).to eq('https://example.com/updated-source')
  264. end
  265. end
  266. end
  267. describe 'DELETE /materials/:id' do
  268. let!(:tag) { Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material) }
  269. let!(:material) do
  270. build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
  271. end
  272. context 'when not logged in' do
  273. before { sign_out }
  274. it 'returns 401' do
  275. delete "/materials/#{ material.id }"
  276. expect(response).to have_http_status(:unauthorized)
  277. end
  278. end
  279. context 'when logged in but not member' do
  280. before { sign_in_as(guest_user) }
  281. it 'returns 403' do
  282. delete "/materials/#{ material.id }"
  283. expect(response).to have_http_status(:forbidden)
  284. end
  285. end
  286. context 'when member' do
  287. before { sign_in_as(member_user) }
  288. it 'returns 404 when material does not exist' do
  289. delete '/materials/999999999'
  290. expect(response).to have_http_status(:not_found)
  291. end
  292. it 'discards the material and returns 204' do
  293. delete "/materials/#{ material.id }"
  294. expect(response).to have_http_status(:no_content)
  295. expect(Material.find_by(id: material.id)).to be_nil
  296. expect(Material.with_discarded.find(material.id)).to be_discarded
  297. end
  298. end
  299. end
  300. end