API for Reactions: Create endpoint (#18355)
* Extract reaction create from controller to service * Success is having no errors * Confirm http status successful * V1 API endpoint for Create Reaction * Actually reset cache count * Tweak JSON response * Reaction create -> toggle * Try rswagger docs for reaction toggle
This commit is contained in:
parent
69a08480a8
commit
b6efce21fe
5 changed files with 266 additions and 22 deletions
34
app/controllers/api/v1/reactions_controller.rb
Normal file
34
app/controllers/api/v1/reactions_controller.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
module Api
|
||||
module V1
|
||||
class ReactionsController < ApiController
|
||||
before_action :authenticate!
|
||||
|
||||
def toggle
|
||||
remove_count_cache_key
|
||||
|
||||
result = ReactionToggle.toggle(params, current_user: current_user || @user)
|
||||
|
||||
if result.success?
|
||||
render json: {
|
||||
result: result.action,
|
||||
category: result.category,
|
||||
id: result.reaction.id,
|
||||
reactable_id: result.reaction.reactable_id,
|
||||
reactable_type: result.reaction.reactable_type
|
||||
}
|
||||
else
|
||||
render json: { error: result.errors_as_sentence, status: 422 }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: should this move to toggle service? refactor?
|
||||
def remove_count_cache_key
|
||||
return unless params[:reactable_type] == "Article"
|
||||
|
||||
Rails.cache.delete "count_for_reactable-Article-#{params[:reactable_id]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -48,6 +48,8 @@ Rails.application.routes.draw do
|
|||
put "/articles/:id/unpublish", to: "articles#unpublish", as: :article_unpublish
|
||||
put "/users/:id/unpublish", to: "users#unpublish", as: :user_unpublish
|
||||
|
||||
post "/reactions/toggle", to: "reactions#toggle"
|
||||
|
||||
draw :api
|
||||
end
|
||||
|
||||
|
|
|
|||
72
spec/requests/api/v1/docs/reactions_spec.rb
Normal file
72
spec/requests/api/v1/docs/reactions_spec.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
require "rails_helper"
|
||||
require "swagger_helper"
|
||||
|
||||
# rubocop:disable RSpec/EmptyExampleGroup
|
||||
# rubocop:disable RSpec/VariableName
|
||||
|
||||
RSpec.describe "api/v1/reactions", type: :request do
|
||||
let(:Accept) { "application/vnd.forem.api-v1+json" }
|
||||
let(:api_secret) { create(:api_secret) }
|
||||
let(:category) { "like" }
|
||||
let(:reactable) { create :article }
|
||||
let(:reaction) { reactable.reactions.create user: user, category: "like" }
|
||||
let(:result) { ReactionToggle::Result.new reaction: reaction }
|
||||
let(:user) { api_secret.user }
|
||||
|
||||
before do
|
||||
result.category = category
|
||||
allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true)
|
||||
allow(ReactionToggle).to receive(:toggle).and_return(result)
|
||||
end
|
||||
|
||||
path "/api/reactions/toggle" do
|
||||
describe "post to create reaction" do
|
||||
post("create reaction") do
|
||||
tags "reactions"
|
||||
description(<<-DESCRIBE.strip)
|
||||
This endpoint allows the client to toggle the user's reaction to a specified reactable (eg, Article, Comment, or User). For examples:
|
||||
* "Like"ing an Article will create a new "like" Reaction from the user for that Articles
|
||||
* "Like"ing that Article a second time will remove the "like" from the user
|
||||
DESCRIBE
|
||||
|
||||
produces "application/json"
|
||||
parameter name: :category, in: :query, required: true, schema: {
|
||||
type: :string,
|
||||
enum: Reaction::PUBLIC_CATEGORIES
|
||||
}
|
||||
parameter name: :reactable_id, in: :query, required: true, schema: {
|
||||
type: :integer,
|
||||
format: :int32
|
||||
}
|
||||
parameter name: :reactable_type, in: :query, required: true, schema: {
|
||||
type: :string,
|
||||
enum: Reaction::REACTABLE_TYPES
|
||||
}
|
||||
|
||||
let(:reactable_id) { reactable.id }
|
||||
let(:reactable_type) { "Article" }
|
||||
|
||||
before do
|
||||
result.action = "create"
|
||||
end
|
||||
|
||||
response(200, "successful") do
|
||||
let(:"api-key") { api_secret.secret }
|
||||
add_examples
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "401", "unauthorized" do
|
||||
let(:"api-key") { "invalid" }
|
||||
add_examples
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable RSpec/VariableName
|
||||
# rubocop:enable RSpec/EmptyExampleGroup
|
||||
73
spec/requests/api/v1/reactions_spec.rb
Normal file
73
spec/requests/api/v1/reactions_spec.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Api::V1::Reactions", type: :request do
|
||||
let!(:v1_headers) { { "content-type" => "application/json", "Accept" => "application/vnd.forem.api-v1+json" } }
|
||||
let(:params) do
|
||||
{
|
||||
reactable_type: "Article",
|
||||
reactable_id: "123",
|
||||
category: "like"
|
||||
}
|
||||
end
|
||||
|
||||
before { allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) }
|
||||
|
||||
shared_context "when user is authorized" do
|
||||
let(:api_secret) { create(:api_secret) }
|
||||
let(:user) { api_secret.user }
|
||||
let(:auth_header) { v1_headers.merge({ "api-key" => api_secret.secret }) }
|
||||
end
|
||||
|
||||
context "when unauthenticated and post to create" do
|
||||
it "returns unauthorized" do
|
||||
post api_reactions_toggle_path, params: params.to_json, headers: v1_headers
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context "when unauthorized and post to create" do
|
||||
it "returns unauthorized" do
|
||||
post api_reactions_toggle_path, params: params.to_json,
|
||||
headers: v1_headers.merge({ "api-key" => "invalid api key" })
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context "when authorized and post to create" do
|
||||
include_context "when user is authorized"
|
||||
|
||||
before do
|
||||
allow(Rails.cache).to receive(:delete)
|
||||
allow(ReactionToggle).to receive(:toggle).and_return(result)
|
||||
end
|
||||
|
||||
context "when toggled successfully" do
|
||||
let(:result) { ReactionToggle::Result.new reaction: Reaction.new }
|
||||
|
||||
it "responds with success" do
|
||||
post api_reactions_toggle_path, params: params.to_json, headers: auth_header
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it "responds with expected JSON" do
|
||||
post api_reactions_toggle_path, params: params.to_json, headers: auth_header
|
||||
expect(JSON.parse(response.body).keys).to contain_exactly("id", "result", "category", "reactable_type",
|
||||
"reactable_id")
|
||||
end
|
||||
end
|
||||
|
||||
context "when toggled unsuccessfully" do
|
||||
let(:result) do
|
||||
ReactionToggle::Result.new.tap do |bad_result|
|
||||
allow(bad_result).to receive(:success?).and_return(false)
|
||||
allow(bad_result).to receive(:errors_as_sentence).and_return("Stuff was bad")
|
||||
end
|
||||
end
|
||||
|
||||
it "responds with success" do
|
||||
post api_reactions_toggle_path, params: params.to_json, headers: auth_header
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Forem API V1",
|
||||
"version": "1.0.0",
|
||||
"description": "Access Forem articles, users and other resources via API.\n For a real-world example of Forem in action, check out [DEV](https://www.dev.to).\n All endpoints can be accessed with the 'api-key' header and a accept header, but \n some of them are accessible publicly without authentication.\n\n Dates and date times, unless otherwise specified, must be in\n the [RFC 3339](https://tools.ietf.org/html/rfc3339) format."
|
||||
"description": "Access Forem articles, users and other resources via API.\n For a real-world example of Forem in action, check out [DEV](https://www.dev.to).\n All endpoints can be accessed with the 'api-key' header and a accept header, but\n some of them are accessible publicly without authentication.\n\n Dates and date times, unless otherwise specified, must be in\n the [RFC 3339](https://tools.ietf.org/html/rfc3339) format."
|
||||
},
|
||||
"paths": {
|
||||
"/api/articles": {
|
||||
|
|
@ -103,45 +103,45 @@
|
|||
"example": [
|
||||
{
|
||||
"type_of": "article",
|
||||
"id": 1357,
|
||||
"title": "Ego Dominus Tuus168",
|
||||
"description": "Blog pour-over taxidermy pbr&b bitters. Synth chia pork belly butcher. Mumblecore 8-bit banh mi...",
|
||||
"readable_publish_date": "Aug 20",
|
||||
"slug": "ego-dominus-tuus168-32d5",
|
||||
"path": "/username372/ego-dominus-tuus168-32d5",
|
||||
"url": "http://localhost:3000/username372/ego-dominus-tuus168-32d5",
|
||||
"id": 4453,
|
||||
"title": "East of Eden168",
|
||||
"description": "Put a bird on it aesthetic narwhal xoxo pug. Disrupt slow-carb williamsburg crucifix...",
|
||||
"readable_publish_date": "Aug 23",
|
||||
"slug": "east-of-eden168-21ml",
|
||||
"path": "/username372/east-of-eden168-21ml",
|
||||
"url": "http://localhost:3000/username372/east-of-eden168-21ml",
|
||||
"comments_count": 0,
|
||||
"public_reactions_count": 0,
|
||||
"collection_id": null,
|
||||
"published_timestamp": "2022-08-19T16:09:09Z",
|
||||
"published_timestamp": "2022-08-23T12:08:21Z",
|
||||
"positive_reactions_count": 0,
|
||||
"cover_image": "http://localhost:3000/assets/2-1a96ae446ded018b65b215cce3aecc40a00e701642da521fdd6edd3c593ff6c1.png",
|
||||
"social_image": "http://localhost:3000/assets/2-1a96ae446ded018b65b215cce3aecc40a00e701642da521fdd6edd3c593ff6c1.png",
|
||||
"canonical_url": "http://localhost:3000/username372/ego-dominus-tuus168-32d5",
|
||||
"created_at": "2022-08-19T16:09:09Z",
|
||||
"cover_image": "http://localhost:3000/assets/24-377bc0861a9a539e8d1875ea9d4eea9e0226d93e1b6e9317e0c73c754699cc14.png",
|
||||
"social_image": "http://localhost:3000/assets/24-377bc0861a9a539e8d1875ea9d4eea9e0226d93e1b6e9317e0c73c754699cc14.png",
|
||||
"canonical_url": "http://localhost:3000/username372/east-of-eden168-21ml",
|
||||
"created_at": "2022-08-23T12:08:21Z",
|
||||
"edited_at": null,
|
||||
"crossposted_at": null,
|
||||
"published_at": "2022-08-19T16:09:09Z",
|
||||
"last_comment_at": "2022-08-19T16:09:09Z",
|
||||
"published_at": "2022-08-23T12:08:21Z",
|
||||
"last_comment_at": "2022-08-23T12:08:21Z",
|
||||
"reading_time_minutes": 1,
|
||||
"tag_list": ["discuss"],
|
||||
"tags": "discuss",
|
||||
"user": {
|
||||
"name": "Kyla \"Avery\" \\:/ Morissette",
|
||||
"name": "Angelena \"Charis\" \\:/ Lang",
|
||||
"username": "username372",
|
||||
"twitter_username": "twitter372",
|
||||
"github_username": "github372",
|
||||
"user_id": 3080,
|
||||
"user_id": 10747,
|
||||
"website_url": null,
|
||||
"profile_image": "/uploads/user/profile_image/3080/001a3d7d-c716-43aa-a46a-8c024888daf9.jpeg",
|
||||
"profile_image_90": "/uploads/user/profile_image/3080/001a3d7d-c716-43aa-a46a-8c024888daf9.jpeg"
|
||||
"profile_image": "/uploads/user/profile_image/10747/79dbb51d-a63a-4475-af29-a84fe417b9d8.jpeg",
|
||||
"profile_image_90": "/uploads/user/profile_image/10747/79dbb51d-a63a-4475-af29-a84fe417b9d8.jpeg"
|
||||
},
|
||||
"organization": {
|
||||
"name": "Koss and Sons",
|
||||
"name": "Vandervort-Sanford",
|
||||
"username": "org56",
|
||||
"slug": "org56",
|
||||
"profile_image": "/uploads/organization/profile_image/507/464b6b16-79e1-4378-a9fc-76c81403f9e1.png",
|
||||
"profile_image_90": "/uploads/organization/profile_image/507/464b6b16-79e1-4378-a9fc-76c81403f9e1.png"
|
||||
"profile_image": "/uploads/organization/profile_image/1275/45556538-13d9-44af-af7e-c9fcdbe2617c.png",
|
||||
"profile_image_90": "/uploads/organization/profile_image/1275/45556538-13d9-44af-af7e-c9fcdbe2617c.png"
|
||||
},
|
||||
"flare_tag": {
|
||||
"name": "discuss",
|
||||
|
|
@ -211,6 +211,69 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/reactions/toggle": {
|
||||
"post": {
|
||||
"summary": "create reaction",
|
||||
"tags": ["reactions"],
|
||||
"description": "This endpoint allows the client to toggle the user's reaction to a specified reactable (eg, Article, Comment, or User). For examples:\n * \"Like\"ing an Article will create a new \"like\" Reaction from the user for that Articles\n * \"Like\"ing that Article a second time will remove the \"like\" from the user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "category",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["like", "readinglist", "unicorn", "thinking", "hands"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reactable_id",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reactable_type",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["Comment", "Article", "User"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"result": "create",
|
||||
"category": "like",
|
||||
"id": 955,
|
||||
"reactable_id": 4457,
|
||||
"reactable_type": "Article"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"error": "unauthorized",
|
||||
"status": 401
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/users/{id}/unpublish": {
|
||||
"put": {
|
||||
"summary": "Unpublish a User's Articles and Comments",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue