mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-29 10:06:13 +01:00
fix(writeapi): tests
This commit is contained in:
@@ -2,117 +2,4 @@ PostsObject:
|
|||||||
description: One of the objects in the array returned from `Posts.getPostSummaryByPids`
|
description: One of the objects in the array returned from `Posts.getPostSummaryByPids`
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
$ref: ./PostObject.yaml#/PostObject
|
||||||
properties:
|
|
||||||
pid:
|
|
||||||
type: number
|
|
||||||
tid:
|
|
||||||
type: number
|
|
||||||
description: A topic identifier
|
|
||||||
content:
|
|
||||||
type: string
|
|
||||||
uid:
|
|
||||||
type: number
|
|
||||||
description: A user identifier
|
|
||||||
timestamp:
|
|
||||||
type: number
|
|
||||||
deleted:
|
|
||||||
type: boolean
|
|
||||||
upvotes:
|
|
||||||
type: number
|
|
||||||
downvotes:
|
|
||||||
type: number
|
|
||||||
votes:
|
|
||||||
type: number
|
|
||||||
timestampISO:
|
|
||||||
type: string
|
|
||||||
description: An ISO 8601 formatted date string (complementing `timestamp`)
|
|
||||||
user:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
uid:
|
|
||||||
type: number
|
|
||||||
description: A user identifier
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: A friendly name for a given user account
|
|
||||||
userslug:
|
|
||||||
type: string
|
|
||||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces
|
|
||||||
removed, etc.)
|
|
||||||
picture:
|
|
||||||
type: string
|
|
||||||
nullable: true
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
icon:text:
|
|
||||||
type: string
|
|
||||||
description: A single-letter representation of a username. This is used in the
|
|
||||||
auto-generated icon given to users without
|
|
||||||
an avatar
|
|
||||||
icon:bgColor:
|
|
||||||
type: string
|
|
||||||
description: A six-character hexadecimal colour code assigned to the user. This
|
|
||||||
value is used in conjunction with
|
|
||||||
`icon:text` for the user's auto-generated
|
|
||||||
icon
|
|
||||||
example: "#f44336"
|
|
||||||
topic:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
uid:
|
|
||||||
type: number
|
|
||||||
description: A user identifier
|
|
||||||
tid:
|
|
||||||
type: number
|
|
||||||
description: A topic identifier
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
cid:
|
|
||||||
type: number
|
|
||||||
description: A category identifier
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
deleted:
|
|
||||||
type: number
|
|
||||||
postcount:
|
|
||||||
type: number
|
|
||||||
mainPid:
|
|
||||||
type: number
|
|
||||||
description: The post id of the first post in this topic (also called the
|
|
||||||
"original post")
|
|
||||||
teaserPid:
|
|
||||||
type: number
|
|
||||||
description: The post id of the teaser (the most recent post, depending on settings)
|
|
||||||
nullable: true
|
|
||||||
titleRaw:
|
|
||||||
type: string
|
|
||||||
category:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
cid:
|
|
||||||
type: number
|
|
||||||
description: A category identifier
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
icon:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
parentCid:
|
|
||||||
type: number
|
|
||||||
description: The category identifier for the category that is the immediate
|
|
||||||
ancestor of the current category
|
|
||||||
bgColor:
|
|
||||||
type: string
|
|
||||||
color:
|
|
||||||
type: string
|
|
||||||
backgroundImage:
|
|
||||||
nullable: true
|
|
||||||
imageClass:
|
|
||||||
nullable: true
|
|
||||||
type: string
|
|
||||||
isMainPost:
|
|
||||||
type: boolean
|
|
||||||
replies:
|
|
||||||
type: number
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
UserRequest:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
example: Dragon Fruit
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
example: dragonfruit@example.org
|
|
||||||
fullname:
|
|
||||||
type: string
|
|
||||||
example: Mr. Dragon Fruit Jr.
|
|
||||||
website:
|
|
||||||
type: string
|
|
||||||
example: 'https://example.org'
|
|
||||||
location:
|
|
||||||
type: string
|
|
||||||
example: 'Toronto, Canada'
|
|
||||||
groupTitle:
|
|
||||||
type: string
|
|
||||||
example: '["administrators","Staff"]'
|
|
||||||
birthday:
|
|
||||||
type: string
|
|
||||||
description: A birthdate given in an ISO format parseable by the Date object
|
|
||||||
example: 03/27/2020
|
|
||||||
signature:
|
|
||||||
type: string
|
|
||||||
example: |
|
|
||||||
This is an example signature
|
|
||||||
It can span multiple lines.
|
|
||||||
aboutme:
|
|
||||||
type: string
|
|
||||||
example: |
|
|
||||||
This is a paragraph all about how my life got twist-turned upside-down
|
|
||||||
and I'd like to take a minute and sit right here,
|
|
||||||
to tell you all about how I because the administrator of NodeBB
|
|
||||||
@@ -28,6 +28,8 @@ tags:
|
|||||||
- name: categories
|
- name: categories
|
||||||
description: Administrative calls to manage categories
|
description: Administrative calls to manage categories
|
||||||
paths:
|
paths:
|
||||||
|
/users/:
|
||||||
|
$ref: 'write/users.yaml'
|
||||||
/users/{uid}:
|
/users/{uid}:
|
||||||
$ref: 'write/users/uid.yaml'
|
$ref: 'write/users/uid.yaml'
|
||||||
/users/{uid}/settings:
|
/users/{uid}/settings:
|
||||||
@@ -40,12 +42,14 @@ paths:
|
|||||||
$ref: 'write/users/uid/follow.yaml'
|
$ref: 'write/users/uid/follow.yaml'
|
||||||
/users/{uid}/ban:
|
/users/{uid}/ban:
|
||||||
$ref: 'write/users/uid/ban.yaml'
|
$ref: 'write/users/uid/ban.yaml'
|
||||||
/users/{uid}/tokens:
|
/users/{uid}/tokens/{token}:
|
||||||
$ref: 'write/users/uid/tokens.yaml'
|
$ref: 'write/users/uid/tokens/token.yaml'
|
||||||
/categories/:
|
/categories/:
|
||||||
$ref: 'write/categories.yaml'
|
$ref: 'write/categories.yaml'
|
||||||
/groups/:
|
/groups/:
|
||||||
$ref: 'write/groups.yaml'
|
$ref: 'write/groups.yaml'
|
||||||
|
/groups/{slug}:
|
||||||
|
$ref: 'write/groups/slug.yaml'
|
||||||
/groups/{slug}/membership/{uid}:
|
/groups/{slug}/membership/{uid}:
|
||||||
$ref: 'write/groups/slug/membership/uid.yaml'
|
$ref: 'write/groups/slug/membership/uid.yaml'
|
||||||
/topics:
|
/topics:
|
||||||
|
|||||||
@@ -12,38 +12,37 @@ post:
|
|||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
example: My New Category
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
|
example: Lorem ipsum, dolor sit amet
|
||||||
parentCid:
|
parentCid:
|
||||||
type: number
|
type: number
|
||||||
|
example: 0
|
||||||
cloneFromCid:
|
cloneFromCid:
|
||||||
type: number
|
type: number
|
||||||
|
example: 0
|
||||||
icon:
|
icon:
|
||||||
type: string
|
type: string
|
||||||
|
example: bullhorn
|
||||||
description: A ForkAwesome icon without the `fa-` prefix
|
description: A ForkAwesome icon without the `fa-` prefix
|
||||||
bgColor:
|
bgColor:
|
||||||
type: string
|
type: string
|
||||||
|
example: '#ffffff'
|
||||||
color:
|
color:
|
||||||
type: string
|
type: string
|
||||||
|
example: '#000000'
|
||||||
link:
|
link:
|
||||||
type: string
|
type: string
|
||||||
|
example: 'https://example.org'
|
||||||
class:
|
class:
|
||||||
type: string
|
type: string
|
||||||
|
example: 'col-md-3 col-xs-6'
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
type: string
|
type: string
|
||||||
|
example: '/assets/relative/path/to/image'
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
example:
|
|
||||||
name: My New Category
|
|
||||||
description: Lorem ipsum, dolor sit amet
|
|
||||||
parentCid: 0
|
|
||||||
cloneFromCid: 0
|
|
||||||
icon: bullhorn
|
|
||||||
bgColor: '#ffffff'
|
|
||||||
color: '#000000'
|
|
||||||
link: 'https://example.org'
|
|
||||||
class: 'col-md-3 col-xs-6'
|
|
||||||
backgroundImage: '/assets/relative/path/to/image'
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: category successfully created
|
description: category successfully created
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ post:
|
|||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
example: 'My Test Group'
|
||||||
timestamp:
|
timestamp:
|
||||||
type: number
|
type: number
|
||||||
disableJoinRequests:
|
disableJoinRequests:
|
||||||
@@ -23,6 +24,7 @@ post:
|
|||||||
hidden:
|
hidden:
|
||||||
type: number
|
type: number
|
||||||
enum: [0, 1]
|
enum: [0, 1]
|
||||||
|
example: 1
|
||||||
ownerUid:
|
ownerUid:
|
||||||
type: number
|
type: number
|
||||||
private:
|
private:
|
||||||
@@ -37,9 +39,6 @@ post:
|
|||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
example:
|
|
||||||
name: 'My Test Group'
|
|
||||||
hidden: 1
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: group successfully created
|
description: group successfully created
|
||||||
@@ -51,22 +50,4 @@ post:
|
|||||||
status:
|
status:
|
||||||
$ref: ../components/schemas/Status.yaml#/Status
|
$ref: ../components/schemas/Status.yaml#/Status
|
||||||
response:
|
response:
|
||||||
$ref: ../components/schemas/GroupObject.yaml#/GroupDataObject
|
$ref: ../components/schemas/GroupObject.yaml#/GroupDataObject
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- groups
|
|
||||||
summary: Delete an existing group
|
|
||||||
description: This operation deletes an existing group, all members within this group will cease to be members after the group is deleted.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: group successfully deleted
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
$ref: ../components/schemas/Status.yaml#/Status
|
|
||||||
response:
|
|
||||||
type: object
|
|
||||||
properties: {}
|
|
||||||
@@ -10,10 +10,50 @@ put:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
description: slug of the group you would like to join
|
description: slug of the group you would like to join
|
||||||
example: my-group
|
example: test-group
|
||||||
|
- in: path
|
||||||
|
name: uid
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
required: true
|
||||||
|
description: uid of the user to join the group
|
||||||
|
example: 1
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: group successfully joined, or membership requested
|
description: group successfully joined, or membership requested
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||||
|
response:
|
||||||
|
type: object
|
||||||
|
properties: {}
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- groups
|
||||||
|
summary: leave a group
|
||||||
|
description: This operation leaves a group.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: slug
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: slug of the group you would like to leave
|
||||||
|
example: test-group
|
||||||
|
- in: path
|
||||||
|
name: uid
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
required: true
|
||||||
|
description: uid of the user to leave the group
|
||||||
|
example: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: group successfully left
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ put:
|
|||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
description: New post content
|
description: New post content
|
||||||
|
example: New post content
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
description: Topic title, only accepted for main posts
|
description: Topic title, only accepted for main posts
|
||||||
|
example: New title
|
||||||
required:
|
required:
|
||||||
- content
|
- content
|
||||||
example:
|
|
||||||
content: 'New post content'
|
|
||||||
title: 'New title'
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Post successfully edited
|
description: Post successfully edited
|
||||||
@@ -40,7 +39,7 @@ put:
|
|||||||
status:
|
status:
|
||||||
$ref: ../../components/schemas/Status.yaml#/Status
|
$ref: ../../components/schemas/Status.yaml#/Status
|
||||||
response:
|
response:
|
||||||
$ref: ../../components/schemas/PostsObject.yaml#/PostsObject
|
$ref: ../../components/schemas/PostObject.yaml#/PostObject
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- posts
|
- posts
|
||||||
|
|||||||
@@ -12,22 +12,22 @@ post:
|
|||||||
properties:
|
properties:
|
||||||
cid:
|
cid:
|
||||||
type: number
|
type: number
|
||||||
|
example: 1
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
|
example: Test topic
|
||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
|
example: This is the test topic's content
|
||||||
tags:
|
tags:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
example: [test, topic]
|
||||||
required:
|
required:
|
||||||
- cid
|
- cid
|
||||||
- title
|
- title
|
||||||
- content
|
- content
|
||||||
example:
|
|
||||||
cid: 1
|
|
||||||
title: Test topic
|
|
||||||
content: This is the test topic's content
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: topic successfully created
|
description: topic successfully created
|
||||||
@@ -39,4 +39,8 @@ post:
|
|||||||
status:
|
status:
|
||||||
$ref: ../components/schemas/Status.yaml#/Status
|
$ref: ../components/schemas/Status.yaml#/Status
|
||||||
response:
|
response:
|
||||||
$ref: ../components/schemas/TopicObject.yaml#/TopicObject
|
allOf:
|
||||||
|
- $ref: ../components/schemas/TopicObject.yaml#/TopicObject
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
mainPost: {}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- topics
|
- topics
|
||||||
summary: peply to a topic
|
summary: reply to a topic
|
||||||
description: This operation creates a new reply to an existing topic.
|
description: This operation creates a new reply to an existing topic.
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
@@ -10,7 +10,7 @@ post:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
description: a valid topic id
|
description: a valid topic id
|
||||||
example: 1
|
example: 2
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -20,14 +20,13 @@ post:
|
|||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
|
example: This is a test reply
|
||||||
timestamp:
|
timestamp:
|
||||||
type: number
|
type: number
|
||||||
toPid:
|
toPid:
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- content
|
- content
|
||||||
example:
|
|
||||||
content: This is a test reply
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: post successfully created
|
description: post successfully created
|
||||||
@@ -39,7 +38,7 @@ post:
|
|||||||
status:
|
status:
|
||||||
$ref: ../../components/schemas/Status.yaml#/Status
|
$ref: ../../components/schemas/Status.yaml#/Status
|
||||||
response:
|
response:
|
||||||
$ref: ../../components/schemas/PostsObject.yaml#/PostsObject
|
$ref: ../../components/schemas/PostObject.yaml#/PostObject
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- topics
|
- topics
|
||||||
@@ -52,7 +51,7 @@ delete:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
description: a valid topic id
|
description: a valid topic id
|
||||||
example: 1
|
example: 2
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Topic successfully purged
|
description: Topic successfully purged
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ put:
|
|||||||
description: 'An array of tags'
|
description: 'An array of tags'
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
example:
|
example: [test, foobar]
|
||||||
tags:
|
|
||||||
- test
|
|
||||||
- foobar
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Topic tags successfully added
|
description: Topic tags successfully added
|
||||||
|
|||||||
@@ -13,16 +13,15 @@ post:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
description: 'If the username is taken, a number will be appended'
|
description: 'If the username is taken, a number will be appended'
|
||||||
|
example: Dragon Fruit
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
|
example: s3cre7password
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
example: dragonfruit@example.org
|
||||||
required:
|
required:
|
||||||
- username
|
- username
|
||||||
example:
|
|
||||||
username: Dragon Fruit
|
|
||||||
password: s3cre7password
|
|
||||||
email: dragonfruit@example.org
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: user successfully created
|
description: user successfully created
|
||||||
@@ -62,11 +61,7 @@ delete:
|
|||||||
description: A collection of uids
|
description: A collection of uids
|
||||||
items:
|
items:
|
||||||
type: number
|
type: number
|
||||||
example:
|
example: [5, 6]
|
||||||
uids:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: user account(s) deleted
|
description: user account(s) deleted
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ delete:
|
|||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: uid of the user to delete
|
description: uid of the user to delete
|
||||||
example: 1
|
example: 3
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: user account deleted
|
description: user account deleted
|
||||||
@@ -39,7 +39,35 @@ put:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: ../../components/schemas/UserRequest.yaml#/UserRequest
|
type: object
|
||||||
|
properties:
|
||||||
|
fullname:
|
||||||
|
type: string
|
||||||
|
example: Mr. Dragon Fruit Jr.
|
||||||
|
website:
|
||||||
|
type: string
|
||||||
|
example: 'https://example.org'
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
example: 'Toronto, Canada'
|
||||||
|
groupTitle:
|
||||||
|
type: string
|
||||||
|
example: '["administrators","Staff"]'
|
||||||
|
birthday:
|
||||||
|
type: string
|
||||||
|
description: A birthdate given in an ISO format parseable by the Date object
|
||||||
|
example: 03/27/2020
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
example: |
|
||||||
|
This is an example signature
|
||||||
|
It can span multiple lines.
|
||||||
|
aboutme:
|
||||||
|
type: string
|
||||||
|
example: |
|
||||||
|
This is a paragraph all about how my life got twist-turned upside-down
|
||||||
|
and I'd like to take a minute and sit right here,
|
||||||
|
to tell you all about how I because the administrator of NodeBB
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: user profile updated
|
description: user profile updated
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ put:
|
|||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: uid of the user to ban
|
description: uid of the user to ban
|
||||||
example: 1
|
example: 2
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -46,7 +46,7 @@ delete:
|
|||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: uid of the user to unban
|
description: uid of the user to unban
|
||||||
example: 1
|
example: 2
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successfully unbanned user
|
description: successfully unbanned user
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
post:
|
put:
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: follow a user
|
summary: follow a user
|
||||||
@@ -9,7 +9,7 @@ post:
|
|||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: uid of the user to follow
|
description: uid of the user to follow
|
||||||
example: 1
|
example: 2
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successfully followed user
|
description: successfully followed user
|
||||||
@@ -33,7 +33,7 @@ delete:
|
|||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: uid of the user to unfollow
|
description: uid of the user to unfollow
|
||||||
example: 1
|
example: 2
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successfully unfollowed user
|
description: successfully unfollowed user
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ put:
|
|||||||
currentPassword:
|
currentPassword:
|
||||||
type: string
|
type: string
|
||||||
description: test
|
description: test
|
||||||
example: oldp455word
|
example: '123456'
|
||||||
newPassword:
|
newPassword:
|
||||||
type: string
|
type: string
|
||||||
example: s3cre7password
|
example: '123456'
|
||||||
required:
|
required:
|
||||||
- newPassword
|
- newPassword
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@@ -3,33 +3,17 @@ post:
|
|||||||
- users
|
- users
|
||||||
summary: generate a user token
|
summary: generate a user token
|
||||||
description: This route can only be used to generate tokens for the same user. In other words, you cannot use this route to generate a token for a different user than the one you are authenticated as.
|
description: This route can only be used to generate tokens for the same user. In other words, you cannot use this route to generate a token for a different user than the one you are authenticated as.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: uid
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
required: true
|
||||||
|
description: uid of the user to generate a token for
|
||||||
|
example: 1
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successfully generated a user token
|
description: successfully generated a user token
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
|
||||||
response:
|
|
||||||
type: object
|
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- users
|
|
||||||
summary: delete user token
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: token
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
description: a valid API token
|
|
||||||
example: 6d03a630-86fd-4515-9a35-e957502f4f89
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successfully deleted user token
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
249
test/api.js
249
test/api.js
@@ -10,6 +10,7 @@ const wait = util.promisify(setTimeout);
|
|||||||
|
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
|
const meta = require('../src/meta');
|
||||||
const user = require('../src/user');
|
const user = require('../src/user');
|
||||||
const groups = require('../src/groups');
|
const groups = require('../src/groups');
|
||||||
const categories = require('../src/categories');
|
const categories = require('../src/categories');
|
||||||
@@ -17,7 +18,7 @@ const topics = require('../src/topics');
|
|||||||
const plugins = require('../src/plugins');
|
const plugins = require('../src/plugins');
|
||||||
const flags = require('../src/flags');
|
const flags = require('../src/flags');
|
||||||
const messaging = require('../src/messaging');
|
const messaging = require('../src/messaging');
|
||||||
|
const utils = require('../src/utils');
|
||||||
|
|
||||||
describe('Read API', async () => {
|
describe('Read API', async () => {
|
||||||
let readApi = false;
|
let readApi = false;
|
||||||
@@ -25,9 +26,30 @@ describe('Read API', async () => {
|
|||||||
const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
||||||
const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml');
|
const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml');
|
||||||
let jar;
|
let jar;
|
||||||
|
let csrfToken;
|
||||||
let setup = false;
|
let setup = false;
|
||||||
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user
|
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
get: {},
|
||||||
|
post: {},
|
||||||
|
put: {},
|
||||||
|
delete: {
|
||||||
|
'/users/{uid}/tokens/{token}': [
|
||||||
|
{
|
||||||
|
in: 'path',
|
||||||
|
name: 'uid',
|
||||||
|
example: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 'path',
|
||||||
|
name: 'token',
|
||||||
|
example: utils.generateUUID(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
async function dummySearchHook(data) {
|
async function dummySearchHook(data) {
|
||||||
return [1];
|
return [1];
|
||||||
}
|
}
|
||||||
@@ -44,8 +66,26 @@ describe('Read API', async () => {
|
|||||||
// Create sample users
|
// Create sample users
|
||||||
const adminUid = await user.create({ username: 'admin', password: '123456', email: 'test@example.org' });
|
const adminUid = await user.create({ username: 'admin', password: '123456', email: 'test@example.org' });
|
||||||
const unprivUid = await user.create({ username: 'unpriv', password: '123456', email: 'unpriv@example.org' });
|
const unprivUid = await user.create({ username: 'unpriv', password: '123456', email: 'unpriv@example.org' });
|
||||||
|
for (let x = 0; x < 3; x++) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await user.create({ username: 'deleteme', password: '123456' }); // for testing of user deletion routes (uids 4-6)
|
||||||
|
}
|
||||||
await groups.join('administrators', adminUid);
|
await groups.join('administrators', adminUid);
|
||||||
|
|
||||||
|
// Create sample group
|
||||||
|
await groups.create({
|
||||||
|
name: 'Test Group',
|
||||||
|
});
|
||||||
|
|
||||||
|
await meta.settings.set('core.api', {
|
||||||
|
tokens: [{
|
||||||
|
token: mocks.delete['/users/{uid}/tokens/{token}'][1].example,
|
||||||
|
uid: 1,
|
||||||
|
description: 'for testing of token deletion rotue',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
// Create a category
|
// Create a category
|
||||||
const testCategory = await categories.create({ name: 'test' });
|
const testCategory = await categories.create({ name: 'test' });
|
||||||
|
|
||||||
@@ -78,6 +118,15 @@ describe('Read API', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
jar = await helpers.loginUser('admin', '123456');
|
jar = await helpers.loginUser('admin', '123456');
|
||||||
|
|
||||||
|
// Retrieve CSRF token using cookie, to test Write API
|
||||||
|
const config = await request({
|
||||||
|
url: nconf.get('url') + '/api/config',
|
||||||
|
json: true,
|
||||||
|
jar: jar,
|
||||||
|
});
|
||||||
|
csrfToken = config.csrf_token;
|
||||||
|
|
||||||
setup = true;
|
setup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,87 +142,133 @@ describe('Read API', async () => {
|
|||||||
readApi = await SwaggerParser.dereference(readApiPath);
|
readApi = await SwaggerParser.dereference(readApiPath);
|
||||||
writeApi = await SwaggerParser.dereference(writeApiPath);
|
writeApi = await SwaggerParser.dereference(writeApiPath);
|
||||||
|
|
||||||
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
|
// generateTests(readApi, Object.keys(readApi.paths));
|
||||||
// const paths = Object.keys(writeApi.paths);
|
generateTests(writeApi, Object.keys(writeApi.paths), writeApi.servers[0].url);
|
||||||
const paths = Object.keys(readApi.paths);
|
|
||||||
|
|
||||||
paths.forEach((path) => {
|
function generateTests(api, paths, prefix) {
|
||||||
// const context = writeApi.paths[path];
|
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
|
||||||
const context = readApi.paths[path];
|
paths.forEach((path) => {
|
||||||
let schema;
|
const context = api.paths[path];
|
||||||
let response;
|
let schema;
|
||||||
let url;
|
let response;
|
||||||
let method;
|
let url;
|
||||||
const headers = {};
|
let method;
|
||||||
const qs = {};
|
const headers = {};
|
||||||
|
const qs = {};
|
||||||
|
|
||||||
Object.keys(context).forEach((_method) => {
|
Object.keys(context).forEach((_method) => {
|
||||||
if (_method !== 'get') {
|
// if (_method !== 'get') {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
it('should have examples when parameters are present', () => {
|
it('should have examples when parameters are present', () => {
|
||||||
method = _method;
|
method = _method;
|
||||||
const parameters = context[method].parameters;
|
let parameters = context[method].parameters;
|
||||||
let testPath = path;
|
let testPath = path;
|
||||||
|
|
||||||
if (parameters) {
|
if (parameters) {
|
||||||
parameters.forEach((param) => {
|
// Use mock data if provided
|
||||||
assert(param.example !== null && param.example !== undefined, `${method.toUpperCase()} ${path} has parameters without examples`);
|
parameters = mocks[method][path] || parameters;
|
||||||
|
|
||||||
switch (param.in) {
|
parameters.forEach((param) => {
|
||||||
case 'path':
|
assert(param.example !== null && param.example !== undefined, `${method.toUpperCase()} ${path} has parameters without examples`);
|
||||||
testPath = testPath.replace('{' + param.name + '}', param.example);
|
|
||||||
break;
|
|
||||||
case 'header':
|
|
||||||
headers[param.name] = param.example;
|
|
||||||
break;
|
|
||||||
case 'query':
|
|
||||||
qs[param.name] = param.example;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
url = nconf.get('url') + testPath;
|
switch (param.in) {
|
||||||
});
|
case 'path':
|
||||||
|
testPath = testPath.replace('{' + param.name + '}', param.example);
|
||||||
|
break;
|
||||||
|
case 'header':
|
||||||
|
headers[param.name] = param.example;
|
||||||
|
break;
|
||||||
|
case 'query':
|
||||||
|
qs[param.name] = param.example;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it('should resolve with a 200 when called', async () => {
|
url = nconf.get('url') + prefix + testPath;
|
||||||
await setupData();
|
});
|
||||||
|
|
||||||
try {
|
it('may contain a request body with application/json type if POST/PUT/DELETE', () => {
|
||||||
response = await request(url, {
|
if (['post', 'put', 'delete'].includes(method) && context[method].hasOwnProperty('requestBody')) {
|
||||||
method: method,
|
assert(context[method].requestBody);
|
||||||
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
|
assert(context[method].requestBody.content);
|
||||||
json: true,
|
assert(context[method].requestBody.content['application/json']);
|
||||||
headers: headers,
|
assert(context[method].requestBody.content['application/json'].schema);
|
||||||
qs: qs,
|
assert(context[method].requestBody.content['application/json'].schema.properties);
|
||||||
});
|
}
|
||||||
} catch (e) {
|
});
|
||||||
assert(!e, `${method.toUpperCase()} ${path} resolved with ${e.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
// Recursively iterate through schema properties, comparing type
|
it('should resolve with a 200 when called', async () => {
|
||||||
it('response should match schema definition', () => {
|
await setupData();
|
||||||
const has200 = context[method].responses['200'];
|
|
||||||
if (!has200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasJSON = has200.content && has200.content['application/json'];
|
if (csrfToken) {
|
||||||
if (hasJSON) {
|
headers['x-csrf-token'] = csrfToken;
|
||||||
schema = context[method].responses['200'].content['application/json'].schema;
|
}
|
||||||
compare(schema, response, 'root');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO someday: text/csv, binary file type checking?
|
let body = {};
|
||||||
|
if (context[method].hasOwnProperty('requestBody')) {
|
||||||
|
body = buildBody(context[method].requestBody.content['application/json'].schema.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// console.log(`calling ${method} ${url} with`, body);
|
||||||
|
response = await request(url, {
|
||||||
|
method: method,
|
||||||
|
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
|
||||||
|
json: true,
|
||||||
|
headers: headers,
|
||||||
|
qs: qs,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
assert(!e, `${method.toUpperCase()} ${path} resolved with ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recursively iterate through schema properties, comparing type
|
||||||
|
it('response should match schema definition', () => {
|
||||||
|
const has200 = context[method].responses['200'];
|
||||||
|
if (!has200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasJSON = has200.content && has200.content['application/json'];
|
||||||
|
if (hasJSON) {
|
||||||
|
schema = context[method].responses['200'].content['application/json'].schema;
|
||||||
|
compare(schema, response, method.toUpperCase(), path, 'root');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO someday: text/csv, binary file type checking?
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully re-login if needed', async () => {
|
||||||
|
const reloginPaths = ['/users/{uid}/password'];
|
||||||
|
if (method === 'put' && reloginPaths.includes(path)) {
|
||||||
|
jar = await helpers.loginUser('admin', '123456');
|
||||||
|
|
||||||
|
// Retrieve CSRF token using cookie, to test Write API
|
||||||
|
const config = await request({
|
||||||
|
url: nconf.get('url') + '/api/config',
|
||||||
|
json: true,
|
||||||
|
jar: jar,
|
||||||
|
});
|
||||||
|
csrfToken = config.csrf_token;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
function compare(schema, response, context) {
|
function buildBody(schema) {
|
||||||
|
return Object.keys(schema).reduce((memo, cur) => {
|
||||||
|
memo[cur] = schema[cur].example;
|
||||||
|
return memo;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(schema, response, method, path, context) {
|
||||||
let required = [];
|
let required = [];
|
||||||
const additionalProperties = schema.hasOwnProperty('additionalProperties');
|
const additionalProperties = schema.hasOwnProperty('additionalProperties');
|
||||||
|
|
||||||
@@ -194,7 +289,7 @@ describe('Read API', async () => {
|
|||||||
// Compare the schema to the response
|
// Compare the schema to the response
|
||||||
required.forEach((prop) => {
|
required.forEach((prop) => {
|
||||||
if (schema.hasOwnProperty(prop)) {
|
if (schema.hasOwnProperty(prop)) {
|
||||||
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')');
|
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
|
|
||||||
// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec)
|
// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec)
|
||||||
if (response[prop] === null && schema[prop].nullable === true) {
|
if (response[prop] === null && schema[prop].nullable === true) {
|
||||||
@@ -202,30 +297,30 @@ describe('Read API', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Therefore, if the value is actually null, that's a problem (nullable is probably missing)
|
// Therefore, if the value is actually null, that's a problem (nullable is probably missing)
|
||||||
assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')');
|
assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
|
|
||||||
switch (schema[prop].type) {
|
switch (schema[prop].type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
break;
|
break;
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
break;
|
break;
|
||||||
case 'object':
|
case 'object':
|
||||||
assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop);
|
compare(schema[prop], response[prop], method, path, context ? [context, prop].join('.') : prop);
|
||||||
break;
|
break;
|
||||||
case 'array':
|
case 'array':
|
||||||
assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
|
|
||||||
if (schema[prop].items) {
|
if (schema[prop].items) {
|
||||||
// Ensure the array items have a schema defined
|
// Ensure the array items have a schema defined
|
||||||
assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')');
|
assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + method + ' ' + path + ', context: ' + context + ')');
|
||||||
|
|
||||||
// Compare types
|
// Compare types
|
||||||
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
|
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
|
||||||
response[prop].forEach((res) => {
|
response[prop].forEach((res) => {
|
||||||
compare(schema[prop].items, res, context ? [context, prop].join('.') : prop);
|
compare(schema[prop].items, res, method, path, context ? [context, prop].join('.') : prop);
|
||||||
});
|
});
|
||||||
} else if (response[prop].length) { // for now
|
} else if (response[prop].length) { // for now
|
||||||
response[prop].forEach((item) => {
|
response[prop].forEach((item) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user