From 7c82d128ffd26129e29be833617499b134045aa7 Mon Sep 17 00:00:00 2001 From: Joshua Wehner Date: Mon, 21 Nov 2022 09:31:01 +0100 Subject: [PATCH] DisplayAd API: Read endpoints (#18726) * DisplayAd API: Read endpoints * Updated swagger docs for display_ads read endpoints * Update swagger with type_of * Remove extraneous comment * Restrict authentication to strictly api_key --- .../api/v1/display_ads_controller.rb | 25 +++ config/routes.rb | 2 + spec/requests/api/v1/display_ads_spec.rb | 65 +++++++ spec/requests/api/v1/docs/display_ads_spec.rb | 91 ++++++++++ swagger/v1/api_v1.json | 169 ++++++++++++++---- 5 files changed, 322 insertions(+), 30 deletions(-) create mode 100644 app/controllers/api/v1/display_ads_controller.rb create mode 100644 spec/requests/api/v1/display_ads_spec.rb create mode 100644 spec/requests/api/v1/docs/display_ads_spec.rb diff --git a/app/controllers/api/v1/display_ads_controller.rb b/app/controllers/api/v1/display_ads_controller.rb new file mode 100644 index 000000000..7fe5bc287 --- /dev/null +++ b/app/controllers/api/v1/display_ads_controller.rb @@ -0,0 +1,25 @@ +module Api + module V1 + class DisplayAdsController < ApiController + before_action :authenticate_with_api_key! + before_action :require_admin + + def index + @display_ads = DisplayAd.order(id: :desc).page(params[:page]).per(50) + @display_ads = @display_ads.search_ads(params[:search]) if params[:search].present? + render json: @display_ads + end + + def show + @display_ad = DisplayAd.find(params[:id]) + render json: @display_ad + end + + private + + def require_admin + authorize DisplayAd, :access?, policy_class: InternalPolicy + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index fa5742a01..58b5bbb02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,6 +51,8 @@ Rails.application.routes.draw do post "/reactions", to: "reactions#create" post "/reactions/toggle", to: "reactions#toggle" + resources :display_ads, only: %i[index show] + draw :api end diff --git a/spec/requests/api/v1/display_ads_spec.rb b/spec/requests/api/v1/display_ads_spec.rb new file mode 100644 index 000000000..63776d9aa --- /dev/null +++ b/spec/requests/api/v1/display_ads_spec.rb @@ -0,0 +1,65 @@ +require "rails_helper" + +# rubocop:disable RSpec/InstanceVariable + +RSpec.describe "Api::V1::DisplayAds" do + let!(:v1_headers) { { "content-type" => "application/json", "Accept" => "application/vnd.forem.api-v1+json" } } + + before do + allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) + @ad1 = create(:display_ad, published: true, approved: true) + end + + 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 }) } + before { user.add_role(:admin) } + end + + context "when authenticated and authorized and get to index" do + include_context "when user is authorized" + + describe "GET /api/display_ads" do + it "returns json response" do + get api_display_ads_path, headers: auth_header + + expect(response).to have_http_status(:success) + expect(response.media_type).to eq("application/json") + expect(response.parsed_body.size).to eq(1) + end + end + + describe "GET /api/display_ads/:id" do + it "returns json response" do + get api_display_ad_path(@ad1.id), headers: auth_header + + expect(response).to have_http_status(:success) + expect(response.media_type).to eq("application/json") + expect(response.parsed_body).to include( + "id" => @ad1.id, + "published" => true, + "approved" => true, + "cached_tag_list" => "", + "clicks_count" => 0, + ) + end + end + end + + context "when unauthenticated and get to index" do + it "returns unauthorized" do + get api_display_ads_path, headers: v1_headers + expect(response).to have_http_status(:unauthorized) + end + end + + context "when unauthorized and get to show" do + it "returns unauthorized" do + get api_display_ads_path, params: { id: @ad1.id }, + headers: v1_headers.merge({ "api-key" => "invalid api key" }) + expect(response).to have_http_status(:unauthorized) + end + end +end +# rubocop:enable RSpec/InstanceVariable diff --git a/spec/requests/api/v1/docs/display_ads_spec.rb b/spec/requests/api/v1/docs/display_ads_spec.rb new file mode 100644 index 000000000..4f260aa22 --- /dev/null +++ b/spec/requests/api/v1/docs/display_ads_spec.rb @@ -0,0 +1,91 @@ +require "rails_helper" +require "swagger_helper" + +# rubocop:disable RSpec/EmptyExampleGroup +# rubocop:disable RSpec/VariableName + +RSpec.describe "api/v1/display_ads" do + let(:Accept) { "application/vnd.forem.api-v1+json" } + let(:api_secret) { create(:api_secret) } + let(:display_ad) { create(:display_ad) } + let(:user) { api_secret.user } + + before { user.add_role(:admin) } + + path "/api/display_ads" do + describe "get all display ads" do + get("display ads") do + tags "display ads" + description(<<-DESCRIBE.strip) + This endpoint allows the client to retrieve a list of all display ads. + DESCRIBE + + produces "application/json" + + 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 + + path "/api/display_ads/{id}" do + describe "GET a single display ad" do + get("display ad") do + tags "display ads" + description(<<-DESCRIBE.strip) + This endpoint allows the client to retrieve a single display ad, via its id. + DESCRIBE + + produces "application/json" + parameter name: :id, + in: :path, + required: true, + description: "The ID of the user to unpublish.", + schema: { + type: :integer, + format: :int32, + minimum: 1 + }, + example: 123 + + response(200, "successful") do + let(:id) { display_ad.id } + let(:"api-key") { api_secret.secret } + add_examples + + run_test! + end + + response "401", "unauthorized" do + let(:"api-key") { "invalid" } + let(:id) { 10_000 } + add_examples + + run_test! + end + + response "404", "Unknown DisplayAd ID" do + let(:"api-key") { api_secret.secret } + let(:id) { 10_000 } + add_examples + + run_test! + end + end + end + end +end + +# rubocop:enable RSpec/VariableName +# rubocop:enable RSpec/EmptyExampleGroup diff --git a/swagger/v1/api_v1.json b/swagger/v1/api_v1.json index 3ddf26ce8..21e6316fc 100644 --- a/swagger/v1/api_v1.json +++ b/swagger/v1/api_v1.json @@ -103,45 +103,45 @@ "example": [ { "type_of": "article", - "id": 1, - "title": "Mr Standfast1", - "description": "Fixie crucifix vegan vinyl cleanse stumptown. Ennui chicharrones actually heirloom taxidermy selfies...", - "readable_publish_date": "Sep 3", - "slug": "mr-standfast1-o1j", - "path": "/username1/mr-standfast1-o1j", - "url": "http://localhost:3000/username1/mr-standfast1-o1j", + "id": 191, + "title": "An Acceptable Time172", + "description": "Shabby chic wayfarers normcore marfa carry. Schlitz kinfolk +1 kickstarter pitchfork cardigan 90's....", + "readable_publish_date": "Nov 17", + "slug": "an-acceptable-time172-25ik", + "path": "/username378/an-acceptable-time172-25ik", + "url": "http://localhost:3000/username378/an-acceptable-time172-25ik", "comments_count": 0, "public_reactions_count": 0, "collection_id": null, - "published_timestamp": "2022-09-02T19:24:23Z", + "published_timestamp": "2022-11-17T09:30:31Z", "positive_reactions_count": 0, - "cover_image": "http://localhost:3000/assets/19-ed58d3e8defcefc445020631589697a05e725243e834b5192aee4e6b91a3e927.png", - "social_image": "http://localhost:3000/assets/19-ed58d3e8defcefc445020631589697a05e725243e834b5192aee4e6b91a3e927.png", - "canonical_url": "http://localhost:3000/username1/mr-standfast1-o1j", - "created_at": "2022-09-02T19:24:24Z", + "cover_image": "http://localhost:3000/assets/23-33ea5a2b5af3dc15b9ed90de0b850f67e2390eaec3361d6687d3b9750c699f84.png", + "social_image": "http://localhost:3000/assets/23-33ea5a2b5af3dc15b9ed90de0b850f67e2390eaec3361d6687d3b9750c699f84.png", + "canonical_url": "http://localhost:3000/username378/an-acceptable-time172-25ik", + "created_at": "2022-11-17T09:30:31Z", "edited_at": null, "crossposted_at": null, - "published_at": "2022-09-02T19:24:23Z", - "last_comment_at": "2022-09-02T19:24:23Z", + "published_at": "2022-11-17T09:30:31Z", + "last_comment_at": "2022-11-17T09:30:31Z", "reading_time_minutes": 1, "tag_list": ["discuss"], "tags": "discuss", "user": { - "name": "Lavona \"Lenna\" \\:/ Watsica", - "username": "username1", - "twitter_username": "twitter1", - "github_username": "github1", - "user_id": 1, + "name": "Melaine \"Quinn\" \\:/ Goodwin", + "username": "username378", + "twitter_username": "twitter378", + "github_username": "github378", + "user_id": 386, "website_url": null, - "profile_image": "/uploads/user/profile_image/1/4e030890-caa7-4c5b-836a-432e7b1bfdfc.jpeg", - "profile_image_90": "/uploads/user/profile_image/1/4e030890-caa7-4c5b-836a-432e7b1bfdfc.jpeg" + "profile_image": "/uploads/user/profile_image/386/93f316c4-1287-45b3-b1c7-49861128e477.jpeg", + "profile_image_90": "/uploads/user/profile_image/386/93f316c4-1287-45b3-b1c7-49861128e477.jpeg" }, "organization": { - "name": "Weimann-Toy", - "username": "org1", - "slug": "org1", - "profile_image": "/uploads/organization/profile_image/1/2df76e17-f420-434a-8027-2e90ebfb89bf.png", - "profile_image_90": "/uploads/organization/profile_image/1/2df76e17-f420-434a-8027-2e90ebfb89bf.png" + "name": "Baumbach Group", + "username": "org60", + "slug": "org60", + "profile_image": "/uploads/organization/profile_image/65/0bccbabf-3f76-473b-8c5e-bac56178315b.png", + "profile_image_90": "/uploads/organization/profile_image/65/0bccbabf-3f76-473b-8c5e-bac56178315b.png" }, "flare_tag": { "name": "discuss", @@ -180,6 +180,16 @@ "minimum": 1 }, "example": 1 + }, + { + "name": "note", + "in": "query", + "required": false, + "description": "Content for the note that's created along with unpublishing", + "schema": { + "type": "string" + }, + "example": "Admin requested unpublishing all articles via API" } ], "responses": { @@ -211,6 +221,105 @@ } } }, + "/api/display_ads": { + "get": { + "summary": "display ads", + "tags": ["display ads"], + "description": "This endpoint allows the client to retrieve a list of all display ads.", + "responses": { + "200": { + "description": "successful", + "content": { + "application/json": { + "example": [] + } + } + }, + "401": { + "description": "unauthorized", + "content": { + "application/json": { + "example": { + "error": "unauthorized", + "status": 401 + } + } + } + } + } + } + }, + "/api/display_ads/{id}": { + "get": { + "summary": "display ad", + "tags": ["display ads"], + "description": "This endpoint allows the client to retrieve a single display ad, via its id.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The ID of the user to unpublish.", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + }, + "example": 123 + } + ], + "responses": { + "200": { + "description": "successful", + "content": { + "application/json": { + "example": { + "id": 10, + "approved": false, + "body_markdown": "Hello _hey_ Hey hey 5", + "cached_tag_list": "", + "clicks_count": 0, + "created_at": "2022-11-17T10:30:32.066+01:00", + "display_to": "all", + "impressions_count": 0, + "name": "Display Ad 10", + "organization_id": 66, + "placement_area": "sidebar_left", + "processed_html": "

Hello hey Hey hey 5

", + "published": false, + "success_rate": 0.0, + "type_of": "in_house", + "updated_at": "2022-11-17T10:30:32.069+01:00", + "tag_list": [] + } + } + } + }, + "401": { + "description": "unauthorized", + "content": { + "application/json": { + "example": { + "error": "unauthorized", + "status": 401 + } + } + } + }, + "404": { + "description": "Unknown DisplayAd ID", + "content": { + "application/json": { + "example": { + "error": "not found", + "status": 404 + } + } + } + } + } + } + }, "/api/reactions/toggle": { "post": { "summary": "toggle reaction", @@ -223,7 +332,7 @@ "required": true, "schema": { "type": "string", - "enum": ["like", "readinglist", "unicorn", "thinking", "hands"] + "enum": ["like", "readinglist", "unicorn"] } }, { @@ -254,7 +363,7 @@ "result": "create", "category": "like", "id": 1, - "reactable_id": 5, + "reactable_id": 195, "reactable_type": "Article" } } @@ -286,7 +395,7 @@ "required": true, "schema": { "type": "string", - "enum": ["like", "readinglist", "unicorn", "thinking", "hands"] + "enum": ["like", "readinglist", "unicorn"] } }, { @@ -317,7 +426,7 @@ "result": "none", "category": "like", "id": 3, - "reactable_id": 7, + "reactable_id": 197, "reactable_type": "Article" } }