mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 03:55:55 +01:00
Testing suite integration for openapi spec (#8263)
* feat: testing suite integration for openapi spec The testing suite now takes the openapi spec into account. It will check each route defined, make a call to it, and compare the response with the defined schema. Any mismatches will cause the test to fail. * fix(openapi): removed debug stuff from tests * fix(openapi): fixed some tests * fix(openapi): added additional check to tests, test fixes * fix(openapi): better tests, fixed spec errors * fix(openapi): bad conditional in test * fix: oops * fix(openapi): more tests fixing * fix(openapi): more tests * fix(openapi): fix some more tests * fix: verbose'd an info log * fix: topic pagination route returns schema-optimized pagination block * fix(openapi): more test/spec fixes * fix(openapi): accidentally sending in authenticated jar for anon routes * fix(openapi): more test/spec fixes * fix(openapi): more spec fixes * fix: timestampReadable Invalid Date * fix(openapi): more tests... almost there * fix(openapi): more tests fixing * fix(openapi): finally all tests passing * fix(openapi): added reverse test to compare response to spec ... and fixed all the tests that broke * fix: remove tests related to group covers, as route is gone * fix(openapi): broken test on travis * fix(openapi): broken test on travis * fix(openapi): broken test on travis * fix(openapi): object cache is not present for psql * fix: tests Co-authored-by: Barış Soner Uşaklı <barisusakli@gmail.com>
This commit is contained in:
@@ -109,6 +109,7 @@
|
||||
"email:sendmail:rateLimit": 2,
|
||||
"email:sendmail:rateDelta": 1000,
|
||||
"hideFullname": 0,
|
||||
"hideEmail": 0,
|
||||
"allowGuestHandles": 0,
|
||||
"disableRecentCategoryFilter": 0,
|
||||
"maximumRelatedTopics": 0,
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"prompt": "^1.0.0",
|
||||
"redis": "3.0.2",
|
||||
"request": "2.88.2",
|
||||
"request-promise-native": "^1.0.8",
|
||||
"rimraf": "3.0.2",
|
||||
"rss": "^1.2.2",
|
||||
"sanitize-html": "^1.23.0",
|
||||
|
||||
@@ -10,3 +10,7 @@ Breadcrumbs:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
cid:
|
||||
type: number
|
||||
required:
|
||||
- text
|
||||
@@ -47,10 +47,7 @@ CommonProps:
|
||||
property:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- content
|
||||
- noEscape
|
||||
- property
|
||||
link:
|
||||
type: array
|
||||
items:
|
||||
@@ -69,28 +66,12 @@ CommonProps:
|
||||
required:
|
||||
- rel
|
||||
- href
|
||||
- type
|
||||
- sizes
|
||||
widgets:
|
||||
type: object
|
||||
description: Rendered widgets
|
||||
properties:
|
||||
header:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
html:
|
||||
type: string
|
||||
sidebar:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
html:
|
||||
type: string
|
||||
footer:
|
||||
description: Each widget area will have its own property in this object
|
||||
additionalProperties:
|
||||
type: array
|
||||
description: A collection of HTML snippets that are appended to each widget area
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
GroupObject:
|
||||
GroupFullObject:
|
||||
type: object
|
||||
description: The response from an internal call to `Groups.get(<groupname>)`
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@@ -14,7 +15,7 @@ GroupObject:
|
||||
type: number
|
||||
description: Label text for the user badge
|
||||
userTitleEnabled:
|
||||
type: boolean
|
||||
type: number
|
||||
description:
|
||||
type: string
|
||||
description: The group description
|
||||
@@ -73,3 +74,60 @@ GroupObject:
|
||||
type: boolean
|
||||
isOwner:
|
||||
type: boolean
|
||||
nullable: true
|
||||
GroupDataObject:
|
||||
type: object
|
||||
description: The response from an internal call to `Groups.getGroupData(<groupname>, [])` with **explicitly** no fields passed in
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The group name
|
||||
slug:
|
||||
type: string
|
||||
description: URL-safe slug of the group name
|
||||
createtime:
|
||||
type: number
|
||||
description: UNIX timestamp of the group's creation
|
||||
userTitle:
|
||||
type: number
|
||||
description: Label text for the user badge
|
||||
userTitleEnabled:
|
||||
type: number
|
||||
description:
|
||||
type: string
|
||||
description: The group description
|
||||
memberCount:
|
||||
type: number
|
||||
hidden:
|
||||
type: number
|
||||
system:
|
||||
type: number
|
||||
private:
|
||||
type: number
|
||||
disableJoinRequests:
|
||||
type: number
|
||||
disableLeave:
|
||||
type: number
|
||||
cover:url:
|
||||
type: string
|
||||
cover:thumb:url:
|
||||
type: string
|
||||
nameEncoded:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
description: A custom override of the group's name, a friendly name
|
||||
labelColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
textColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
icon:
|
||||
type: string
|
||||
description: A FontAwesome icon string
|
||||
createtimeISO:
|
||||
type: string
|
||||
description: "`createtime` rendered as an ISO 8601 format"
|
||||
cover:position:
|
||||
type: string
|
||||
@@ -34,10 +34,30 @@ Pagination:
|
||||
type: boolean
|
||||
rel:
|
||||
type: array
|
||||
items: {}
|
||||
description: A collection of objects used to build the link tags pointing to adjacent pages, if any.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
rel:
|
||||
type: string
|
||||
enum: [prev, next]
|
||||
href:
|
||||
type: string
|
||||
description: A query string that points to the previous or next page
|
||||
pages:
|
||||
type: array
|
||||
items: {}
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: number
|
||||
description: The current page
|
||||
active:
|
||||
type: boolean
|
||||
description: If the page noted in this array is the current page
|
||||
qs:
|
||||
type: string
|
||||
description: A query string that points to the page noted in this array
|
||||
currentPage:
|
||||
type: number
|
||||
pageCount:
|
||||
|
||||
@@ -41,6 +41,7 @@ PostsObject:
|
||||
removed, etc.)
|
||||
picture:
|
||||
type: string
|
||||
nullable: true
|
||||
status:
|
||||
type: string
|
||||
icon:text:
|
||||
@@ -79,6 +80,10 @@ PostsObject:
|
||||
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:
|
||||
|
||||
253
public/openapi/components/schemas/TopicObject.yaml
Normal file
253
public/openapi/components/schemas/TopicObject.yaml
Normal file
@@ -0,0 +1,253 @@
|
||||
TopicObject:
|
||||
type: object
|
||||
properties:
|
||||
tid:
|
||||
type: number
|
||||
description: A topic identifier
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
mainPid:
|
||||
type: number
|
||||
description: The post id of the first post in this topic (also called the
|
||||
"original post")
|
||||
title:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
timestamp:
|
||||
type: number
|
||||
lastposttime:
|
||||
type: number
|
||||
postcount:
|
||||
type: number
|
||||
viewcount:
|
||||
type: number
|
||||
teaserPid:
|
||||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
nullable: true
|
||||
upvotes:
|
||||
type: number
|
||||
downvotes:
|
||||
type: number
|
||||
deleted:
|
||||
type: number
|
||||
locked:
|
||||
type: number
|
||||
pinned:
|
||||
type: number
|
||||
description: Whether or not this particular topic is pinned to the top of the
|
||||
category
|
||||
deleterUid:
|
||||
type: number
|
||||
titleRaw:
|
||||
type: string
|
||||
timestampISO:
|
||||
type: string
|
||||
description: An ISO 8601 formatted date string (complementing `timestamp`)
|
||||
lastposttimeISO:
|
||||
type: string
|
||||
votes:
|
||||
type: number
|
||||
category:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
image:
|
||||
nullable: true
|
||||
type: string
|
||||
imageClass:
|
||||
nullable: true
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
disabled:
|
||||
type: number
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
username:
|
||||
type: string
|
||||
description: A friendly name for a given user account
|
||||
fullname:
|
||||
type: string
|
||||
userslug:
|
||||
type: string
|
||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces
|
||||
removed, etc.)
|
||||
reputation:
|
||||
type: number
|
||||
postcount:
|
||||
type: number
|
||||
picture:
|
||||
type: string
|
||||
nullable: true
|
||||
signature:
|
||||
type: string
|
||||
nullable: true
|
||||
banned:
|
||||
type: number
|
||||
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"
|
||||
banned_until_readable:
|
||||
type: string
|
||||
required:
|
||||
- uid
|
||||
- username
|
||||
- userslug
|
||||
- reputation
|
||||
- postcount
|
||||
- picture
|
||||
- signature
|
||||
- banned
|
||||
- status
|
||||
- icon:text
|
||||
- icon:bgColor
|
||||
- banned_until_readable
|
||||
teaser:
|
||||
type: object
|
||||
properties:
|
||||
pid:
|
||||
type: number
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
timestamp:
|
||||
type: number
|
||||
tid:
|
||||
type: number
|
||||
description: A topic identifier
|
||||
content:
|
||||
type: string
|
||||
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:
|
||||
nullable: true
|
||||
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"
|
||||
index:
|
||||
type: number
|
||||
nullable: true
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
valueEscaped:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
score:
|
||||
type: number
|
||||
isOwner:
|
||||
type: boolean
|
||||
ignored:
|
||||
type: boolean
|
||||
unread:
|
||||
type: boolean
|
||||
bookmark:
|
||||
nullable: true
|
||||
type: number
|
||||
unreplied:
|
||||
type: boolean
|
||||
icons:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: HTML injected into the theme
|
||||
index:
|
||||
type: number
|
||||
thumb:
|
||||
type: string
|
||||
required:
|
||||
- tid
|
||||
- uid
|
||||
- cid
|
||||
- mainPid
|
||||
- title
|
||||
- slug
|
||||
- timestamp
|
||||
- lastposttime
|
||||
- postcount
|
||||
- viewcount
|
||||
- teaserPid
|
||||
- upvotes
|
||||
- downvotes
|
||||
- deleted
|
||||
- locked
|
||||
- pinned
|
||||
- deleterUid
|
||||
- titleRaw
|
||||
- timestampISO
|
||||
- lastposttimeISO
|
||||
- votes
|
||||
- category
|
||||
- user
|
||||
- teaser
|
||||
- tags
|
||||
- isOwner
|
||||
- ignored
|
||||
- unread
|
||||
- bookmark
|
||||
- unreplied
|
||||
- icons
|
||||
- index
|
||||
@@ -33,34 +33,41 @@ UserObject:
|
||||
type: string
|
||||
description: A URL pointing to a picture to be used as the user's avatar
|
||||
example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
|
||||
nullable: true
|
||||
fullname:
|
||||
type: string
|
||||
example: Mr. Dragon Fruit Jr.
|
||||
location:
|
||||
type: string
|
||||
example: 'Toronto, Canada'
|
||||
nullable: true
|
||||
birthday:
|
||||
type: string
|
||||
description: A birthdate given in an ISO format parseable by the Date object
|
||||
example: 03/27/2020
|
||||
nullable: true
|
||||
website:
|
||||
type: string
|
||||
example: 'https://example.org'
|
||||
nullable: true
|
||||
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
|
||||
to tell you all about how I became the administrator of NodeBB
|
||||
nullable: true
|
||||
signature:
|
||||
type: string
|
||||
example: |
|
||||
This is an example signature
|
||||
It can span multiple lines.
|
||||
nullable: true
|
||||
uploadedpicture:
|
||||
type: string
|
||||
example: /assets/profile/1-profileimg.png
|
||||
description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
|
||||
nullable: true
|
||||
profileviews:
|
||||
type: number
|
||||
description: The number of times this user's profile has been viewed
|
||||
@@ -98,18 +105,21 @@ UserObject:
|
||||
flags:
|
||||
type: number
|
||||
example: 0
|
||||
followercount:
|
||||
nullable: true
|
||||
followerCount:
|
||||
type: number
|
||||
example: 2
|
||||
followingcount:
|
||||
followingCount:
|
||||
type: number
|
||||
example: 5
|
||||
'cover:url':
|
||||
type: string
|
||||
example: /assets/profile/1-cover.png
|
||||
nullable: true
|
||||
'cover:position':
|
||||
type: string
|
||||
example: 50.0301% 19.2464%
|
||||
nullable: true
|
||||
groupTitle:
|
||||
type: string
|
||||
example: '["administrators","Staff"]'
|
||||
@@ -140,7 +150,45 @@ UserObject:
|
||||
type: string
|
||||
description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
|
||||
example: Not Banned
|
||||
required:
|
||||
- uid
|
||||
- username
|
||||
- userslug
|
||||
- 'email:confirmed'
|
||||
- joindate
|
||||
- lastonline
|
||||
- picture
|
||||
- location
|
||||
- birthday
|
||||
- website
|
||||
- aboutme
|
||||
- signature
|
||||
- uploadedpicture
|
||||
- profileviews
|
||||
- reputation
|
||||
- postcount
|
||||
- topiccount
|
||||
- lastposttime
|
||||
- banned
|
||||
- 'banned:expire'
|
||||
- status
|
||||
- enum
|
||||
- flags
|
||||
- followerCount
|
||||
- followingCount
|
||||
- 'cover:url'
|
||||
- 'cover:position'
|
||||
- groupTitle
|
||||
- groupTitleArray
|
||||
- example
|
||||
- 'icon:text'
|
||||
- 'icon:bgColor'
|
||||
- joindateISO
|
||||
- lastonlineISO
|
||||
- banned_until
|
||||
- banned_until_readable
|
||||
UserObjectFull:
|
||||
# accountHelpers.getUserDataByUserSlug
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
@@ -175,6 +223,7 @@ UserObjectFull:
|
||||
type: string
|
||||
description: A URL pointing to a picture to be used as the user's avatar
|
||||
example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
|
||||
nullable: true
|
||||
fullname:
|
||||
type: string
|
||||
example: Mr. Dragon Fruit Jr.
|
||||
@@ -193,7 +242,7 @@ UserObjectFull:
|
||||
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
|
||||
to tell you all about how I became the administrator of NodeBB
|
||||
signature:
|
||||
type: string
|
||||
example: |
|
||||
@@ -203,6 +252,7 @@ UserObjectFull:
|
||||
type: string
|
||||
example: /assets/profile/1-profileimg.png
|
||||
description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
|
||||
nullable: true
|
||||
profileviews:
|
||||
type: number
|
||||
description: The number of times this user's profile has been viewed
|
||||
@@ -240,18 +290,21 @@ UserObjectFull:
|
||||
flags:
|
||||
type: number
|
||||
example: 0
|
||||
followercount:
|
||||
nullable: true
|
||||
followerCount:
|
||||
type: number
|
||||
example: 2
|
||||
followingcount:
|
||||
followingCount:
|
||||
type: number
|
||||
example: 5
|
||||
'cover:url':
|
||||
type: string
|
||||
example: /assets/profile/1-cover.png
|
||||
nullable: true
|
||||
'cover:position':
|
||||
type: string
|
||||
example: 50.0301% 19.2464%
|
||||
nullable: true
|
||||
groupTitle:
|
||||
type: string
|
||||
example: '["administrators","Staff"]'
|
||||
@@ -332,7 +385,8 @@ UserObjectFull:
|
||||
type: boolean
|
||||
groups:
|
||||
type: array
|
||||
items: {}
|
||||
items:
|
||||
$ref: ./GroupObject.yaml#/GroupFullObject
|
||||
disableSignatures:
|
||||
type: boolean
|
||||
reputation:disabled:
|
||||
@@ -369,6 +423,12 @@ UserObjectFull:
|
||||
type: boolean
|
||||
icon:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- route
|
||||
- name
|
||||
- visibility
|
||||
- public
|
||||
sso:
|
||||
type: array
|
||||
items:
|
||||
@@ -411,6 +471,7 @@ UserObjectSlim:
|
||||
type: string
|
||||
description: A URL pointing to a picture to be used as the user's avatar
|
||||
example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
|
||||
nullable: true
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
@@ -437,6 +498,7 @@ UserObjectSlim:
|
||||
flags:
|
||||
type: number
|
||||
example: 0
|
||||
nullable: true
|
||||
banned:
|
||||
type: number
|
||||
description: A Boolean representing whether a user is banned or not
|
||||
@@ -473,3 +535,74 @@ UserObjectSlim:
|
||||
example: Not Banned
|
||||
administrator:
|
||||
type: boolean
|
||||
UserObjectACP:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: number
|
||||
description: A user identifier
|
||||
example: 1
|
||||
username:
|
||||
type: string
|
||||
description: A friendly name for a given user account
|
||||
example: Dragon Fruit
|
||||
userslug:
|
||||
type: string
|
||||
description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
|
||||
example: dragon-fruit
|
||||
email:
|
||||
type: string
|
||||
description: Email address associated with the user account
|
||||
example: dragonfruit@example.org
|
||||
postcount:
|
||||
type: number
|
||||
example: 1000
|
||||
joindate:
|
||||
type: number
|
||||
description: A UNIX timestamp representing the moment the user's account was created
|
||||
example: 1585337827953
|
||||
banned:
|
||||
type: number
|
||||
description: A Boolean representing whether a user is banned or not
|
||||
example: 0
|
||||
reputation:
|
||||
type: number
|
||||
description: The user's reputation score on the forum. Out-of-the-box, users gain/lose reputation points based on upvotes/downvotes, though plugins can alter the logic and criterion for awarding reputation points
|
||||
example: 100
|
||||
picture:
|
||||
type: string
|
||||
description: A URL pointing to a picture to be used as the user's avatar
|
||||
example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
|
||||
nullable: true
|
||||
flags:
|
||||
type: number
|
||||
example: 0
|
||||
nullable: true
|
||||
lastonline:
|
||||
type: number
|
||||
description: A UNIX timestamp representing the moment the user was last recorded online on this site
|
||||
example: 1585337827953
|
||||
'email:confirmed':
|
||||
type: number
|
||||
description: Whether the user has confirmed their email address or not
|
||||
example: 1
|
||||
'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
|
||||
example: D
|
||||
'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: '#9c27b0'
|
||||
joindateISO:
|
||||
type: string
|
||||
example: '2020-03-27T20:30:36.590Z'
|
||||
lastonlineISO:
|
||||
type: string
|
||||
example: '2020-03-27T20:30:36.590Z'
|
||||
banned_until_readable:
|
||||
type: string
|
||||
description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
|
||||
example: Not Banned
|
||||
administrator:
|
||||
type: boolean
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,7 @@ define('forum/topic/posts', [
|
||||
|
||||
function updatePagination() {
|
||||
$.get(config.relative_path + '/api/topic/pagination/' + ajaxify.data.tid, { page: ajaxify.data.pagination.currentPage }, function (paginationData) {
|
||||
app.parseAndTranslate('partials/paginator', { pagination: paginationData }, function (html) {
|
||||
app.parseAndTranslate('partials/paginator', paginationData, function (html) {
|
||||
$('[component="pagination"]').after(html).remove();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ consentController.get = async function (req, res, next) {
|
||||
const consented = await db.getObjectField('user:' + userData.uid, 'gdpr_consent');
|
||||
userData.gdpr_consent = parseInt(consented, 10) === 1;
|
||||
userData.digest = {
|
||||
frequency: meta.config.dailyDigestFreq,
|
||||
frequency: meta.config.dailyDigestFreq || 'off',
|
||||
enabled: meta.config.dailyDigestFreq !== 'off',
|
||||
};
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ async function getProfileMenu(uid, callerUID) {
|
||||
id: 'info',
|
||||
route: 'info',
|
||||
name: '[[user:account_info]]',
|
||||
icon: 'fa-info',
|
||||
visibility: {
|
||||
self: false,
|
||||
other: false,
|
||||
@@ -155,6 +156,7 @@ async function getProfileMenu(uid, callerUID) {
|
||||
id: 'sessions',
|
||||
route: 'sessions',
|
||||
name: '[[pages:account/sessions]]',
|
||||
icon: 'fa-group',
|
||||
visibility: {
|
||||
self: true,
|
||||
other: false,
|
||||
@@ -170,6 +172,7 @@ async function getProfileMenu(uid, callerUID) {
|
||||
id: 'consent',
|
||||
route: 'consent',
|
||||
name: '[[user:consent.title]]',
|
||||
icon: 'fa-thumbs-o-up',
|
||||
visibility: {
|
||||
self: true,
|
||||
other: false,
|
||||
@@ -190,6 +193,8 @@ async function getProfileMenu(uid, callerUID) {
|
||||
|
||||
async function parseAboutMe(userData) {
|
||||
if (!userData.aboutme) {
|
||||
userData.aboutme = '';
|
||||
userData.aboutmeParsed = '';
|
||||
return;
|
||||
}
|
||||
userData.aboutme = validator.escape(String(userData.aboutme || ''));
|
||||
|
||||
@@ -106,12 +106,12 @@ settingsController.get = async function (req, res, next) {
|
||||
|
||||
userData.categoryWatchState = { [userData.settings.categoryWatchState]: true };
|
||||
|
||||
userData.disableCustomUserSkins = meta.config.disableCustomUserSkins;
|
||||
userData.disableCustomUserSkins = meta.config.disableCustomUserSkins || 0;
|
||||
|
||||
userData.allowUserHomePage = meta.config.allowUserHomePage;
|
||||
userData.allowUserHomePage = meta.config.allowUserHomePage || 1;
|
||||
|
||||
userData.hideFullname = meta.config.hideFullname;
|
||||
userData.hideEmail = meta.config.hideEmail;
|
||||
userData.hideFullname = meta.config.hideFullname || 0;
|
||||
userData.hideEmail = meta.config.hideEmail || 0;
|
||||
|
||||
userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search');
|
||||
|
||||
|
||||
@@ -28,11 +28,17 @@ infoController.get = function (req, res) {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
let port = nconf.get('port');
|
||||
if (!Array.isArray(port) && !isNaN(parseInt(port, 10))) {
|
||||
port = [port];
|
||||
}
|
||||
|
||||
res.render('admin/development/info', {
|
||||
info: data,
|
||||
infoJSON: JSON.stringify(data, null, 4),
|
||||
host: os.hostname(),
|
||||
port: nconf.get('port'),
|
||||
port: port,
|
||||
nodeCount: data.length,
|
||||
timeout: timeoutMS,
|
||||
ip: req.ip,
|
||||
|
||||
@@ -103,7 +103,7 @@ categoryController.get = async function (req, res, next) {
|
||||
|
||||
addTags(categoryData, res);
|
||||
|
||||
categoryData['feeds:disableRSS'] = meta.config['feeds:disableRSS'];
|
||||
categoryData['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0;
|
||||
categoryData['reputation:disabled'] = meta.config['reputation:disabled'];
|
||||
pageCount = Math.max(1, Math.ceil(categoryData.topic_count / userSettings.topicsPerPage));
|
||||
categoryData.pagination = pagination.create(currentPage, pageCount, req.query);
|
||||
|
||||
@@ -62,9 +62,9 @@ recentController.getData = async function (req, url, sort) {
|
||||
data.canPost = canPost;
|
||||
data.categories = categoryData.categories;
|
||||
data.allCategoriesUrl = url + helpers.buildQueryString('', filter, '');
|
||||
data.selectedCategory = categoryData.selectedCategory;
|
||||
data.selectedCategory = categoryData.selectedCategory || null;
|
||||
data.selectedCids = categoryData.selectedCids;
|
||||
data['feeds:disableRSS'] = meta.config['feeds:disableRSS'];
|
||||
data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0;
|
||||
data.rssFeedUrl = nconf.get('relative_path') + '/' + url + '.rss';
|
||||
if (req.loggedIn) {
|
||||
data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
|
||||
|
||||
@@ -32,10 +32,6 @@ tagsController.getTag = async function (req, res) {
|
||||
helpers.getCategoriesByStates(req.uid, '', states),
|
||||
]);
|
||||
|
||||
if (Array.isArray(tids) && !tids.length) {
|
||||
return res.render('tag', templateData);
|
||||
}
|
||||
|
||||
templateData.categories = categoriesData.categories;
|
||||
|
||||
templateData.topics = await topics.getTopics(tids, req.uid);
|
||||
|
||||
@@ -70,16 +70,12 @@ topicsController.get = async function getTopic(req, res, callback) {
|
||||
topics.modifyPostsByPrivilege(topicData, userPrivileges);
|
||||
|
||||
const hookData = await plugins.fireHook('filter:controllers.topic.get', { topicData: topicData, uid: req.uid });
|
||||
await Promise.all([
|
||||
buildBreadcrumbs(hookData.topicData),
|
||||
addTags(topicData, req, res),
|
||||
]);
|
||||
|
||||
topicData.privileges = userPrivileges;
|
||||
topicData.topicStaleDays = meta.config.topicStaleDays;
|
||||
topicData['reputation:disabled'] = meta.config['reputation:disabled'];
|
||||
topicData['downvote:disabled'] = meta.config['downvote:disabled'];
|
||||
topicData['feeds:disableRSS'] = meta.config['feeds:disableRSS'];
|
||||
topicData['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0;
|
||||
topicData.bookmarkThreshold = meta.config.bookmarkThreshold;
|
||||
topicData.necroThreshold = meta.config.necroThreshold;
|
||||
topicData.postEditDuration = meta.config.postEditDuration;
|
||||
@@ -99,6 +95,11 @@ topicsController.get = async function getTopic(req, res, callback) {
|
||||
res.locals.linkTags.push(rel);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
buildBreadcrumbs(hookData.topicData),
|
||||
addTags(topicData, req, res),
|
||||
]);
|
||||
|
||||
incrementViewCount(req, tid);
|
||||
|
||||
markAsRead(req, tid);
|
||||
@@ -338,5 +339,5 @@ topicsController.pagination = async function (req, res, callback) {
|
||||
rel.href = nconf.get('url') + '/topic/' + topic.slug + rel.href;
|
||||
});
|
||||
|
||||
res.json(paginationData);
|
||||
res.json({ pagination: paginationData });
|
||||
};
|
||||
|
||||
@@ -178,7 +178,7 @@ userController.exportUploads = function (req, res, next) {
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
winston.info('[user/export/uploads] Collating uploads for uid ' + targetUid);
|
||||
winston.verbose('[user/export/uploads] Collating uploads for uid ' + targetUid);
|
||||
user.collateUploads(targetUid, archive, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -43,7 +43,6 @@ redisModule.init = function (callback) {
|
||||
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
require('./redis/promisify')(redisModule.client);
|
||||
|
||||
callback();
|
||||
|
||||
@@ -100,6 +100,7 @@ Flags.get = async function (flagId) {
|
||||
|
||||
const flagObj = {
|
||||
state: 'open',
|
||||
assignee: null,
|
||||
...base,
|
||||
description: validator.escape(base.description),
|
||||
datetimeISO: utils.toISOString(base.datetime),
|
||||
@@ -164,6 +165,7 @@ Flags.list = async function (filters, uid) {
|
||||
const userObj = await user.getUserFields(flagObj.uid, ['username', 'picture']);
|
||||
flagObj = {
|
||||
state: 'open',
|
||||
assignee: null,
|
||||
...flagObj,
|
||||
reporter: {
|
||||
username: userObj.username,
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = function (Messaging) {
|
||||
messages = await Promise.all(messages.map(async (message) => {
|
||||
if (message.system) {
|
||||
message.content = validator.escape(String(message.content));
|
||||
message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(message.content));
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ admin.get = async function () {
|
||||
async function getAvailable() {
|
||||
const core = require('../../install/data/navigation.json').map(function (item) {
|
||||
item.core = true;
|
||||
item.id = item.id || '';
|
||||
item.properties = item.properties || { targetBlank: false };
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ module.exports = function (Posts) {
|
||||
}
|
||||
|
||||
async function getTopicAndCategories(tids) {
|
||||
const topicsData = await topics.getTopicsFields(tids, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount', 'mainPid']);
|
||||
const topicsData = await topics.getTopicsFields(tids, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount', 'mainPid', 'teaserPid']);
|
||||
const cids = _.uniq(topicsData.map(topic => topic && topic.cid));
|
||||
const categoriesData = await categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color', 'image', 'imageClass']);
|
||||
return { topics: topicsData, categories: categoriesData };
|
||||
|
||||
@@ -106,4 +106,8 @@ function modifyTopic(topic, fields) {
|
||||
if (topic.hasOwnProperty('upvotes') && topic.hasOwnProperty('downvotes')) {
|
||||
topic.votes = topic.upvotes - topic.downvotes;
|
||||
}
|
||||
|
||||
if (fields.includes('teaserPid') || !fields.length) {
|
||||
topic.teaserPid = topic.teaserPid || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ Topics.getTopicsByTids = async function (tids, options) {
|
||||
if (topics[i]) {
|
||||
topics[i].category = categoriesMap[topics[i].cid];
|
||||
topics[i].user = usersMap[topics[i].uid];
|
||||
topics[i].teaser = teasers[i];
|
||||
topics[i].teaser = teasers[i] || null;
|
||||
topics[i].tags = tags[i];
|
||||
|
||||
topics[i].isOwner = topics[i].uid === parseInt(uid, 10);
|
||||
|
||||
@@ -101,7 +101,7 @@ module.exports = function (User) {
|
||||
banObj.user = usersData[index];
|
||||
banObj.until = parseInt(banObj.expire, 10);
|
||||
banObj.untilReadable = new Date(banObj.until).toString();
|
||||
banObj.timestampReadable = new Date(banObj.timestamp).toString();
|
||||
banObj.timestampReadable = new Date(parseInt(banObj.timestamp, 10)).toString();
|
||||
banObj.timestampISO = utils.toISOString(banObj.timestamp);
|
||||
banObj.reason = validator.escape(String(banObj.reason || '')) || '[[user:info.banned-no-reason]]';
|
||||
return banObj;
|
||||
|
||||
216
test/api.js
216
test/api.js
@@ -3,18 +3,226 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const SwaggerParser = require('@apidevtools/swagger-parser');
|
||||
const request = require('request-promise-native');
|
||||
const nconf = require('nconf');
|
||||
|
||||
describe('Read API', () => {
|
||||
let readApi;
|
||||
const db = require('./mocks/databasemock');
|
||||
const helpers = require('./helpers');
|
||||
const user = require('../src/user');
|
||||
const groups = require('../src/groups');
|
||||
const categories = require('../src/categories');
|
||||
const topics = require('../src/topics');
|
||||
const plugins = require('../src/plugins');
|
||||
const flags = require('../src/flags');
|
||||
const messaging = require('../src/messaging');
|
||||
|
||||
describe('Read API', async () => {
|
||||
let readApi = false;
|
||||
const apiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
||||
let jar;
|
||||
let setup = false;
|
||||
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user
|
||||
|
||||
async function dummySearchHook(data) {
|
||||
return [1];
|
||||
}
|
||||
|
||||
after(async function () {
|
||||
plugins.unregisterHook('core', 'filter:search.query', dummySearchHook);
|
||||
});
|
||||
|
||||
async function setupData() {
|
||||
if (setup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create sample users
|
||||
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' });
|
||||
await groups.join('administrators', adminUid);
|
||||
|
||||
// Create a category
|
||||
const testCategory = await categories.create({ name: 'test' });
|
||||
|
||||
// Post a new topic
|
||||
const testTopic = await topics.post({
|
||||
uid: adminUid,
|
||||
cid: testCategory.cid,
|
||||
title: 'Test Topic',
|
||||
content: 'Test topic content',
|
||||
});
|
||||
|
||||
// Create a sample flag
|
||||
await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
|
||||
|
||||
// Create a new chat room
|
||||
await messaging.newRoom(1, [2]);
|
||||
|
||||
// Attach a search hook so /api/search is enabled
|
||||
plugins.registerHook('core', {
|
||||
hook: 'filter:search.query',
|
||||
method: dummySearchHook,
|
||||
});
|
||||
|
||||
jar = await helpers.loginUser('admin', '123456');
|
||||
setup = true;
|
||||
}
|
||||
|
||||
it('should pass OpenAPI v3 validation', async () => {
|
||||
const apiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
||||
try {
|
||||
readApi = await SwaggerParser.validate(apiPath);
|
||||
await SwaggerParser.validate(apiPath);
|
||||
} catch (e) {
|
||||
assert.ifError(e);
|
||||
}
|
||||
});
|
||||
|
||||
readApi = await SwaggerParser.dereference(apiPath);
|
||||
|
||||
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
|
||||
const paths = Object.keys(readApi.paths);
|
||||
|
||||
paths.forEach((path) => {
|
||||
let schema;
|
||||
let response;
|
||||
let url;
|
||||
const headers = {};
|
||||
const qs = {};
|
||||
|
||||
function compare(schema, response, context) {
|
||||
let required = [];
|
||||
const additionalProperties = schema.hasOwnProperty('additionalProperties');
|
||||
|
||||
if (schema.allOf) {
|
||||
schema = schema.allOf.reduce((memo, obj) => {
|
||||
required = required.concat(obj.required ? obj.required : Object.keys(obj.properties));
|
||||
memo = { ...memo, ...obj.properties };
|
||||
return memo;
|
||||
}, {});
|
||||
} else if (schema.properties) {
|
||||
required = schema.required || Object.keys(schema.properties);
|
||||
schema = schema.properties;
|
||||
} else {
|
||||
// If schema contains no properties, check passes
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare the schema to the response
|
||||
required.forEach((prop) => {
|
||||
if (schema.hasOwnProperty(prop)) {
|
||||
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 + ')');
|
||||
|
||||
switch (schema[prop].type) {
|
||||
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 + ')');
|
||||
break;
|
||||
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 + ')');
|
||||
break;
|
||||
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 + ')');
|
||||
compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop);
|
||||
break;
|
||||
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 + ')');
|
||||
|
||||
if (schema[prop].items) {
|
||||
// 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 + ')');
|
||||
|
||||
// Compare types
|
||||
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
|
||||
response[prop].forEach((res) => {
|
||||
compare(schema[prop].items, res, context ? [context, prop].join('.') : prop);
|
||||
});
|
||||
} else if (response[prop].length) { // for now
|
||||
response[prop].forEach((item) => {
|
||||
assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compare the response to the schema
|
||||
Object.keys(response).forEach((prop) => {
|
||||
if (additionalProperties) { // All bets are off
|
||||
return;
|
||||
}
|
||||
|
||||
assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
|
||||
// TOXO: fix -- premature exit for POST-only routes
|
||||
if (!readApi.paths[path].get) {
|
||||
return;
|
||||
}
|
||||
|
||||
it('should have examples when parameters are present', () => {
|
||||
const parameters = readApi.paths[path].get.parameters;
|
||||
let testPath = path;
|
||||
if (parameters) {
|
||||
parameters.forEach((param) => {
|
||||
assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples');
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
url = nconf.get('url') + testPath;
|
||||
});
|
||||
|
||||
it('should resolve with a 200 when called', async () => {
|
||||
await setupData();
|
||||
|
||||
try {
|
||||
response = await request(url, {
|
||||
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
|
||||
json: true,
|
||||
headers: headers,
|
||||
qs: qs,
|
||||
});
|
||||
} catch (e) {
|
||||
assert(!e, path + ' resolved with ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Recursively iterate through schema properties, comparing type
|
||||
it('response should match schema definition', () => {
|
||||
const has200 = readApi.paths[path].get.responses['200'];
|
||||
if (!has200) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasJSON = has200.content && has200.content['application/json'];
|
||||
if (hasJSON) {
|
||||
schema = readApi.paths[path].get.responses['200'].content['application/json'].schema;
|
||||
compare(schema, response, 'root');
|
||||
}
|
||||
|
||||
// TODO someday: text/csv, binary file type checking?
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Write API', () => {
|
||||
|
||||
@@ -1071,7 +1071,7 @@ describe('Topic\'s', function () {
|
||||
assert.ifError(err);
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert(body);
|
||||
assert.deepEqual(body, {
|
||||
assert.deepEqual(body.pagination, {
|
||||
prev: { page: 1, active: false },
|
||||
next: { page: 1, active: false },
|
||||
first: { page: 1, active: true },
|
||||
|
||||
Reference in New Issue
Block a user