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:
Joshua Wehner 2022-08-24 17:02:34 +02:00 committed by GitHub
parent 69a08480a8
commit b6efce21fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 266 additions and 22 deletions

View 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

View file

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

View 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

View 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

View file

@ -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&amp;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",