Documentation for Negative Behavior APIs (#18187)

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
Josh Puetz 2022-07-28 14:30:32 -05:00 committed by GitHub
parent 582d78a7ff
commit 74e44aeb61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 392 additions and 38 deletions

View file

@ -112,7 +112,7 @@ module Api
payload = { action: "api_article_unpublish", article_id: @article.id }
Audit::Logger.log(:admin_api, @user, payload)
render head: :ok
render status: :no_content
else
render json: { message: @article.errors.full_messages }, status: :unprocessable_entity
end

View file

@ -36,7 +36,7 @@ module Api
payload = { action: "api_user_suspend", target_user_id: target_user.id }
Audit::Logger.log(:admin_api, @user, payload)
render head: :ok
render status: :no_content
rescue StandardError
render json: {
success: false,
@ -67,7 +67,7 @@ module Api
}
Audit::Logger.log(:admin_api, @user, payload)
render head: :ok
render status: :no_content
end
end
end

View file

@ -1217,7 +1217,7 @@ RSpec.describe "Api::V1::Articles", type: :request do
it "unpublishes an article" do
expect(published_article.published).to be true
put path, headers: v1_headers
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:no_content)
expect(published_article.reload.published).to be false
end
end
@ -1228,7 +1228,7 @@ RSpec.describe "Api::V1::Articles", type: :request do
it "unpublishes an article" do
expect(published_article.published).to be true
put path, headers: v1_headers
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:no_content)
expect(published_article.reload.published).to be false
end

View file

@ -1,23 +1,28 @@
require "rails_helper"
require "swagger_helper"
# rubocop:disable RSpec/EmptyExampleGroup
# rubocop:disable RSpec/VariableName
RSpec.describe "Api::V1::Docs::Articles", type: :request do
let(:organization) { create(:organization) } # not used by every spec but lower times overall
let(:tag) { create(:tag, :with_colors, name: "discuss") }
let(:article) { create(:article, featured: true, tags: "discuss") }
let(:article) { create(:article, featured: true, tags: "discuss", published: true) }
let(:unpublished_aricle) { create(:article, published: false) }
let(:api_secret) { create(:api_secret) }
let(:user) { api_secret.user }
let(:Accept) { "application/vnd.forem.api-v1+json" }
before do
stub_const("FlareTag::FLARE_TAG_IDS_HASH", { "discuss" => tag.id })
allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true)
end
# rubocop:disable RSpec/EmptyExampleGroup
describe "GET /articles" do
before do
article.update_columns(organization_id: organization.id)
end
# rubocop:disable RSpec/VariableName
path "/api/articles" do
get "Published articles" do
tags "articles"
@ -88,7 +93,7 @@ belonging to the requested collection, ordered by ascending publication date.",
example: 99
response "200", "A List of Articles" do
let(:"api-key") { "valid" }
let(:"api-key") { api_secret.secret }
schema type: :array,
items: { "$ref": "#/components/schemas/ArticleIndex" }
add_examples
@ -97,13 +102,76 @@ belonging to the requested collection, ordered by ascending publication date.",
end
response "401", "unauthorized" do
let(:Accept) { "application/vnd.forem.api-v1+json" }
let(:"api-key") { "invalid" }
run_test!
end
end
end
# rubocop:enable RSpec/VariableName
end
describe "PUT /articles/:id/unpublish" do
before do
user.add_role(:admin)
end
path "/api/articles/{id}/unpublish" do
put "Unpublish an article" do
tags "articles"
description "This endpoint allows the client to unpublish an article.
The user associated with the API key must have any 'admin' or 'moderator' role.
The article will be unpublished and will no longer be visible to the public. It will remain
in the database and will set back to draft status on the author's posts dashboard. Any
notifications associated with the article will be deleted. Any comments on the article
will remain."
operationId "unpublishArticle"
produces "application/json"
parameter name: :id, in: :path, required: true,
description: "The ID of the article to unpublish.",
schema: {
type: :integer,
format: :int32,
minimum: 1
},
example: 1
response "204", "Article successfully unpublished" do
let(:"api-key") { api_secret.secret }
let(:id) { article.id }
add_examples
run_test!
end
response "401", "Article already unpublished" do
let(:"api-key") { api_secret.secret }
let(:id) { unpublished_aricle.id }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:regular_user) { create(:user) }
let(:low_security_api_secret) { create(:api_secret, user: regular_user) }
let(:"api-key") { low_security_api_secret.secret }
let(:id) { unpublished_aricle.id }
add_examples
run_test!
end
response "404", "Article Not Found" do
let(:"api-key") { api_secret.secret }
let(:id) { 0 }
add_examples
run_test!
end
end
end
end
# rubocop:enable RSpec/VariableName
# rubocop:enable RSpec/EmptyExampleGroup
end

View file

@ -0,0 +1,139 @@
require "rails_helper"
require "swagger_helper"
# rubocop:disable RSpec/EmptyExampleGroup
# rubocop:disable RSpec/VariableName
RSpec.describe "Api::V1::Docs::Users", type: :request do
let(:Accept) { "application/vnd.forem.api-v1+json" }
let(:api_secret) { create(:api_secret) }
let(:user) { api_secret.user }
let(:banned_user) { create(:user) }
let(:article) { create(:article, user: banned_user, published: true) }
let(:comment) { create(:comment, user: banned_user, article: article) }
before do
allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true)
user.add_role(:admin)
end
describe "PUT /users/:id/unpublish" do
before do
user.add_role(:admin)
end
path "/api/users/{id}/unpublish" do
put "Unpublish a User's Articles and Comments" do
tags "users"
description "This endpoint allows the client to unpublish all of the articles and
comments created by a user.
The user associated with the API key must have any 'admin' or 'moderator' role.
This specified user's articles and comments will be unpublished and will no longer be
visible to the public. They will remain in the database and will set back to draft status
on the specified user's dashboard. Any notifications associated with the specified user's
articles and comments will be deleted.
Note this endpoint unpublishes articles and comments asychronously: it will return a 204 NO CONTENT
status code immediately, but the articles and comments will not be unpublished until the
request is completed on the server."
operationId "unpublishUser"
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: 1
response "204", "User's articles and comments successfully unpublished" do
let(:"api-key") { api_secret.secret }
let(:id) { banned_user.id }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:regular_user) { create(:user) }
let(:low_security_api_secret) { create(:api_secret, user: regular_user) }
let(:"api-key") { low_security_api_secret.secret }
let(:id) { banned_user.id }
add_examples
run_test!
end
response "404", "Unknown User ID (still accepted for async processing)" do
let(:"api-key") { api_secret.secret }
let(:id) { 10_000 }
add_examples
run_test!
end
end
end
end
describe "PUT /users/:id/suspend" do
before do
user.add_role(:admin)
end
path "/api/users/{id}/suspend" do
put "Suspend a User" do
tags "users"
description "This endpoint allows the client to suspend a user.
The user associated with the API key must have any 'admin' or 'moderator' role.
This specified user will be assigned the 'suspended' role. Suspending a user will stop the
user from posting new posts and comments. It doesn't delete any of the user's content, just
prevents them from creating new content while suspended. Users are not notified of their suspension
in the UI, so if you want them to know about this, you must notify them."
operationId "suspendUser"
produces "application/json"
parameter name: :id, in: :path, required: true,
description: "The ID of the user to suspend.",
schema: {
type: :integer,
format: :int32,
minimum: 1
},
example: 1
response "204", "User successfully unpublished" do
let(:"api-key") { api_secret.secret }
let(:id) { banned_user.id }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:regular_user) { create(:user) }
let(:low_security_api_secret) { create(:api_secret, user: regular_user) }
let(:"api-key") { low_security_api_secret.secret }
let(:id) { banned_user.id }
add_examples
run_test!
end
response "404", "Unknown User 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

View file

@ -190,7 +190,7 @@ RSpec.describe "Api::V0::Users", type: :request do
params: payload,
headers: v1_headers
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:no_content)
expect(target_user.reload.suspended?).to be true
expect(Note.last.content).to eq(payload[:note])
end.to change(Note, :count).by(1)
@ -255,7 +255,7 @@ RSpec.describe "Api::V0::Users", type: :request do
put api_user_unpublish_path(id: target_user.id),
headers: v1_headers
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:no_content)
sidekiq_perform_enqueued_jobs

View file

@ -102,44 +102,44 @@
"example": [
{
"type_of": "article",
"id": 286,
"title": "Terrible Swift Sword1",
"description": "Cardigan taxidermy stumptown muggle magic. Tumblr lumbersexual yolo gluten-free taxidermy truffaut...",
"readable_publish_date": "Jul 6",
"slug": "terrible-swift-sword1-4gja",
"path": "/username1/terrible-swift-sword1-4gja",
"url": "http://localhost:3000/username1/terrible-swift-sword1-4gja",
"id": 2661,
"title": "To a God Unknown172",
"description": "Thundercats kitsch park bespoke scenester cray irony occupy. Crucifix salvia vegan. Disrupt offal 3...",
"readable_publish_date": "Jul 27",
"slug": "to-a-god-unknown172-3lh1",
"path": "/username469/to-a-god-unknown172-3lh1",
"url": "http://localhost:3000/username469/to-a-god-unknown172-3lh1",
"comments_count": 0,
"public_reactions_count": 0,
"collection_id": null,
"published_timestamp": "2022-07-06T16:49:01Z",
"published_timestamp": "2022-07-27T20:31:54Z",
"positive_reactions_count": 0,
"cover_image": "http://localhost:3000/assets/12-f9d673ae4ff98002f782ab82c641f2f26673be728e8f5409bea83f2d1de15323.png",
"social_image": "http://localhost:3000/assets/12-f9d673ae4ff98002f782ab82c641f2f26673be728e8f5409bea83f2d1de15323.png",
"canonical_url": "http://localhost:3000/username1/terrible-swift-sword1-4gja",
"created_at": "2022-07-06T16:49:07Z",
"cover_image": "http://localhost:3000/assets/19-ed58d3e8defcefc445020631589697a05e725243e834b5192aee4e6b91a3e927.png",
"social_image": "http://localhost:3000/assets/19-ed58d3e8defcefc445020631589697a05e725243e834b5192aee4e6b91a3e927.png",
"canonical_url": "http://localhost:3000/username469/to-a-god-unknown172-3lh1",
"created_at": "2022-07-27T20:31:54Z",
"edited_at": null,
"crossposted_at": null,
"published_at": "2022-07-06T16:49:01Z",
"last_comment_at": "2022-07-06T16:49:01Z",
"published_at": "2022-07-27T20:31:54Z",
"last_comment_at": "2022-07-27T20:31:54Z",
"reading_time_minutes": 1,
"tag_list": ["discuss"],
"tags": "discuss",
"user": {
"name": "Malorie \"Herman\" \\:/ Trantow",
"username": "username1",
"twitter_username": "twitter1",
"github_username": "github1",
"name": "Carey \"Maya\" \\:/ Corwin",
"username": "username469",
"twitter_username": "twitter469",
"github_username": "github469",
"website_url": null,
"profile_image": "/uploads/user/profile_image/407/bcaaa39b-60d0-4dc2-97d2-9ec5c9e05b47.jpeg",
"profile_image_90": "/uploads/user/profile_image/407/bcaaa39b-60d0-4dc2-97d2-9ec5c9e05b47.jpeg"
"profile_image": "/uploads/user/profile_image/6332/5475485a-eb89-4e7f-b2d2-c548389462e8.jpeg",
"profile_image_90": "/uploads/user/profile_image/6332/5475485a-eb89-4e7f-b2d2-c548389462e8.jpeg"
},
"organization": {
"name": "Stamm Group",
"username": "org1",
"slug": "org1",
"profile_image": "/uploads/organization/profile_image/109/fb157fe5-9a66-4baf-91cb-216f0a206e5e.png",
"profile_image_90": "/uploads/organization/profile_image/109/fb157fe5-9a66-4baf-91cb-216f0a206e5e.png"
"name": "Blanda, Barton and Tremblay",
"username": "org56",
"slug": "org56",
"profile_image": "/uploads/organization/profile_image/934/ba061551-7cbe-4adc-80d0-bc60abcdaa32.png",
"profile_image_90": "/uploads/organization/profile_image/934/ba061551-7cbe-4adc-80d0-bc60abcdaa32.png"
},
"flare_tag": {
"name": "discuss",
@ -162,6 +162,153 @@
}
}
}
},
"/api/articles/{id}/unpublish": {
"put": {
"summary": "Unpublish an article",
"tags": ["articles"],
"description": "This endpoint allows the client to unpublish an article.\n\nThe user associated with the API key must have any 'admin' or 'moderator' role.\n\nThe article will be unpublished and will no longer be visible to the public. It will remain\nin the database and will set back to draft status on the author's posts dashboard. Any\nnotifications associated with the article will be deleted. Any comments on the article\nwill remain.",
"operationId": "unpublishArticle",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The ID of the article to unpublish.",
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1
},
"example": 1
}
],
"responses": {
"204": {
"description": "Article successfully unpublished"
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
},
"404": {
"description": "Article Not Found",
"content": {
"application/json": {
"example": {
"error": "not found",
"status": 404
}
}
}
}
}
}
},
"/api/users/{id}/unpublish": {
"put": {
"summary": "Unpublish a User's Articles and Comments",
"tags": ["users"],
"description": "This endpoint allows the client to unpublish all of the articles and\ncomments created by a user.\n\nThe user associated with the API key must have any 'admin' or 'moderator' role.\n\nThis specified user's articles and comments will be unpublished and will no longer be\nvisible to the public. They will remain in the database and will set back to draft status\non the specified user's dashboard. Any notifications associated with the specified user's\narticles and comments will be deleted.\n\nNote this endpoint unpublishes articles and comments asychronously: it will return a 204 NO CONTENT\nstatus code immediately, but the articles and comments will not be unpublished until the\nrequest is completed on the server.",
"operationId": "unpublishUser",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The ID of the user to unpublish.",
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1
},
"example": 1
}
],
"responses": {
"204": {
"description": "User's articles and comments successfully unpublished"
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
},
"404": {
"description": "Unknown User ID (still accepted for async processing)",
"content": {
"application/json": {
"example": {
"error": "not found",
"status": 404
}
}
}
}
}
}
},
"/api/users/{id}/suspend": {
"put": {
"summary": "Suspend a User",
"tags": ["users"],
"description": "This endpoint allows the client to suspend a user.\n\nThe user associated with the API key must have any 'admin' or 'moderator' role.\n\nThis specified user will be assigned the 'suspended' role. Suspending a user will stop the\nuser from posting new posts and comments. It doesn't delete any of the user's content, just\nprevents them from creating new content while suspended. Users are not notified of their suspension\nin the UI, so if you want them to know about this, you must notify them.",
"operationId": "suspendUser",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The ID of the user to suspend.",
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1
},
"example": 1
}
],
"responses": {
"204": {
"description": "User successfully unpublished"
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
},
"404": {
"description": "Unknown User ID",
"content": {
"application/json": {
"example": {
"error": "not found",
"status": 404
}
}
}
}
}
}
}
},
"servers": [