Merge branch '443-docker-experimental' into develop

indiachan-spamvector
Thomas Lynch 2 years ago
commit 986e0c89bd
  1. 8
      .dockerignore
  2. 1
      .gitignore
  3. 23
      CONTRIBUTING.md
  4. 18
      INSTALLATION.md
  5. 12
      controllers/forms/globalsettings.js
  6. 70
      docker-compose.yml
  7. 23
      docker/jschan/Dockerfile
  8. 20
      docker/jschan/Dockerfile-reset
  9. 40
      docker/jschan/secrets.js
  10. 88
      docker/nginx/Dockerfile
  11. 16
      docker/nginx/jschan.conf
  12. 44
      docker/nginx/nginx.conf
  13. 4
      helpers/checks/captcha.js
  14. 12
      helpers/datearray.js
  15. 2
      helpers/decodequeryip.js
  16. 25
      helpers/decodequeryip.test.js
  17. 17
      helpers/escaperegexp.test.js
  18. 2
      helpers/files/formatsize.js
  19. 19
      helpers/files/formatsize.test.js
  20. 1
      helpers/pagequeryconverter.js
  21. 23
      helpers/pagequeryconverter.test.js
  22. 2
      helpers/paramconverter.js
  23. 41
      helpers/paramconverter.test.js
  24. 73
      helpers/permission.test.js
  25. 33
      helpers/posting/diceroll.test.js
  26. 19
      helpers/posting/escape.test.js
  27. 2
      helpers/posting/fortune.js
  28. 9
      helpers/posting/fortune.test.js
  29. 42
      helpers/posting/linkmatch.test.js
  30. 2
      helpers/posting/name.js
  31. 31
      helpers/posting/name.test.js
  32. 29
      helpers/posting/tripcode.test.js
  33. 21
      helpers/randomrange.test.js
  34. 4
      helpers/schema.js
  35. 124
      helpers/schema.test.js
  36. 11
      helpers/setting.js
  37. 69
      helpers/setting.test.js
  38. 13
      helpers/settingsdiff.js
  39. 59
      helpers/settingsdiff.test.js
  40. 56
      helpers/timeutils.test.js
  41. 2
      models/forms/create.js
  42. 2
      models/forms/makepost.js
  43. 13155
      package-lock.json
  44. 8
      package.json
  45. 2
      server.js
  46. 658
      test/actions.js
  47. 286
      test/board.js
  48. 109
      test/cleanup.js
  49. 139
      test/global.js
  50. 9
      test/integration.test.js
  51. 59
      test/pages.js
  52. 132
      test/posting.js
  53. 246
      test/setup.js

@ -0,0 +1,8 @@
node_modules/
static/
docker/jschan/Dockerfile
docker/jschan/Dockerfile-reset
docker/nginx/Dockerfile
docker/static/
tools/
gulp/res/js/socket.io.js

1
.gitignore vendored

@ -3,6 +3,7 @@ backup.sh
configs/*.json
configs/*.js
static/*
docker/static/*
gulp/res/js/socket.io.js
/gulp/res/css/codethemes
gulp/res/css/locals.css

@ -31,5 +31,28 @@ Read the code to understand, but basically:
* TAB for indentation.
* Please include comments.
## Running tests (WIP)
Make sure these still pass after your changes, or adjust them to meet the new expected results.
There is a "jschan-test" service in the `docker-compose.yml` file that will run all the tests in a jschan instance using the docker instance. See the advanced section of installation for some instruction on how to use this.
You can also Run them locally if you have an instance setup (or for quickly running unit tests):
```bash
#unit tests
npm run test
# OR npm run test:unit
#integration tests
TEST_ADMIN_PASSWORD=<password from jschan-reset docker> npm run test:integration
#all tests
npm run test:all
#specific test(s)
npm run test:all <filename|regex>
```
Thanks,
Tom

@ -242,3 +242,21 @@ To build all css files, run `gulp css`. For some situations, such as adding or r
For detecting and automatically updating Tor exit node lists, see [tools/update_tor_exits.sh](tools/update_tor_exits.sh)
For updating the GeoIP database for nginx, see [tools/update_geoip.sh](tools/update_geoip.sh)
#### Docker
Experimental, strictly for development only.
Basically:
```bash
docker-compose up -d mongodb redis
#on the first run, or to "gulp reset" later:
docker-compose up jschan-reset
docker-compose up -d jschan
docker-compose up -d nginx
```

@ -164,12 +164,12 @@ module.exports = {
{ result: numberBody(req.body.board_defaults_tph_trigger_action, 0, 4), expected: true, error: 'Board default tph trigger action must be a number from 0-4' },
{ result: numberBody(req.body.board_defaults_captcha_reset, 0, 2), expected: true, error: 'Board defaults captcha reset must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_lock_reset, 0, 2), expected: true, error: 'Board defaults lock reset must be a number from 0-2' },
{ result: numberBodyVariable(req.body.board_defaults_reply_limit, req.body.global_limits_reply_limit_min, globalLimits.replyLimit.min, req.body.global_limits_reply_limit_max, globalLimits.replyLimit.max), expected: true, error: `Board defaults reply limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_thread_limit, req.body.global_limits_thread_limit_min, globalLimits.threadLimit.min, req.body.global_limits_thread_limit_max, globalLimits.threadLimit.max), expected: true, error: `Board defaults thread limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_bump_limit, req.body.global_limits_bump_limit_min, globalLimits.bumpLimit.min, req.body.global_limits_bump_limit_max, globalLimits.bumpLimit.max), expected: true, error: `Board defaults bump limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_files, 0, 0, req.body.global_limits_post_files_max, globalLimits.postFiles.max), expected: true, error: `Board defaults max files must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_thread_message_length, 0, 0, req.body.global_limits_field_length_message, globalLimits.fieldLength.message), expected: true, error: `Board defaults max thread message length must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_reply_message_length, 0, 0, req.body.global_limits_field_length_message, globalLimits.fieldLength.message), expected: true, error: `Board defaults max reply message length must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_reply_limit, globalLimits.replyLimit.min, req.body.global_limits_reply_limit_min, globalLimits.replyLimit.max, req.body.global_limits_reply_limit_max), expected: true, error: `Board defaults reply limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_thread_limit, globalLimits.threadLimit.min, req.body.global_limits_thread_limit_min, globalLimits.threadLimit.max, req.body.global_limits_thread_limit_max), expected: true, error: `Board defaults thread limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_bump_limit, globalLimits.bumpLimit.min, req.body.global_limits_bump_limit_min, globalLimits.bumpLimit.max, req.body.global_limits_bump_limit_max), expected: true, error: `Board defaults bump limit must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_files, 0, 0, globalLimits.postFiles.max, req.body.global_limits_post_files_max), expected: true, error: `Board defaults max files must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_thread_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: `Board defaults max thread message length must be within global limits` },
{ result: numberBodyVariable(req.body.board_defaults_max_reply_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: `Board defaults max reply message length must be within global limits` },
{ result: numberBody(req.body.board_defaults_min_thread_message_length), expected: true, error: 'Board defaults min thread message length must be a number' },
{ result: numberBody(req.body.board_defaults_min_reply_message_length), expected: true, error: 'Board defaults min reply message length must be a number' },
{ result: minmaxBody(req.body.board_defaults_min_thread_message_length, req.body.board_defaults_max_thread_message_length), expected: true, error: 'Board defaults thread message length min must be less than max' },

@ -0,0 +1,70 @@
version: "3.5"
services:
redis:
command: redis-server --requirepass changeme
image: redis:alpine
mongodb:
image: mongo:latest
environment:
- MONGO_INITDB_ROOT_USERNAME=jschan
- MONGO_INITDB_ROOT_PASSWORD=changeme
nginx:
build:
context: .
dockerfile: ./docker/nginx/Dockerfile
args:
ENABLED_MODULES: geoip
ports:
- "80:80"
volumes:
- ./docker/static/:/path/to/jschan/static/
depends_on:
- jschan
jschan:
build:
context: .
dockerfile: ./docker/jschan/Dockerfile
network: jschan_default
environment:
- NODE_ENV=development
- JSCHAN_IP=0.0.0.0
- NO_CAPTCHA=1
- MONGO_USERNAME=jschan
- MONGO_PASSWORD=changeme
- REDIS_PASSWORD=changeme
- COOKIE_SECRET=changeme
- TRIPCODE_SECRET=changeme
- IP_HASH_SECRET=changeme
- POST_PASSWORD_SECRET=changeme
- GOOGLE_SITEKEY=changeme
- GOOGLE_SECRETKEY=changeme
- HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
- HCAPTCHA_SECRETKEY=0x0000000000000000000000000000000000000000
volumes:
- ./docker/static:/opt/static/
depends_on:
- redis
- mongodb
jschan-reset:
build:
context: .
dockerfile: ./docker/jschan/Dockerfile-reset
network: jschan_default
environment:
- MONGO_USERNAME=jschan
- MONGO_PASSWORD=changeme
- REDIS_PASSWORD=changeme
volumes:
- ./docker/static:/opt/static/
depends_on:
- redis
- mongodb
networks:
default:
name: jschan_default

@ -0,0 +1,23 @@
FROM node:16
RUN apt-get update -y
RUN apt-get install ffmpeg imagemagick graphicsmagick -y
WORKDIR /opt
COPY . .
RUN npm install
RUN npm install -g pm2 gulp
COPY ./docker/jschan/secrets.js ./configs/secrets.js
#i fucking hate docker
ENV MONGO_USERNAME jschan
ENV MONGO_PASSWORD changeme
ENV REDIS_PASSWORD changeme
RUN gulp generate-favicon
CMD ["/bin/sh", "-c", "gulp; pm2-runtime start ecosystem.config.js"]

@ -0,0 +1,20 @@
FROM node:16
WORKDIR /opt
COPY . .
RUN npm install
RUN npm i -g pm2 gulp
COPY ./docker/jschan/secrets.js ./configs/secrets.js
#i fucking hate docker
ENV MONGO_USERNAME jschan
ENV MONGO_PASSWORD changeme
ENV REDIS_PASSWORD changeme
RUN gulp generate-favicon
CMD ["/bin/sh", "-c", "gulp reset; gulp"]

@ -0,0 +1,40 @@
module.exports = {
//mongodb connection string
dbURL: `mongodb://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@mongodb:27017`,
//database name
dbName: 'jschan',
//redis connection info
redis: {
host: 'redis',
port: '6379',
password: process.env.REDIS_PASSWORD,
},
//backend webserver port
port: 7000,
//secrets/salts for various things
cookieSecret: process.env.COOKIE_SECRET,
tripcodeSecret: process.env.TRIPCODE_SECRET,
ipHashSecret: process.env.IP_HASH_SECRET,
postPasswordSecret: process.env.POST_PASSWORD_SECRET,
//keys for google recaptcha
google: {
siteKey: process.env.GOOGLE_SITEKEY,
secretKey: process.env.GOOGLE_SECRETKEY,
},
//keys for hcaptcha
hcaptcha: {
siteKey: process.env.HCAPTCHA_SITEKEY,
secretKey: process.env.HCAPTCHA_SECRETKEY,
},
//enable debug logging
debugLogs: true,
};

@ -0,0 +1,88 @@
FROM nginx:mainline as builder
ARG ENABLED_MODULES
RUN set -ex \
&& if [ "$ENABLED_MODULES" = "" ]; then \
echo "No additional modules enabled, exiting"; \
exit 1; \
fi
#COPY ./ /modules/
RUN set -ex \
&& apt update \
&& apt install -y --no-install-suggests --no-install-recommends \
patch make wget mercurial devscripts debhelper dpkg-dev \
quilt lsb-release build-essential libxml2-utils xsltproc \
equivs git g++ \
&& hg clone -r ${NGINX_VERSION}-${PKG_RELEASE%%~*} https://hg.nginx.org/pkg-oss/ \
&& cd pkg-oss \
&& mkdir /tmp/packages \
&& for module in $ENABLED_MODULES; do \
echo "Building $module for nginx-$NGINX_VERSION"; \
if [ -d /modules/$module ]; then \
echo "Building $module from user-supplied sources"; \
# check if module sources file is there and not empty
if [ ! -s /modules/$module/source ]; then \
echo "No source file for $module in modules/$module/source, exiting"; \
exit 1; \
fi; \
# some modules require build dependencies
if [ -f /modules/$module/build-deps ]; then \
echo "Installing $module build dependencies"; \
apt update && apt install -y --no-install-suggests --no-install-recommends $(cat /modules/$module/build-deps | xargs); \
fi; \
# if a module has a build dependency that is not in a distro, provide a
# shell script to fetch/build/install those
# note that shared libraries produced as a result of this script will
# not be copied from the builder image to the main one so build static
if [ -x /modules/$module/prebuild ]; then \
echo "Running prebuild script for $module"; \
/modules/$module/prebuild; \
fi; \
/pkg-oss/build_module.sh -v $NGINX_VERSION -f -y -o /tmp/packages -n $module $(cat /modules/$module/source); \
BUILT_MODULES="$BUILT_MODULES $(echo $module | tr '[A-Z]' '[a-z]' | tr -d '[/_\-\.\t ]')"; \
elif make -C /pkg-oss/debian list | grep -P "^$module\s+\d" > /dev/null; then \
echo "Building $module from pkg-oss sources"; \
cd /pkg-oss/debian; \
make rules-module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
mk-build-deps --install --tool="apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes" debuild-module-$module/nginx-$NGINX_VERSION/debian/control; \
make module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \
find ../../ -maxdepth 1 -mindepth 1 -type f -name "*.deb" -exec mv -v {} /tmp/packages/ \;; \
BUILT_MODULES="$BUILT_MODULES $module"; \
else \
echo "Don't know how to build $module module, exiting"; \
exit 1; \
fi; \
done \
&& echo "BUILT_MODULES=\"$BUILT_MODULES\"" > /tmp/packages/modules.env
FROM nginx:mainline
COPY --from=builder /tmp/packages /tmp/packages
RUN set -ex \
&& apt update \
&& apt-get install wget -y \
&& . /tmp/packages/modules.env \
&& for module in $BUILT_MODULES; do \
apt install --no-install-suggests --no-install-recommends -y /tmp/packages/nginx-module-${module}_${NGINX_VERSION}*.deb; \
done \
&& rm -rf /tmp/packages \
&& rm -rf /var/lib/apt/lists/
RUN mkdir /usr/share/GeoIP
RUN wget https://dl.miyuru.lk/geoip/dbip/country/dbip.dat.gz
RUN gunzip dbip.dat.gz
RUN mv dbip.dat /usr/share/GeoIP/GeoIP.dat
RUN rm /etc/nginx/conf.d/default.conf
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/nginx/jschan.conf /etc/nginx/conf.d/
COPY ./configs/nginx/snippets/ /etc/nginx/snippets/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,16 @@
upstream chan {
server jschan:7000;
}
server {
server_name _;
client_max_body_size 0;
listen 80;
listen [::]:80;
include /etc/nginx/snippets/security_headers.conf;
include /etc/nginx/snippets/error_pages.conf;
include /etc/nginx/snippets/jschan_clearnet_routes.conf;
include /etc/nginx/snippets/jschan_common_routes.conf;
}

@ -0,0 +1,44 @@
load_module /etc/nginx/modules/ngx_http_geoip_module-debug.so;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1000;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
geoip_country /usr/share/GeoIP/GeoIP.dat;
map_hash_max_size 4096;
map_hash_bucket_size 256;
aio threads;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
server_names_hash_bucket_size 128;
client_max_body_size 0;
#proxy_request_buffering off;
log_format custom '[$time_local] $remote_addr $status "$request" "$http_referer" "$http_user_agent" $bytes_sent';
access_log /var/log/nginx/access.log custom;
error_log /var/log/nginx/error.log;
gzip on;
#gzip_vary off;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/css text/js text/xml text/javascript image/x-icon application/javascript application/json application/xml application/rss+xml image/svg+xml;
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
include /etc/nginx/conf.d/*;
}

@ -10,6 +10,10 @@ const { Captchas } = require(__dirname+'/../../db/')
module.exports = async (captchaInput, captchaId) => {
if (process.env.NO_CAPTCHA) {
return true;
}
const { captchaOptions } = config.get;
//check if captcha field in form is valid

@ -1,12 +0,0 @@
'use strict';
//https://stackoverflow.com/a/4413721
module.exports = (startDate, stopDate) => {
const dateArray = new Array();
let currentDate = startDate;
while (currentDate <= stopDate) {
dateArray.push(new Date (currentDate.valueOf()));
currentDate.setDate(currentDate.getDate() + 1);
}
return dateArray;
}

@ -5,7 +5,7 @@ const escapeRegExp = require(__dirname+'/escaperegexp.js')
, Permissions = require(__dirname+'/permissions.js');
module.exports = (query, permissions) => {
if (query.ip && typeof query.ip === 'string') {
if (query && query.ip && typeof query.ip === 'string') {
const decoded = decodeURIComponent(query.ip);
//if is IP but no permission, return null
if (isIP(decoded) && !permissions.get(Permissions.VIEW_RAW_IP)) {

@ -0,0 +1,25 @@
const decodeQueryIp = require('./decodequeryip.js');
const Permission = require('./permission.js');
const Permissions = require('./permissions.js');
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
const NO_PERMISSION = new Permission();
describe('decode query ip', () => {
const cases = [
{ in: { query: null, permission: ROOT }, out: null },
{ in: { query: {}, permission: ROOT }, out: null },
{ in: { query: { ip: '10.0.0.1' }, permission: ROOT }, out: '10.0.0.1' },
{ in: { query: { ip: '10.0.0.1' }, permission: NO_PERMISSION }, out: null },
{ in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: ROOT }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' },
{ in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: NO_PERMISSION }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' },
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(decodeQueryIp(cases[i].in.query, cases[i].in.permission)).toStrictEqual(cases[i].out)
});
}
});

@ -0,0 +1,17 @@
const escapeRegExp = require('./escaperegexp.js');
describe('escape regular expression', () => {
const cases = [
{ in: '', out: '' },
{ in: '/', out: '/' },
{ in: '.*+?^${}()|[]\\', out: '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' },
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(escapeRegExp(cases[i].in)).toStrictEqual(cases[i].out)
});
}
});

@ -7,6 +7,6 @@ module.exports = (bytes) => {
if (bytes === 0) {
return '0B';
}
const i = Math.floor(Math.log(bytes) / Math.log(k));
const i = Math.min(sizes.length-1, Math.floor(Math.log(bytes) / Math.log(k)));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
};

@ -0,0 +1,19 @@
const formatSize = require('./formatsize.js');
describe('formatSize() - convert bytes to human readable file size', () => {
const cases = [
{in: 1024, out: "1KB"},
{in: Math.pow(1024, 2), out: "1MB"},
{in: Math.pow(1024, 3), out: "1GB"},
{in: Math.pow(1024, 4), out: "1TB"},
{in: Math.pow(1024, 5), out: "1024TB"},
{in: Math.pow(1024, 3)+(Math.pow(1024, 2)*512), out: "1.5GB"},
{in: 100, out: "100B"},
{in: 0, out: "0B"},
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in} bytes`, () => {
expect(formatSize(cases[i].in)).toBe(cases[i].out);
});
}
});

@ -1,6 +1,7 @@
'use strict';
module.exports = (query, limit) => {
query = query || {};
const nopage = { ...query };
delete nopage.page;
const queryString = new URLSearchParams(nopage).toString();

@ -0,0 +1,23 @@
const pageQueryConverter = require('./pagequeryconverter.js');
const limit = 30;
describe('page query converter', () => {
const cases = [
{ in: null, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: [1, 2, 3] }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: "test" }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: null }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: -1 }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: 0 }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: 1 }, out: { offset: 0, "queryString": "", page: 1 } },
{ in: { page: 5 }, out: { offset: limit*4, "queryString": "", page: 5 } },
{ in: { page: 10, other: "test" }, out: { offset: limit*9, "queryString": "other=test", page: 10 } },
{ in: { other: "test" }, out: { offset: 0, "queryString": "other=test", page: 1 } },
];
for(let i in cases) {
test(`should contain ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(pageQueryConverter(cases[i].in, limit)).toStrictEqual(cases[i].out)
});
}
});

@ -1,6 +1,6 @@
'use strict';
const { ObjectId } = require(__dirname+'/../db/db.js')
const { ObjectId } = require('mongodb')
, timeFieldRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
, timeUtils = require(__dirname+'/timeutils.js')
, dynamicResponse = require(__dirname+'/dynamic.js')

@ -0,0 +1,41 @@
const paramConverter = require('./paramconverter.js');
const { WEEK, DAY, HOUR } = require('./timeutils.js');
/*
const defaultOptions = {
timeFields: [],
trimFields: [],
allowedArrays: [],
numberFields: [],
numberArrays: [],
objectIdParams: [],
objectIdFields: [],
objectIdArrays: [],
processThreadIdParam: false,
processDateParam: false,
processMessageLength: false,
};
*/
describe('paramconverter', () => {
const cases = [
{
in: { options: { trimFields: ['username', 'password'] }, body: { username: 'trimmed ', password: 'trimmed ' } },
out: { username: 'trimmed', password: 'trimmed' }
},
{
in: { options: { timeFields: ['test'] }, body: { test: '1w2d3h' } },
out: { test: WEEK+(2*DAY)+(3*HOUR) }
},
//todo: add a bunch more
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
const converter = paramConverter(cases[i].in.options);
converter({ body: cases[i].in.body }, {}, () => {});
expect(cases[i].in.body).toStrictEqual(cases[i].out);
});
}
});

@ -0,0 +1,73 @@
const randomRange = require('./randomrange.js');
const Permission = require('./permission.js');
const Permissions = require('./permissions.js');
describe('testing permissions', () => {
const NO_PERMISSION = new Permission();
const ANON = new Permission();
ANON.setAll([
Permissions.USE_MARKDOWN_PINKTEXT, Permissions.USE_MARKDOWN_GREENTEXT, Permissions.USE_MARKDOWN_BOLD,
Permissions.USE_MARKDOWN_UNDERLINE, Permissions.USE_MARKDOWN_STRIKETHROUGH, Permissions.USE_MARKDOWN_TITLE,
Permissions.USE_MARKDOWN_ITALIC, Permissions.USE_MARKDOWN_SPOILER, Permissions.USE_MARKDOWN_MONO,
Permissions.USE_MARKDOWN_CODE, Permissions.USE_MARKDOWN_DETECTED, Permissions.USE_MARKDOWN_LINK,
Permissions.USE_MARKDOWN_DICE, Permissions.USE_MARKDOWN_FORTUNE, Permissions.CREATE_BOARD,
Permissions.CREATE_ACCOUNT
]);
test('test a permission they have = true', () => {
expect(ANON.get(Permissions.CREATE_ACCOUNT)).toBe(true);
});
test('test a permission they dont have = false', () => {
expect(ANON.get(Permissions.ROOT)).toBe(false);
});
const BOARD_STAFF = new Permission(ANON.base64)
BOARD_STAFF.setAll([
Permissions.MANAGE_BOARD_GENERAL, Permissions.MANAGE_BOARD_BANS, Permissions.MANAGE_BOARD_LOGS,
]);
const BOARD_OWNER = new Permission(BOARD_STAFF.base64)
BOARD_OWNER.setAll([
Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_BOARD_STAFF, Permissions.MANAGE_BOARD_CUSTOMISATION,
Permissions.MANAGE_BOARD_SETTINGS,
]);
test('BO has all board perms', () => {
Permissions._MANAGE_BOARD_BITS.every(b => expect(BOARD_OWNER.get(b)).toBe(true));
});
test('applyInheritance() gives BO all board perms as long as they have Permissions.MANAGE_BOARD_OWNER', () => {
BOARD_OWNER.setAll(Permissions._MANAGE_BOARD_BITS, false);
BOARD_OWNER.set(Permissions.MANAGE_BOARD_OWNER);
BOARD_OWNER.applyInheritance();
Permissions._MANAGE_BOARD_BITS.every(b => expect(BOARD_OWNER.get(b)).toBe(true));
});
const GLOBAL_STAFF = new Permission(BOARD_OWNER.base64);
GLOBAL_STAFF.setAll([
Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_GLOBAL_BANS, Permissions.MANAGE_GLOBAL_LOGS, Permissions.MANAGE_GLOBAL_NEWS,
Permissions.MANAGE_GLOBAL_BOARDS, Permissions.MANAGE_GLOBAL_SETTINGS, Permissions.MANAGE_BOARD_OWNER, Permissions.BYPASS_FILTERS,
Permissions.BYPASS_BANS, Permissions.BYPASS_SPAMCHECK, Permissions.BYPASS_RATELIMITS,
]);
const ADMIN = new Permission(GLOBAL_STAFF.base64);
ADMIN.setAll([
Permissions.MANAGE_GLOBAL_ACCOUNTS, Permissions.MANAGE_GLOBAL_ROLES, Permissions.VIEW_RAW_IP,
]);
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
test('root has all permissions', () => {
Permission.allPermissions.every(p => expect(ROOT.get(p)).toBe(true));
});
test('applyInheritance() gives ROOT all permissions as long as they have Permissions.ROOT', () => {
NO_PERMISSION.set(Permissions.ROOT);
NO_PERMISSION.applyInheritance();
Permission.allPermissions.every(b => expect(NO_PERMISSION.get(b)).toBe(true));
});
//todo: what othe rpermissions test should be added?
});

@ -0,0 +1,33 @@
const diceroll = require('./diceroll.js');
describe('diceroll markdown', () => {
const prepareCases = [
{ in: '##3d6', out: '##3d6=' },
{ in: '##99d99', out: '##99d99=' },
{ in: '##999d999', out: '##999d999' },
{ in: '##3d8+5', out: '##3d8+5=' },
{ in: '##3d8-5', out: '##3d8-5=' },
{ in: '##0d0', out: '##0d0' },
];
for(let i in prepareCases) {
test(`should contain ${prepareCases[i].out} for an input of ${prepareCases[i].in}`, () => {
const output = prepareCases[i].in.replace(diceroll.regexPrepare, diceroll.prepare.bind(null, false));
expect(output).toContain(prepareCases[i].out);
});
}
const markdownCases = [
{ in: '##3d6&#x3D;10', out: 'Rolled 3 dice with 6 sides =' },
{ in: '##99d99&#x3D;5138', out: 'Rolled 99 dice with 99 sides =' },
{ in: '##999d999&#x3D;10000', out: '##999d999&#x3D;10000' },
{ in: '##0d0', out: '##0d0' },
];
for(let i in markdownCases) {
test(`should contain ${markdownCases[i].out} for an input of ${markdownCases[i].in}`, () => {
const output = markdownCases[i].in.replace(diceroll.regexMarkdown, diceroll.markdown.bind(null, false));
expect(output).toContain(markdownCases[i].out);
});
}
});

@ -0,0 +1,19 @@
const escape = require('./escape.js');
describe('escape() - convert some characters to html entities', () => {
const cases = [
{ in: "'", out: '&#39;' },
{ in: '/', out: '&#x2F;' },
{ in: '`', out: '&#x60;' },
{ in: '=', out: '&#x3D;' },
{ in: '&', out: '&amp;' },
{ in: '<', out: '&lt;' },
{ in: '>', out: '&gt;' },
{ in: '"', out: '&quot;' },
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(escape(cases[i].in)).toBe(cases[i].out);
});
}
});

@ -4,6 +4,8 @@ const fortunes = ['example1', 'example2', 'example3'];
module.exports = {
fortunes,
regex: /##fortune/gmi,
markdown: () => {

@ -0,0 +1,9 @@
const fortune = require('./fortune.js');
describe('fortune markdown', () => {
test(`should contain a random fortune for an input of ##fortune`, () => {
const output = '##fortune'.replace(fortune.regex, fortune.markdown.bind(null, false));
const hasFortuneText = fortune.fortunes.some(f => output.includes(f));
expect(hasFortuneText).toBe(true);
});
});

@ -0,0 +1,42 @@
const linkmatch = require('./linkmatch.js');
const linkRegex = /\[(?<label>[^\[][^\]]*?)\]\((?<url>(?:&#x2F;[^\s<>\[\]{}|\\^)]+|https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^)]+))\)|(?<urlOnly>https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+)/g;
const Permission = require('../permission.js')
const Permissions = require('../permissions.js')
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
const NO_PERMISSION = new Permission();
describe('link markdown', () => {
const rootCases = [
{ in: 'http:&#x2F;&#x2F;something.com', out: "href=" },
{ in: 'https:&#x2F;&#x2F;something.com', out: "href=" },
{ in: '[test](http:&#x2F;&#x2F;something.com)', out: ">test</a>" },
{ in: '[test](https:&#x2F;&#x2F;something.com)', out: ">test</a>" },
{ in: 'http:&#x2F;&#x2F;', out: "http:&#x2F;&#x2F;" },
{ in: 'https:&#x2F;&#x2F;', out: "https:&#x2F;&#x2F;" },
];
for(let i in rootCases) {
test(`should contain ${rootCases[i].out} for an input of ${rootCases[i].in}`, () => {
expect(rootCases[i].in.replace(linkRegex, linkmatch.bind(null, ROOT))).toContain(rootCases[i].out)
});
}
const cases = [
{ in: 'http:&#x2F;&#x2F;something.com', out: "href=" },
{ in: 'https:&#x2F;&#x2F;something.com', out: "href=" },
{ in: '[http:&#x2F;&#x2F;something.com](test)', out: ">http:&#x2F;&#x2F;something.com</a>" },
{ in: '[https:&#x2F;&#x2F;something.com](test)', out: ">https:&#x2F;&#x2F;something.com</a>" },
{ in: 'http:&#x2F;&#x2F;', out: "http:&#x2F;&#x2F;" },
{ in: 'https:&#x2F;&#x2F;', out: "https:&#x2F;&#x2F;" },
];
for(let i in cases) {
test(`should contain ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(cases[i].in.replace(linkRegex, linkmatch.bind(null, NO_PERMISSION))).toContain(cases[i].out)
});
}
});

@ -68,7 +68,7 @@ module.exports = async (inputName, permissions, boardSettings, boardOwner, board
if (capcodeInput && capcodeInput.toLowerCase() !== staffLevel.toLowerCase()) {
capcode = `${staffLevel} ${capcodeInput.replace(staffLevelsRegex, '').trim()}`;
}
capcode = `## ${capcode}`;
capcode = `## ${capcode.trim()}`;
}
}
}

@ -0,0 +1,31 @@
const name = require('./name.js');
const Permission = require('../permission.js')
const Permissions = require('../permissions.js')
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
describe('name/trip/capcode handler', () => {
const cases = [
{ in: '## Admin', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '## Global Staff', out: { name: 'Anon', tripcode: null, capcode: '## Global Staff' } },
{ in: '## Board Owner', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '## Board Mod', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '##', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '', out: { name: 'Anon', tripcode: null, capcode: null } },
{ in: 'test', out: { name: 'test', tripcode: null, capcode: null } },
{ in: 'test#12345', out: { name: 'test', tripcode: '!CSZ6G0yP9Q', capcode: null } },
{ in: '#12345', out: { name: 'Anon', tripcode: '!CSZ6G0yP9Q', capcode: null } },
{ in: '#12345## Admin', out: { name: 'Anon', tripcode: '!CSZ6G0yP9Q', capcode: '## Admin' } },
{ in: 'test#12345## Admin', out: { name: 'test', tripcode: '!CSZ6G0yP9Q', capcode: '## Admin' } },
];
for(let i in cases) {
test(`should contain ${cases[i].out.capcode} for an input of ${cases[i].in}`, async () => {
const output = await name(cases[i].in, ROOT, {forceAnon: false, defaultName: 'Anon'}, 'a', {a:ROOT}, 'b')
expect(output).toStrictEqual(cases[i].out)
});
}
});

@ -0,0 +1,29 @@
const { getSecureTrip, getInsecureTrip } = require('./tripcode.js');
describe('getSecureTrip() - "secure" tripcodes', () => {
const cases = [
{ in: '' },
{ in: null },
{ in: '13245' },
{ in: '1324512345123451234512345123451234512345' },
];
for(let i in cases) {
test(`should not error for an input of ${cases[i].in}`, async () => {
expect((await getSecureTrip(cases[i].in)));
});
}
});
describe('getInsecureTrip() - "insecure" tripcodes', () => {
const cases = [
{ in: '', out: "8NBuQ4l6uQ" },
{ in: null, out: "8NBuQ4l6uQ" },
{ in: '13245', out: "VPkdFNhOGY" },
{ in: '1324512345123451234512345123451234512345', out: "9ovLU2O1wk" },
];
for(let i in cases) {
test(`should contain ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(getInsecureTrip(cases[i].in)).toBe(cases[i].out);
});
}
});

@ -0,0 +1,21 @@
const randomRange = require('./randomrange.js');
describe('randomRange() - return number with secure crypto', () => {
const cases = [
{ in: { min: 0, max: 10 } },
{ in: { min: 0.5, max: 1 } },
{ in: { min: 0, max: 1000 } },
{ in: { min: -1, max: 10 } },
{ in: { min: 0, max: 0 } },
{ in: { min: 1000, max: 1000 } },
];
for(let i in cases) {
test(`randomRange should output ${cases[i].in.min}<=out<=${cases[i].in.max} for an input of ${cases[i].in.min}, ${cases[i].in.max}`, async () => {
const output = await randomRange(cases[i].in.min, cases[i].in.max);
expect(output).toBeGreaterThanOrEqual(Math.floor(cases[i].in.min));
expect(output).toBeLessThanOrEqual(Math.floor(cases[i].in.max));
});
}
});

@ -31,11 +31,11 @@ module.exports = {
if (maxNew == null) {
maxNew = maxOld;
}
const varMin = Math.min(minOld, minNew);
const varMin = minNew;
if (isNaN(varMin)) {
varMin = minOld;
}
const varMax = Math.max(maxOld, maxNew);
const varMax = maxNew;
if (isNaN(varMax)) {
varMax = maxOld;
}

@ -0,0 +1,124 @@
const { existsBody, lengthBody, numberBody, numberBodyVariable,
minmaxBody, inArrayBody, arrayInBody, checkSchema } = require('./schema.js');
describe('schema checking (input handling after paramconverter)', () => {
const existsBodyCases = [
{ in: { body: { test: 1 }, expected: true }, out: 0 },
{ in: { body: { test: null }, expected: false }, out: 0 },
{ in: { body: {}, expected: false }, out: 0 },
{ in: { body: { test: 1 }, expected: false }, out: 1 },
{ in: { body: { test: '' }, expected: true }, out: 0 },
];
for(let i in existsBodyCases) {
test(`existsBody should output ${existsBodyCases[i].out} for an input of ${existsBodyCases[i].in.body.test}`, async () => {
const result = await checkSchema([
{ result: existsBody(existsBodyCases[i].in.body.test), expected: existsBodyCases[i].in.expected, error: 'error' },
]);
expect(result.length).toBe(existsBodyCases[i].out);
});
}
const lengthBodyCases = [
{ in: { body: { test: null }, expected: false }, out: 0 },
{ in: { body: { test: '' }, expected: false }, out: 0 },
{ in: { body: { test: null }, min: 1, expected: false }, out: 1 },
{ in: { body: { test: '' }, min: 1, expected: false }, out: 1 },
{ in: { body: { test: 'hello' }, expected: false }, out: 0 },
{ in: { body: { test: 'hello' }, min: 0, max: 10, expected: false }, out: 0 },
{ in: { body: { test: 'hello' }, min: 10, expected: false }, out: 1 },
{ in: { body: { test: 'hellohellohello' }, min: 10, expected: false }, out: 0 },
{ in: { body: { test: 'hellohellohello' }, min: 0, max: 10, expected: false }, out: 1 },
{ in: { body: { test: 'hellohellohello' }, max: 10, expected: false }, out: 1 },
];
for(let i in lengthBodyCases) {
test(`lengthBody should output ${lengthBodyCases[i].out} for an input of ${lengthBodyCases[i].in.body.test}`, async () => {
const result = await checkSchema([
{ result: lengthBody(lengthBodyCases[i].in.body.test, lengthBodyCases[i].in.min, lengthBodyCases[i].in.max), expected: lengthBodyCases[i].in.expected, error: 'error' },
]);
expect(result.length).toBe(lengthBodyCases[i].out);
});
}
const numberBodyCases = [
{ in: { body: { test: null }, expected: false }, out: 0 },
{ in: { body: { test: 1 }, expected: true }, out: 0 },
{ in: { body: { test: 10 }, max: 10, expected: true }, out: 0 },
{ in: { body: { test: 11 }, max: 10, expected: true }, out: 1 },
{ in: { body: { test: 9 }, min: 10, expected: true }, out: 1 },
{ in: { body: { test: 10 }, min: 10, expected: true }, out: 0 },
];
for(let i in numberBodyCases) {
test(`numberBody should output ${numberBodyCases[i].out} for an input of ${numberBodyCases[i].in.body.test}`, async () => {
const result = await checkSchema([
{ result: numberBody(numberBodyCases[i].in.body.test, numberBodyCases[i].in.min, numberBodyCases[i].in.max), expected: numberBodyCases[i].in.expected, error: 'error' },
]);
expect(result.length).toBe(numberBodyCases[i].out);
});
}
const numberBodyVariableCases = [
{ in: { body: { test: 5 }, minOld: 0, minNew: 0, maxOld: 10, maxNew: 5, expected: true }, out: 0 },
{ in: { body: { test: 5 }, minOld: 0, minNew: 6, maxOld: 10, maxNew: 10, expected: true }, out: 1 },
];
for(let i in numberBodyVariableCases) {
test(`numberBodyVariable should output ${numberBodyVariableCases[i].out} for an input of ${numberBodyVariableCases[i].in.body.test}`, async () => {
const { body, minOld, minNew, maxOld, maxNew } = numberBodyVariableCases[i].in;
const result = await checkSchema([
{ result: numberBodyVariable(body.test, minOld, minNew, maxOld, maxNew), expected: numberBodyVariableCases[i].in.expected, error: 'error' },
]);
expect(result.length).toBe(numberBodyVariableCases[i].out);
});
}
const minmaxBodyCases = [
{ in: { a: 0, b: 100, expected: true }, out: 0 },
{ in: { a: 101, b: 100, expected: true }, out: 1 },
];
for(let i in minmaxBodyCases) {
test(`minmaxBody should output ${minmaxBodyCases[i].out} for an input of ${minmaxBodyCases[i].in}`, async () => {
const { a, b, expected } = minmaxBodyCases[i].in;
const result = await checkSchema([
{ result: minmaxBody(a, b), expected, error: 'error' },
]);
expect(result.length).toBe(minmaxBodyCases[i].out);
});
}
const inArrayBodyCases = [
{ in: { a: null, b: ['a', 'b', 'c'], expected: false }, out: 0 },
{ in: { a: 'x', b: [], expected: false }, out: 0 },
{ in: { a: 'x', b: ['a', 'b', 'c'], expected: false }, out: 0 },
{ in: { a: 'a', b: ['a', 'b', 'c'], expected: true }, out: 0 },
{ in: { a: null, b: new Set(['a', 'b', 'c']), expected: false }, out: 0 },
{ in: { a: 'x', b: new Set(), expected: false }, out: 0 },
{ in: { a: 'x', b: new Set(['a', 'b', 'c']), expected: false }, out: 0 },
{ in: { a: 'a', b: new Set(['a', 'b', 'c']), expected: true }, out: 0 },
];
for(let i in inArrayBodyCases) {
test(`inArrayBody should output ${inArrayBodyCases[i].out} for an input of ${inArrayBodyCases[i].in}`, async () => {
const { a, b, expected } = inArrayBodyCases[i].in;
const result = await checkSchema([
{ result: inArrayBody(a, b), expected, error: 'error' },
]);
expect(result.length).toBe(inArrayBodyCases[i].out);
});
}
const arrayInBodyCases = [
{ in: { a: null, b: ['a', 'b', 'c'], expected: false }, out: 0 },
{ in: { a: 'x', b: [], expected: false }, out: 0 },
{ in: { a: 'x', b: ['a', 'b', 'c'], expected: false }, out: 0 },
{ in: { a: 'a', b: ['a', 'b', 'c'], expected: true }, out: 0 },
];
for(let i in arrayInBodyCases) {
test(`arrayInBody should output ${arrayInBodyCases[i].out} for an input of ${arrayInBodyCases[i].in}`, async () => {
const { a, b, expected } = arrayInBodyCases[i].in;
const result = await checkSchema([
{ result: arrayInBody(b, a), expected, error: 'error' },
]);
expect(result.length).toBe(arrayInBodyCases[i].out);
});
}
});

@ -1,17 +1,21 @@
'use strict';
module.exports = {
trimSetting: (setting, oldSetting) => {
return setting != null ? setting.trim() : oldSetting;
return typeof setting === 'string' ? setting.trim() : oldSetting;
},
numberSetting: (setting, oldSetting) => {
return typeof setting === 'number' && setting !== oldSetting ? setting : oldSetting;
},
booleanSetting: (setting) => {
return setting != null;
},
arraySetting: (setting, oldSetting, limit=false) => {
if (setting !== null) {
if (typeof setting === 'string') {
const split = setting
.split(/\r?\n/)
.filter(n => n);
@ -19,5 +23,6 @@ module.exports = {
.slice(0, limit || split.length);
}
return oldSetting;
}
},
};

@ -0,0 +1,69 @@
const { trimSetting, numberSetting, booleanSetting, arraySetting } = require('./setting.js');
describe('trimSetting, numberSetting, booleanSetting, arraySetting', () => {
const trimCases = [
{ in: '', out: '' },
{ in: ' lol ', out: 'lol' },
{ in: 1, out: 'OLDSETTING' },
{ in: null, out: 'OLDSETTING' },
];
for(let i in trimCases) {
test(`trimSetting should output ${trimCases[i].out} for an input of ${trimCases[i].in}`, () => {
expect(trimSetting(trimCases[i].in, 'OLDSETTING')).toStrictEqual(trimCases[i].out);
});
}
const numberCases = [
{ in: 3, out: 3 },
{ in: undefined, out: 'OLDSETTING' },
{ in: null, out: 'OLDSETTING' },
{ in: [], out: 'OLDSETTING' },
{ in: '', out: 'OLDSETTING' },
{ in: 'string', out: 'OLDSETTING' },
];
for(let i in numberCases) {
test(`numberSetting should output ${numberCases[i].out} for an input of ${numberCases[i].in}`, () => {
expect(numberSetting(numberCases[i].in, 'OLDSETTING')).toStrictEqual(numberCases[i].out);
});
}
const booleanCases = [
{ in: null, out: false },
{ in: undefined, out: false },
{ in: '', out: true },
{ in: 'test', out: true },
{ in: 1, out: true },
{ in: [], out: true },
{ in: [1], out: true },
];
for(let i in booleanCases) {
test(`booleanSetting should output ${booleanCases[i].out} for an input of ${booleanCases[i].in}`, () => {
expect(booleanSetting(booleanCases[i].in)).toStrictEqual(booleanCases[i].out);
});
}
const arrayCases = [
{ in: undefined, out: 'OLDSETTING' },
{ in: null, out: 'OLDSETTING' },
{ in: '', out: [] },
{ in: 'test', out: ['test'] },
{ in: 1, out: 'OLDSETTING' },
{ in: [], out: 'OLDSETTING' },
{ in: '1', out: ['1'] },
{ in: `1
2
3`, out: ['1', '2', '3'] },
{ in: ` hello
123
xxx`, out: [' hello ', '123', 'xxx'] },
];
for(let i in arrayCases) {
test(`arraySetting should output ${arrayCases[i].out} for an input of ${arrayCases[i].in}`, () => {
expect(arraySetting(arrayCases[i].in, 'OLDSETTING', 10)).toStrictEqual(arrayCases[i].out);
});
}
});

@ -1,15 +1,20 @@
'use strict';
const { isDeepStrictEqual } = require('util')
const { isDeepStrictEqual } = require('util');
function getDotProp(obj, prop) {
return prop
.split('.')
.reduce((a, b) => a[b], obj);
.reduce((a, b) => {
if (a && a[b]) {
return a[b];
}
return null;
}, obj);
}
function includeChildren(template, prop, tasks) {
return Object.keys(getDotProp(template, prop))
return Object.keys(getDotProp(template, prop) || {})
.reduce((a, x) => {
a[`${prop}.${x}`] = tasks;
return a;
@ -24,7 +29,7 @@ function compareSettings(entries, oldObject, newObject, maxSetSize) {
if (!isDeepStrictEqual(oldValue, newValue)) {
entry[1].forEach(t => resultSet.add(t));
}
return resultSet.size < maxSetSize;
return resultSet.size <= maxSetSize;
});
return resultSet;
}

@ -0,0 +1,59 @@
const { getDotProp, includeChildren, compareSettings } = require('./settingsdiff.js');
describe('getDotProp, includeChildren, compareSettings in settingsdiff', () => {
const getDotPropCases = [
{ in: { object: {a:{b:{c:1}}}, prop: 'a.b.c' }, out: 1 },
{ in: { object: {a:null}, prop: 'a.b.c' }, out: null },
{ in: { object: {}, prop: 'a.b.c' }, out: null },
];
for(let i in getDotPropCases) {
test(`getDotProp should output ${getDotPropCases[i].out} for an input of ${getDotPropCases[i].in}`, () => {
expect(getDotProp(getDotPropCases[i].in.object, getDotPropCases[i].in.prop)).toStrictEqual(getDotPropCases[i].out);
});
}
const includeChildrenCases = [
{ in: { object: {a:{b:1,c:2,d:3},a2:{b:'notme'}}, prop: 'a' }, out: {'a.b':['example'], 'a.c':['example'], 'a.d':['example']} },
{ in: { object: null, prop: 'a' }, out: {} },
{ in: { object: {a:null}, prop: 'a' }, out: {} },
];
for(let i in includeChildrenCases) {
test(`includeChildren should output ${includeChildrenCases[i].out} for an input of ${includeChildrenCases[i].in}`, () => {
expect(includeChildren(includeChildrenCases[i].in.object, includeChildrenCases[i].in.prop, ['example'])).toStrictEqual(includeChildrenCases[i].out);
});
}
const compareSettingsCases = [
{
in: {
entries: [['a.b.c',['1']]],
old: {a:{b:{c:1,d:2}}},
new: {a:{b:{c:1,d:2}}},
},
out: new Set(),
},
{
in: {
entries: [['a.b.c',['1']]],
old: {a:{b:{c:1,d:2}}},
new: {a:{b:{c:1,d:3}}},
},
out: new Set(),
},
{
in: {
entries: [['a.b.c',['1']]],
old: {a:{b:{c:1,d:2}}},
new: {a:{b:{c:2,d:2}}},
},
out: new Set(['1']),
},
];
for(let i in compareSettingsCases) {
test(`compareSettings should output ${compareSettingsCases[i].out} for an input of ${compareSettingsCases[i].in}`, () => {
expect(compareSettings(compareSettingsCases[i].in.entries, compareSettingsCases[i].in.old, compareSettingsCases[i].in.new, 4)).toStrictEqual(compareSettingsCases[i].out);
});
}
});

@ -0,0 +1,56 @@
const { relativeString, relativeColor, durationString } = require('./timeutils.js');
describe('timeutils relativeString, relativeColor, durationString', () => {
const relativeStringCases = [
{ in: { start: new Date('2022-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: 'Now'},
{ in: { start: new Date('2022-04-07T08:01:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '1 minute ago'},
{ in: { start: new Date('2022-04-07T08:01:29.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '1 minute ago'},
{ in: { start: new Date('2022-04-07T08:01:30.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '2 minutes ago'},
{ in: { start: new Date('2022-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:02:00.000Z') }, out: '2 minutes from now'},
{ in: { start: new Date('2022-04-07T11:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '3 hours ago'},
{ in: { start: new Date('2022-04-10T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '3 days ago'},
{ in: { start: new Date('2022-04-28T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '3 weeks ago'},
{ in: { start: new Date('2022-07-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '3 months ago'},
{ in: { start: new Date('2022-07-08T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '3 months ago'},
{ in: { start: new Date('2022-07-25T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '4 months ago'},
{ in: { start: new Date('2023-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '1 year ago'},
{ in: { start: new Date('2032-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '10 years ago'},
{ in: { start: new Date('2132-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '110 years ago'},
{ in: { end: new Date('2132-04-07T08:00:00.000Z'), start: new Date('2022-04-07T08:00:00.000Z') }, out: '110 years from now'},
];
for(let i in relativeStringCases) {
test(`relativeString should output ${relativeStringCases[i].out} for an input of ${relativeStringCases[i].in}`, () => {
expect(relativeString(relativeStringCases[i].in.start, relativeStringCases[i].in.end)).toStrictEqual(relativeStringCases[i].out);
});
}
const relativeColorCases = [
{ in: { start: new Date('2022-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '#0098ff'},
{ in: { start: new Date('2022-04-10T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '#d9aa00'},
{ in: { start: new Date('2023-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '#000000'},
{ in: { start: new Date('2032-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '#000000'},
{ in: { start: new Date('2132-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '#000000'},
{ in: { end: new Date('2132-04-07T08:00:00.000Z'), start: new Date('2022-04-07T08:00:00.000Z') }, out: '#0098ff'},
];
for(let i in relativeColorCases) {
test(`relativeColor should output ${relativeColorCases[i].out} for an input of ${relativeColorCases[i].in}`, () => {
expect(relativeColor(relativeColorCases[i].in.start, relativeColorCases[i].in.end)).toStrictEqual(relativeColorCases[i].out);
});
}
const durationStringCases = [
{ in: 0*1000, out: '00:00' },
{ in: 10*1000, out: '00:10' },
{ in: 100*1000, out: '01:40' },
{ in: 121*1000, out: '02:01' },
{ in: 2*60*60*1000, out: '02:00:00' },
{ in: 999*60*60*1000, out: '999:00:00' },
];
for(let i in durationStringCases) {
test(`durationString should output ${durationStringCases[i].out} for an input of ${durationStringCases[i].in}`, () => {
expect(durationString(durationStringCases[i].in)).toStrictEqual(durationStringCases[i].out);
});
}
});

@ -15,7 +15,7 @@ module.exports = async (req, res, next) => {
const { name, description } = req.body
, uri = req.body.uri.toLowerCase()
, tags = req.body.tags.split(/\r?\n/).filter(n => n)
, tags = (req.body.tags || '').split(/\r?\n/).filter(n => n)
, owner = req.session.user;
if (restrictedURIs.has(uri)) {

@ -527,7 +527,7 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
}
//for cyclic threads, delete posts beyond bump limit
if (thread && thread.cyclic && thread.replyposts > replyLimit) {
if (thread && thread.cyclic && thread.replyposts >= replyLimit) {
const cyclicOverflowPosts = await Posts.db.find({
'thread': data.thread,
'board': req.params.board

13155
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -52,8 +52,14 @@
"uid-safe": "^2.1.5",
"unix-crypt-td-js": "^1.1.4"
},
"devDependencies": {
"jest": "^27.5.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "npm run test:unit",
"test:unit": "jest --verbose --testPathIgnorePatterns=./test/",
"test:integration": "jest --verbose --testPathPattern=./test/",
"test:all": "jest --verbose",
"setup": "npm i -g pm2 gulp && gulp generate-favicon && gulp default",
"start": "pm2 start ecosystem.config.js --env production",
"start-dev": "pm2 start ecosystem.config.js --env development"

@ -153,7 +153,7 @@ const config = require(__dirname+'/config.js')
})
//listen
server.listen(port, '127.0.0.1', () => {
server.listen(port, (process.env.JSCHAN_IP || '127.0.0.1'), () => {
new CachePugTemplates({ app, views }).start();
debugLogs && console.log(`LISTENING ON :${port}`);
//let PM2 know that this is ready for graceful reloads and to serialise startup

@ -0,0 +1,658 @@
const fetch = require('node-fetch');
const FormData = require('form-data');
const fs = require('fs-extra');
module.exports = () => describe('Test post modactions', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
.then(res => res.json())
.then(json => json.token);
});
jest.setTimeout(5*60*1000); //give a generous timeout
test('make new 10 threads with 10 replies each', async () => {
const threadParams = new URLSearchParams();
threadParams.append('message', Math.random());
threadParams.append('captcha', '000000');
const promises = [];
for (let t = 0; t < 10; t++) {
const promise = fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: threadParams
}).then(async (response) => {
expect(response.ok).toBe(true);
const thread = (await response.json()).postId;
for (let r = 0; r < 10; r++) {
const replyParams = new URLSearchParams();
replyParams.append('message', Math.random());
replyParams.append('thread', thread);
replyParams.append('captcha', '000000');
const promise2 = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: replyParams
}).then(async (response2) => {
expect(response2.ok).toBe(true);
});
promises.push(promise2);
}
});
promises.push(promise);
}
await Promise.all(promises); //wait for all posts to go through
jest.setTimeout(5*1000); //back to normal timeout
await new Promise((resolve) => { setTimeout(resolve, 1000) }); //wait for async builds
});
test('bumplock, lock, and sticky 5 random posts from /test/', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const params = new URLSearchParams({
_csrf: csrfToken,
sticky: '1',
bumplock: '1',
lock: '1',
});
for (let i = 0; i < 5; i++) {
const thread = threads[Math.floor(Math.random() * threads.length)];
params.append('checkedposts', thread.postId);
}
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
await new Promise((resolve) => { setTimeout(resolve, 1000) }); //wait for async builds
});
test('remove the bumplock, lock and sticky on any threads', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const params = new URLSearchParams({
_csrf: csrfToken,
sticky: '0',
bumplock: '1', //these are a "toggle",
lock: '1',
});
threads.filter(t => t.locked).forEach(t => params.append('checkedposts', t.postId));
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('delete 5 random posts from /test/', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const params = new URLSearchParams({
_csrf: csrfToken,
delete: '1',
});
for (let i = 0; i < 5; i++) {
const thread = threads[Math.floor(Math.random() * threads.length)];
params.append('checkedposts', thread.postId);
}
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('lower reply limit', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
name: 'test',
description: '',
tags: '',
announcement: '',
theme: 'yotsuba-b',
code_theme: 'ir-black',
custom_css: '',
enable_tegaki: 'true',
max_files: '5',
files_allow_video: 'true',
files_allow_image: 'true',
files_allow_animated_image: 'true',
files_allow_audio: 'true',
user_post_spoiler: 'true',
user_post_unlink: 'true',
default_name: 'Anon',
user_post_delete: 'true',
min_thread_message_length: '0',
min_reply_message_length: '0',
max_thread_message_length: '20000',
max_reply_message_length: '20000',
thread_limit: '100',
reply_limit: '20',
bump_limit: '500',
file_r9k_mode: '0',
message_r9k_mode: '0',
delete_protection_count: '0',
delete_protection_age: '0',
lock_mode: '0',
captcha_mode: '2',
pph_trigger: '50',
pph_trigger_action: '2',
tph_trigger: '10',
tph_trigger_action: '1',
lock_reset: '0',
captcha_reset: '0',
filters: '',
filter_mode: '0',
ban_duration: '0'
});
const response = await fetch('http://localhost/forms/board/test/settings', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
});
expect(response.status).toBe(200);
});
jest.setTimeout(5*60*1000); //give generous timeout
test('make new cyclic thread and check it prunes replies after the limit', async () => {
//new thread
const threadParams = new URLSearchParams();
threadParams.append('message', Math.random());
threadParams.append('captcha', '000000');
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: threadParams
});
expect(response.ok).toBe(true);
const thread = (await response.json()).postId;
//make it cyclic
const params = new URLSearchParams({
_csrf: csrfToken,
cyclic: '1',
checkedposts: thread,
});
const response2 = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response2.ok).toBe(true);
//make the replies
const promises = [];
for (let r = 0; r < 25; r++) {
const replyParams = new URLSearchParams();
replyParams.append('message', Math.random());
replyParams.append('thread', thread);
replyParams.append('captcha', '000000');
const promise = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: replyParams
}).then(response3 => {
expect(response3.ok).toBe(true);
});
promises.push(promise);
}
await Promise.all(promises); //wait for all posts to go through
//check the replies in json
const response3 = await fetch(`http://localhost/test/thread/${thread}.json`).then(res => res.json());
expect(response3.replies.length).toBe(20);
jest.setTimeout(5*1000); //back to normal timeout
});
test('move/merge a thread', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const params = new URLSearchParams({
_csrf: csrfToken,
checkedposts: threads[Math.floor(threads.length/2)].postId,
move: '1',
move_to_thread: threads[0].postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
let reportedPost;
test('test local report + global report', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThread = threads[Math.floor(Math.random() * threads.length)];
reportedPost = randomThread;
const randomThreadId = randomThread.postId;
const params = new URLSearchParams({
_csrf: csrfToken,
report: '1',
global_report: '1',
report_reason: 'test',
checkedposts: randomThreadId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('test post editing, add a bunch of markdown', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
const params = new URLSearchParams({
_csrf: csrfToken,
board: 'test',
postId: randomThreadId,
subject: 'NEW SUBJECT',
email: 'NEW EMAIL',
name: 'NEW NAME',
message: `>greentext
<pinktext
==title==
''bold''
__underline__
~strikethrough~~
||spoiler text||
**italic**
(((detected)))
##2d9+3
https://example.com
[Board Rules](https://your.imageboard/a/custompage/rules.html)(staff only)
>>123
>>>/test/
>>>/test/123
\`inline monospace\`
[code]language
int main() {...}
[/code]
[code]aa
_
( ω) Let's try that again.
[/code]`,
log_message: 'test edit',
});
const response = await fetch('http://localhost/forms/editpost', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
let postId;
test('make post with image', async () => {
const threadParams = new FormData({
message: Math.random(),
captcha: '000000',
});
const filePath = 'gulp/res/img/flags.png';
const fileSizeInBytes = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
threadParams.append('file', fileStream, { knownLength: fileSizeInBytes });
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
...threadParams.getHeaders(),
},
method: 'POST',
body: threadParams
});
expect(response.ok).toBe(true);
postId = (await response.json()).postId;
});
test('spoiler the file in a post', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
spoiler: '1',
checkedposts: postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('unlink file', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
unlink_file: '1',
checkedposts: postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('make post with already spoilered image', async () => {
const threadParams = new FormData({
message: Math.random(),
captcha: '000000',
spoiler_all: '1',
});
const filePath = 'gulp/res/img/flags.png';
const fileSizeInBytes = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
threadParams.append('file', fileStream, { knownLength: fileSizeInBytes });
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
...threadParams.getHeaders(),
},
method: 'POST',
body: threadParams
});
expect(response.ok).toBe(true);
postId = (await response.json()).postId;
});
test('delete file', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
delete_file: '1',
checkedposts: postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('ban reporter for local report', async () => {
const reportsPage = await fetch('http://dev-jschan/test/manage/reports.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedreports" value="';
const checkIndex = reportsPage.indexOf(checkString);
const reportId = reportsPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
const params = new URLSearchParams({
_csrf: csrfToken,
report_ban: '1',
checkedposts: reportedPost.postId,
checkedreports: reportId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('dismiss local report', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
dismiss: '1',
checkedposts: reportedPost.postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('ban reporter for global report', async () => {
const reportsPage = await fetch('http://dev-jschan/globalmanage/reports.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedreports" value="';
const checkIndex = reportsPage.indexOf(checkString);
const reportId = reportsPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
const params = new URLSearchParams({
_csrf: csrfToken,
global_report_ban: '1',
globalcheckedposts: reportedPost._id,
checkedreports: reportId,
});
const response = await fetch('http://localhost/forms/global/actions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('remove global report ban', async () => {
const banPage = await fetch('http://localhost/globalmanage/bans.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedbans" value="';
const checkIndex = banPage.indexOf(checkString);
banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbans: banId,
option: 'unban',
});
const response = await fetch('http://localhost/forms/global/editbans', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('test banning', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
const params = new URLSearchParams({
_csrf: csrfToken,
ban: '1',
checkedposts: randomThreadId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
test('test global ban', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
const params = new URLSearchParams({
_csrf: csrfToken,
global_ban: '1',
checkedposts: randomThreadId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
let banId
test('deny ban appeal', async () => {
const banPage = await fetch('http://localhost/globalmanage/bans.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedbans" value="';
const checkIndex = banPage.indexOf(checkString);
banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbans: banId,
option: 'deny_appeal',
});
const response = await fetch('http://localhost/forms/global/editbans', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('edit ban duration', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbans: banId,
option: 'edit',
ban_duration: '3d',
});
const response = await fetch('http://localhost/forms/global/editbans', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('remove ban', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbans: banId,
option: 'unban',
});
const response = await fetch('http://localhost/forms/global/editbans', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('test ban + qrange + non-appealable + show post in ban + hide name in modlog + modlog message + ban reason', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
const params = new URLSearchParams({
_csrf: csrfToken,
ban: '1',
ban_q: '1',
ban_reason: 'test',
log_message: 'test',
preserve_post: '1',
hide_name: '1',
no_appeal: '1',
checkedposts: randomThreadId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
});
});

@ -0,0 +1,286 @@
const fetch = require('node-fetch');
const FormData = require('form-data');
const fs = require('fs-extra');
module.exports = () => describe('test some global form submissions', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
.then(res => res.json())
.then(json => json.token);
});
let bannerId, assetId;
test('add banner', async () => {
const fileParams = new FormData();
const filePath = 'gulp/res/img/flags.png';
const fileSizeInBytes = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
fileParams.append('_csrf', csrfToken);
fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
const response = await fetch('http://localhost/forms/board/test/addbanners', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
...fileParams.getHeaders(),
},
method: 'POST',
body: fileParams
});
expect(response.ok).toBe(true);
const bannerPage = await fetch('http://localhost/test/manage/assets.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedbanners" value="';
const checkIndex = bannerPage.indexOf(checkString);
const bannerSubstring = bannerPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+70);
bannerId = bannerSubstring.substring(0, bannerSubstring.lastIndexOf('"'));
});
test('add flag', async () => {
const fileParams = new FormData();
const filePath = 'gulp/res/img/flags.png';
const fileSizeInBytes = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
fileParams.append('_csrf', csrfToken);
fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
const response = await fetch('http://localhost/forms/board/test/addflags', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
...fileParams.getHeaders(),
},
method: 'POST',
body: fileParams
});
expect(response.ok).toBe(true);
flagId = 'flags';
});
test('add asset', async () => {
const fileParams = new FormData();
const filePath = 'gulp/res/img/flags.png';
const fileSizeInBytes = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
fileParams.append('_csrf', csrfToken);
fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
const response = await fetch('http://localhost/forms/board/test/addassets', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
...fileParams.getHeaders(),
},
method: 'POST',
body: fileParams
});
expect(response.ok).toBe(true);
const assetPage = await fetch('http://localhost/test/manage/assets.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkString = 'name="checkedassets" value="';
const checkIndex = assetPage.indexOf(checkString);
const assetSubstring = assetPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+70);
assetId = assetSubstring.substring(0, assetSubstring.lastIndexOf('"'));
});
test('delete banner', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedbanners: bannerId,
});
const response = await fetch('http://localhost/forms/board/test/deletebanners', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('delete flag', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedflags: flagId,
});
const response = await fetch('http://localhost/forms/board/test/deleteflags', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('delete asset', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedassets: assetId,
});
const response = await fetch('http://localhost/forms/board/test/deleteassets', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('add custompage', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
page: 'test',
title: 'test',
message: `==This is a test custompage==
wow
>very cool
testing 123`
});
const response = await fetch('http://localhost/forms/board/test/addcustompages', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
const response2 = await fetch('http://localhost/test/custompage/test.html');
expect(response2.ok).toBe(true);
});
test('edit and rename/move custompage', async () => {
const custompagesPage = await fetch('http://localhost/test/manage/custompages.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const custompageId = custompagesPage.match(/href="\/test\/manage\/editcustompage\/([0-9a-f]{24}).html"/)[1];
const params = new URLSearchParams({
_csrf: csrfToken,
page_id: custompageId,
page: 'test2',
title: 'test2',
message: 'test2',
});
const response = await fetch('http://localhost/forms/board/test/editcustompage', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
const response2 = await fetch('http://localhost/test/custompage/test.html');
expect(response2.status).toBe(404);
const response3 = await fetch('http://localhost/test/custompage/test2.html');
expect(response3.status).toBe(200);
});
test('delete custompage', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedcustompages: 'test2',
});
const response = await fetch('http://localhost/forms/board/test/deletecustompages', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
const response2 = await fetch('http://localhost/test/custompage/test2.html');
expect(response2.status).toBe(404);
});
test('add staff', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
username: 'test',
});
const response = await fetch('http://localhost/forms/board/test/addstaff', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('edit staff permission', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
username: 'test',
MANAGE_BOARD_BANS: '1',
MANAGE_BOARD_LOGS: '1',
MANAGE_BOARD_SETTINGS: '1',
});
const response = await fetch('http://localhost/forms/board/test/editstaff', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('remove staff', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedstaff: 'test',
});
const response = await fetch('http://localhost/forms/board/test/deletestaff', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
});

@ -0,0 +1,109 @@
const fetch = require('node-fetch');
module.exports = () => describe('delete tests and cleanup', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
.then(res => res.json())
.then(json => json.token);
});
test('delete_ip_thread test', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
//delete a reply and check if the OP is deleted (ip is the same for all posts atm)
const randomThreadId = threads.find(t => t.replyposts > 0).postId;
const thread = await fetch(`http://localhost/test/thread/${randomThreadId}.json`).then(res => res.json());
const post = thread.replies[Math.floor(Math.random() * thread.replies.length)];
const params = new URLSearchParams({
_csrf: csrfToken,
delete_ip_thread: '1',
checkedposts: post.postId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
const response2 = await fetch(`http://localhost/test/thread/${randomThreadId}.json`);
expect(response2.status).toBe(404);
});
test('delete_ip_board test', async () => {
const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
const params = new URLSearchParams({
_csrf: csrfToken,
delete_ip_board: '1',
checkedposts: randomThreadId,
});
const response = await fetch('http://localhost/forms/board/test/modactions', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
});
expect(response.ok).toBe(true);
await new Promise((resolve) => { setTimeout(resolve, 1000) }); //wait for async builds
const response2 = await fetch('http://localhost/test/catalog.json').then(res => res.json());
expect(response2.length).toBe(0);
});
test('delete test board', async () => {
const params = new URLSearchParams();
params.append('_csrf', csrfToken);
params.append('uri', 'test');
params.append('confirm', 'true');
const response = await fetch('http://localhost/forms/board/test/deleteboard', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
});
expect([200, 404]).toContain(response.status)
});
test('delete test account', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkedaccounts: 'test',
});
const response = await fetch('http://localhost/forms/global/deleteaccounts', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
});

@ -0,0 +1,139 @@
const fetch = require('node-fetch');
module.exports = () => describe('test some global form submissions', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
.then(res => res.json())
.then(json => json.token);
});
let newsId;
test('add news post', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
title: 'Newspost title~',
message: `==This is news==
wow
>very cool
testing 123`
});
const response = await fetch('http://localhost/forms/global/addnews', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
const newsPage = await fetch('http://localhost/globalmanage/news.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const checkIndex = newsPage.indexOf('name="checkednews" value="');
newsId = newsPage.substring(checkIndex+26, checkIndex+26+24);
});
test('edit news post', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
news_id: newsId,
title: 'edited title',
message: 'edited message',
});
const response = await fetch('http://localhost/forms/global/editnews', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
const newsPage = await fetch('http://localhost/globalmanage/news.html', {
headers: {
'cookie': sessionCookie,
},
}).then(res => res.text());
const editTextIndex = newsPage.indexOf('edited title');
expect(editTextIndex).not.toBe(-1);
});
test('delete news post', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
checkednews: newsId,
});
const response = await fetch('http://localhost/forms/global/deletenews', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
test('register test account', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
username: 'test',
password: 'test',
passwordconfirm: 'test',
captcha: '000000',
});
const response = await fetch('http://localhost/forms/register', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.status).toBe(302);
});
test('edit account permission', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
username: 'test',
template: 'fz/P4B//gAA=',
});
const response = await fetch('http://localhost/forms/global/editaccount', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
})
expect(response.ok).toBe(true);
});
});

@ -0,0 +1,9 @@
describe('run integration tests', () => {
require('./setup.js')();
require('./posting.js')();
require('./global.js')();
require('./actions.js')();
require('./board.js')();
require('./pages.js')();
require('./cleanup.js')();
})

@ -0,0 +1,59 @@
const fetch = require('node-fetch');
module.exports = () => describe('Test loading a bunch of pages', () => {
const urls = [
'boards.html',
'boards.json',
'boards.json?search=test&sort=popularity&direction=desc',
'boards.html?search=test&sort=popularity&direction=desc',
'overboard.html',
'overboard.json',
'overboard.html?add=test&rem=abc',
'overboard.json?add=test&rem=abc',
'index.html',
'news.html',
'rules.html',
'faq.html',
'globalmanage/recent.html',
'globalmanage/reports.html',
'globalmanage/bans.html',
'globalmanage/boards.html',
'globalmanage/globallogs.html',
'globalmanage/accounts.html',
'globalmanage/roles.html',
'globalmanage/news.html',
'globalmanage/settings.html',
]
let sessionCookie
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
});
urls.forEach(u => {
test(u, async () => {
const response = await fetch(`http://localhost/${u}`, {
headers: {
cookie: sessionCookie,
},
});
expect(response.ok).toBe(true);
});
})
});

@ -0,0 +1,132 @@
const fetch = require('node-fetch');
module.exports = () => describe('Test posting', () => {
let threadId;
test('post new thread', async () => {
const params = new URLSearchParams();
params.append('message', Math.random());
params.append('captcha', '000000');
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params
});
expect(response.ok).toBe(true);
threadId = (await response.json()).postId;
});
let replyId;
test('post reply', async () => {
const params = new URLSearchParams();
params.append('message', Math.random());
params.append('thread', threadId);
params.append('captcha', '000000');
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params
});
expect(response.ok).toBe(true);
replyId = (await response.json()).postId;
});
test('post reply with quotes', async () => {
const params = new URLSearchParams();
params.append('message', `>>${threadId}
>>${replyId}`);
params.append('thread', threadId);
params.append('captcha', '000000');
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params
});
expect(response.ok).toBe(true);
});
test('post reply with all markdowns', async () => {
const params = new URLSearchParams();
params.append('captcha', '000000');
params.append('message', `>greentext
<pinktext
==title==
''bold''
__underline__
~strikethrough~~
||spoiler text||
**italic**
(((detected)))
##2d9+3
https://example.com
[Board Rules](https://your.imageboard/a/custompage/rules.html)(staff only)
>>${threadId}
>>>/test/
>>>/test/${threadId}
\`inline monospace\`
[code]language
int main() {...}
[/code]
[code]aa
_
( ω) Let's try that again.
[/code]
`);
params.append('thread', threadId);
const response = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params
});
expect(response.ok).toBe(true);
});
jest.setTimeout(5*60*1000); //give a generous timeout
test('post 100 threads with 10 replies each', async () => {
const threadParams = new URLSearchParams();
threadParams.append('message', Math.random());
threadParams.append('captcha', '000000');
const promises = [];
for (let t = 0; t < 100; t++) {
const promise = fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: threadParams
}).then(async (response) => {
expect(response.ok).toBe(true);
const thread = (await response.json()).postId;
for (let r = 0; r < 10; r++) {
const replyParams = new URLSearchParams();
replyParams.append('message', Math.random());
replyParams.append('thread', thread);
replyParams.append('captcha', '000000');
const promise2 = await fetch('http://localhost/forms/board/test/post', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: replyParams
}).then(async (response2) => {
expect(response2.ok).toBe(true);
});
promises.push(promise2);
}
});
promises.push(promise);
}
await Promise.all(promises); //wait for all posts to go through
jest.setTimeout(5*1000); //back to normal timeout
});
});

@ -0,0 +1,246 @@
const fetch = require('node-fetch');
module.exports = () => describe('login and create test board', () => {
let sessionCookie
, csrfToken;
test('login as admin', async () => {
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', process.env.TEST_ADMIN_PASSWORD);
const response = await fetch('http://localhost/forms/login', {
headers: {
'x-using-xhr': 'true',
},
method: 'POST',
body: params,
redirect: 'manual',
});
const rawHeaders = response.headers.raw();
expect(rawHeaders['set-cookie']).toBeDefined();
expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
sessionCookie = rawHeaders['set-cookie'][0];
csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
.then(res => res.json())
.then(json => json.token);
});
test('delete test board if exists', async () => {
const params = new URLSearchParams();
params.append('_csrf', csrfToken);
params.append('uri', 'test');
params.append('confirm', 'true');
const response = await fetch('http://localhost/forms/board/test/deleteboard', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
});
expect([200, 404]).toContain(response.status)
});
test('create test board', async () => {
const params = new URLSearchParams();
params.append('uri', 'test');
params.append('name', 'test');
const response = await fetch('http://localhost/forms/create', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
});
expect([302, 409]).toContain(response.status)
});
test('change global settings, disable antispam', async () => {
const params = new URLSearchParams({
_csrf: csrfToken,
allowed_hosts: '',
prune_ips: '0',
global_announcement: '',
country_code_header: 'x-country-code',
ip_header: 'x-real-ip',
meta_site_name: '',
meta_url: '',
stats_count_anonymizers: 'true',
prune_immediately: 'true',
thumb_extension: '.jpg',
cache_templates: 'true',
lock_wait: '3000',
overboard_limit: '20',
overboard_catalog_limit: '100',
allow_custom_overboard: 'true',
archive_links: 'https://archive.today/?run=1&url=%s',
reverse_links: 'https://tineye.com/search?url=%s',
prune_modlogs: '30',
default_ban_duration: '31536000000',
quote_limit: '25',
preview_replies: '5',
sticky_preview_replies: '5',
early_404_fraction: '3',
early_404_replies: '5',
max_recent_news: '5',
captcha_options_type: 'text',
captcha_options_grid_image_size: '120',
captcha_options_grid_size: '4',
captcha_options_grid_icon_y_offset: '15',
captcha_options_generate_limit: '250',
captcha_options_num_distorts_min: '2',
captcha_options_num_distorts_max: '3',
captcha_options_distortion: '7',
block_bypass_force_anonymizers: 'true',
block_bypass_expire_after_uses: '50',
block_bypass_expire_after_time: '86400000',
filters: '',
strict_filtering: 'true',
filter_mode: '0',
ban_duration: '0',
flood_timers_same_content_same_ip: '0',
flood_timers_same_content_any_ip: '0',
flood_timers_any_content_same_ip: '0',
dnsbl_blacklists: 'tor.dan.me.uk\nzen.spamhaus.org',
dnsbl_cache_time: '3600',
rate_limit_cost_captcha: '10',
rate_limit_cost_board_settings: '30',
rate_limit_cost_edit_post: '30',
highlight_options_language_subset: 'javascript\n' +
'typescript\n' +
'perl\n' +
'js\n' +
'c++\n' +
'c\n' +
'java\n' +
'kotlin\n' +
'php\n' +
'h\n' +
'csharp\n' +
'bash\n' +
'sh\n' +
'zsh\n' +
'python\n' +
'ruby\n' +
'css\n' +
'html\n' +
'json\n' +
'golang\n' +
'rust\n' +
'aa',
highlight_options_threshold: '5',
themes: '',
code_themes: '',
board_defaults_theme: 'yotsuba-b',
board_defaults_code_theme: 'ir-black',
global_limits_custom_css_enabled: 'true',
global_limits_custom_css_filters: '@\nurl(',
global_limits_custom_css_strict: 'true',
global_limits_custom_css_max: '10000',
global_limits_field_length_name: '100',
global_limits_field_length_email: '100',
global_limits_field_length_subject: '100',
global_limits_field_length_postpassword: '100',
global_limits_field_length_message: '20000',
global_limits_field_length_report_reason: '100',
global_limits_field_length_ban_reason: '100',
global_limits_field_length_log_message: '100',
global_limits_field_length_uri: '50',
global_limits_field_length_boardname: '50',
global_limits_field_length_description: '100',
global_limits_multi_input_posts_anon: '20',
global_limits_multi_input_posts_staff: '100',
frontend_script_default_embeds_enabled: 'true',
frontend_script_default_hide_recursive: 'true',
frontend_script_default_smooth_scrolling: 'true',
frontend_script_default_thread_watcher: 'true',
frontend_script_default_volume: '100',
frontend_script_default_loop: 'true',
frontend_script_default_image_loading_bars: 'true',
frontend_script_default_live: 'true',
frontend_script_default_local_time: 'true',
frontend_script_default_relative_time: 'true',
frontend_script_default_show_yous: 'true',
frontend_script_default_notifications_yous_only: 'true',
frontend_script_default_tegaki_width: '500',
frontend_script_default_tegaki_height: '500',
audio_thumbnails: 'true',
ffmpeg_gif_thumbnails: 'true',
thumb_size: '250',
video_thumb_percentage: '5',
other_mime_types: 'text/plain\napplication/pdf',
space_file_name_replacement: '_',
global_limits_reply_limit_min: '10',
global_limits_reply_limit_max: '1000',
global_limits_thread_limit_min: '100',
global_limits_thread_limit_max: '200',
global_limits_bump_limit_min: '10',
global_limits_bump_limit_max: '1000',
global_limits_post_files_max: '5',
global_limits_post_files_size_max: '10485760',
global_limits_custom_pages_max_length: '10000',
global_limits_custom_pages_max: '10',
global_limits_banner_files_width: '500',
global_limits_banner_files_height: '500',
global_limits_banner_files_size_max: '10485760',
global_limits_banner_files_max: '10',
global_limits_banner_files_total: '100',
global_limits_flag_files_size_max: '1048576',
global_limits_flag_files_max: '10',
global_limits_flag_files_total: '100',
global_limits_asset_files_size_max: '10485760',
global_limits_asset_files_max: '10',
global_limits_asset_files_total: '50',
webring_proxy_address: '',
webring_following: '',
webring_logos: '',
webring_blacklist: '',
board_defaults_message_r9k_mode: '0',
board_defaults_file_r9k_mode: '0',
board_defaults_lock_mode: '0',
board_defaults_captcha_mode: '0',
board_defaults_pph_trigger: '50',
board_defaults_pph_trigger_action: '2',
board_defaults_tph_trigger: '10',
board_defaults_tph_trigger_action: '1',
board_defaults_lock_reset: '0',
board_defaults_captcha_reset: '0',
board_defaults_default_name: 'Anon',
board_defaults_enable_tegaki: 'true',
board_defaults_user_post_delete: 'true',
board_defaults_user_post_spoiler: 'true',
board_defaults_user_post_unlink: 'true',
board_defaults_thread_limit: '100',
board_defaults_reply_limit: '1000',
board_defaults_bump_limit: '500',
board_defaults_max_files: '5',
board_defaults_min_thread_message_length: '0',
board_defaults_min_reply_message_length: '0',
board_defaults_max_thread_message_length: '20000',
board_defaults_max_reply_message_length: '20000',
board_defaults_delete_protection_count: '0',
board_defaults_delete_protection_age: '0',
board_defaults_filter_mode: '0',
board_defaults_filter_ban_duration: '0',
board_defaults_allowed_file_types_video: 'true',
board_defaults_allowed_file_types_image: 'true',
board_defaults_allowed_file_types_animated_image: 'true',
board_defaults_allowed_file_types_audio: 'true',
});
const response = await fetch('http://localhost/forms/global/settings', {
headers: {
'x-using-xhr': 'true',
'cookie': sessionCookie,
},
method: 'POST',
body: params,
redirect: 'manual',
});
expect(response.status).toBe(200);
});
});
Loading…
Cancel
Save