Merge remote-tracking branch 'upstream/master'

This commit is contained in:
nothink 2018-07-19 11:31:14 +09:00
commit 1bb7ac0c44
22 changed files with 1510 additions and 1456 deletions

View file

@ -84,29 +84,32 @@ user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox
### Preparing `szurubooru` for first run ### Preparing `szurubooru` for first run
1. Configure things: 1. Compile the frontend:
```console ```console
user@host:szuru$ cd client
user@host:szuru/client$ node build.js
```
You can include the flags `--no-transpile` to disable the JavaScript
transpiler, which provides compatibility with older browsers, and
`--debug` to generate JS source mappings.
2. Configure things:
```console
user@host:szuru/client$ cd ..
user@host:szuru$ cp config.yaml.dist config.yaml user@host:szuru$ cp config.yaml.dist config.yaml
user@host:szuru$ vim config.yaml user@host:szuru$ vim config.yaml
``` ```
Pay extra attention to these fields: Pay extra attention to these fields:
- base URL,
- API URL,
- data directory, - data directory,
- data URL, - data URL,
- database, - database,
- the `smtp` section. - the `smtp` section.
2. Compile the frontend:
```console
user@host:szuru$ cd client
user@host:szuru/client$ npm run build
```
3. Upgrade the database: 3. Upgrade the database:
```console ```console
@ -121,7 +124,7 @@ user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox
4. Run the tests: 4. Run the tests:
```console ```console
(python_modules) user@host:szuru/server$ ./test (python_modules) user@host:szuru/server$ pytest
``` ```
It is recommended to rebuild the frontend after each change to configuration. It is recommended to rebuild the frontend after each change to configuration.
@ -140,6 +143,11 @@ meant to be exposed directly to the end users.
The API should be exposed using WSGI server such as `waitress`, `gunicorn` or The API should be exposed using WSGI server such as `waitress`, `gunicorn` or
similar. Other configurations might be possible but I didn't pursue them. similar. Other configurations might be possible but I didn't pursue them.
API calls are made to the relative URL `/api/`. Your HTTP server should be
configured to proxy this URL format to the WSGI server. Some users may prefer
to use a dedicated reverse proxy for this, to incorporate additional features
such as load balancing and SSL.
Note that the API URL in the virtual host configuration needs to be the same as Note that the API URL in the virtual host configuration needs to be the same as
the one in the `config.yaml`, so that client knows how to access the backend! the one in the `config.yaml`, so that client knows how to access the backend!
@ -177,8 +185,6 @@ server {
**`config.yaml`**: **`config.yaml`**:
```yaml ```yaml
api_url: 'http://example.com/api/'
base_url: 'http://example.com/'
data_url: 'http://example.com/data/' data_url: 'http://example.com/data/'
data_dir: '/srv/www/booru/client/public/data' data_dir: '/srv/www/booru/client/public/data'
``` ```

View file

@ -1 +1 @@
{ "presets": ["es2015"] } { "presets": ["env"] }

View file

@ -5,20 +5,6 @@ const glob = require('glob');
const path = require('path'); const path = require('path');
const util = require('util'); const util = require('util');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const camelcase = require('camelcase');
function convertKeysToCamelCase(input) {
let result = {};
Object.keys(input).map((key, _) => {
const value = input[key];
if (value !== null && value.constructor == Object) {
result[camelcase(key)] = convertKeysToCamelCase(value);
} else {
result[camelcase(key)] = value;
}
});
return result;
}
function readTextFile(path) { function readTextFile(path) {
return fs.readFileSync(path, 'utf-8'); return fs.readFileSync(path, 'utf-8');
@ -29,37 +15,27 @@ function writeFile(path, content) {
} }
function getVersion() { function getVersion() {
return execSync('git describe --always --dirty --long --tags') let build_info = process.env.BUILD_INFO;
.toString() if (build_info) {
.trim(); return build_info.trim();
} else {
try {
build_info = execSync('git describe --always --dirty --long --tags')
.toString();
} catch (e) {
console.warn('Cannot find build version');
return 'unknown';
}
return build_info.trim();
}
} }
function getConfig() { function getConfig() {
const yaml = require('js-yaml'); let config = {
const merge = require('merge'); meta: {
const camelcaseKeys = require('camelcase-keys'); version: getVersion(),
buildDate: new Date().toUTCString()
function parseConfigFile(path) { }
let result = yaml.load(readTextFile(path, 'utf-8'));
return convertKeysToCamelCase(result);
}
let config = parseConfigFile('../config.yaml.dist');
try {
const localConfig = parseConfigFile('../config.yaml');
config = merge.recursive(config, localConfig);
} catch (e) {
console.warn('Local config does not exist, ignoring');
}
config.canSendMails = !!config.smtp.host;
delete config.secret;
delete config.smtp;
delete config.database;
config.meta = {
version: getVersion(),
buildDate: new Date().toUTCString(),
}; };
return config; return config;
@ -70,11 +46,11 @@ function copyFile(source, target) {
} }
function minifyJs(path) { function minifyJs(path) {
return require('uglify-es').minify(fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code; return require('terser').minify(fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
} }
function minifyCss(css) { function minifyCss(css) {
return require('csso').minify(css); return require('csso').minify(css).css;
} }
function minifyHtml(html) { function minifyHtml(html) {
@ -85,15 +61,11 @@ function minifyHtml(html) {
}).trim(); }).trim();
} }
function bundleHtml(config) { function bundleHtml() {
const underscore = require('underscore'); const underscore = require('underscore');
const babelify = require('babelify'); const babelify = require('babelify');
const baseHtml = readTextFile('./html/index.htm', 'utf-8'); const baseHtml = readTextFile('./html/index.htm', 'utf-8');
const finalHtml = baseHtml writeFile('./public/index.htm', minifyHtml(baseHtml));
.replace(
/(<title>)(.*)(<\/title>)/,
util.format('$1%s$3', config.name));
writeFile('./public/index.htm', minifyHtml(finalHtml));
glob('./html/**/*.tpl', {}, (er, files) => { glob('./html/**/*.tpl', {}, (er, files) => {
let compiledTemplateJs = '\'use strict\'\n'; let compiledTemplateJs = '\'use strict\'\n';
@ -143,7 +115,7 @@ function bundleCss() {
}); });
} }
function bundleJs(config) { function bundleJs() {
const browserify = require('browserify'); const browserify = require('browserify');
const external = [ const external = [
'underscore', 'underscore',
@ -170,7 +142,7 @@ function bundleJs(config) {
for (let lib of external) { for (let lib of external) {
b.require(lib); b.require(lib);
} }
if (config.transpile) { if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill')); b.add(require.resolve('babel-polyfill'));
} }
writeJsBundle( writeJsBundle(
@ -179,15 +151,15 @@ function bundleJs(config) {
if (!process.argv.includes('--no-app-js')) { if (!process.argv.includes('--no-app-js')) {
let outputFile = fs.createWriteStream('./public/js/app.min.js'); let outputFile = fs.createWriteStream('./public/js/app.min.js');
let b = browserify({debug: config.debug}); let b = browserify({debug: process.argv.includes('--debug')});
if (config.transpile) { if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify'); b = b.transform('babelify');
} }
writeJsBundle( writeJsBundle(
b.external(external).add(files), b.external(external).add(files),
'./public/js/app.min.js', './public/js/app.min.js',
'Bundled app JS', 'Bundled app JS',
!config.debug); !process.argv.includes('--debug'));
} }
}); });
} }
@ -217,11 +189,11 @@ const config = getConfig();
bundleConfig(config); bundleConfig(config);
bundleBinaryAssets(); bundleBinaryAssets();
if (!process.argv.includes('--no-html')) { if (!process.argv.includes('--no-html')) {
bundleHtml(config); bundleHtml();
} }
if (!process.argv.includes('--no-css')) { if (!process.argv.includes('--no-css')) {
bundleCss(); bundleCss();
} }
if (!process.argv.includes('--no-js')) { if (!process.argv.includes('--no-js')) {
bundleJs(config); bundleJs();
} }

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset='utf-8'/> <meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
<title><!-- configured in the config file --></title> <title>Loading...</title>
<link href='/css/app.min.css' rel='stylesheet' type='text/css'/> <link href='/css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/> <link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/> <link rel='shortcut icon' type='image/png' href='/img/favicon.png'/>

View file

@ -36,8 +36,8 @@
<section class='search'> <section class='search'>
Search on Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>IQDB</a> &middot; <a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>Google Images</a> <a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section> </section>
<section class='social'> <section class='social'>

View file

@ -2,7 +2,6 @@
const cookies = require('js-cookie'); const cookies = require('js-cookie');
const request = require('superagent'); const request = require('superagent');
const config = require('./config.js');
const events = require('./events.js'); const events = require('./events.js');
const progress = require('./util/progress.js'); const progress = require('./util/progress.js');
const uri = require('./util/uri.js'); const uri = require('./util/uri.js');
@ -257,7 +256,7 @@ class Api extends events.EventTarget {
_getFullUrl(url) { _getFullUrl(url) {
const fullUrl = const fullUrl =
(config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/'); ('/api/' + url).replace(/([^:])\/+/g, '$1/');
const matches = fullUrl.match(/^([^?]*)\??(.*)$/); const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
const baseUrl = matches[1]; const baseUrl = matches[1];
const request = matches[2]; const request = matches[2];

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const api = require('../api.js'); const api = require('../api.js');
const config = require('../config.js');
const events = require('../events.js'); const events = require('../events.js');
const views = require('../util/views.js'); const views = require('../util/views.js');

View file

@ -30,6 +30,7 @@ class Post extends events.EventTarget {
get user() { return this._user; } get user() { return this._user; }
get safety() { return this._safety; } get safety() { return this._safety; }
get contentUrl() { return this._contentUrl; } get contentUrl() { return this._contentUrl; }
get fullContentUrl() { return this._fullContentUrl; }
get thumbnailUrl() { return this._thumbnailUrl; } get thumbnailUrl() { return this._thumbnailUrl; }
get canvasWidth() { return this._canvasWidth || 800; } get canvasWidth() { return this._canvasWidth || 800; }
get canvasHeight() { return this._canvasHeight || 450; } get canvasHeight() { return this._canvasHeight || 450; }
@ -275,6 +276,7 @@ class Post extends events.EventTarget {
_user: response.user, _user: response.user,
_safety: response.safety, _safety: response.safety,
_contentUrl: response.contentUrl, _contentUrl: response.contentUrl,
_fullContentUrl: new URL(response.contentUrl, window.location.href).href,
_thumbnailUrl: response.thumbnailUrl, _thumbnailUrl: response.thumbnailUrl,
_canvasWidth: response.canvasWidth, _canvasWidth: response.canvasWidth,
_canvasHeight: response.canvasHeight, _canvasHeight: response.canvasHeight,

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const settings = require('../models/settings.js'); const settings = require('../models/settings.js');
const config = require('../config.js');
const api = require('../api.js'); const api = require('../api.js');
const uri = require('../util/uri.js'); const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js'); const AbstractList = require('./abstract_list.js');

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const marked = require('marked'); const marked = require('marked');
const config = require('../config.js');
class BaseMarkdownWrapper { class BaseMarkdownWrapper {
preprocess(text) { preprocess(text) {
@ -64,15 +63,12 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
class EntityPermalinkWrapper extends BaseMarkdownWrapper { class EntityPermalinkWrapper extends BaseMarkdownWrapper {
preprocess(text) { preprocess(text) {
// URL-based permalinks // URL-based permalinks
let baseUrl = config.baseUrl.replace(/\/+$/, '');
text = text.replace( text = text.replace(
new RegExp('\\b' + baseUrl + '/post/(\\d+)/?\\b', 'g'), '@$1'); new RegExp('\\b/post/(\\d+)/?\\b', 'g'), '@$1');
text = text.replace( text = text.replace(
new RegExp('\\b' + baseUrl + '/tag/([a-zA-Z0-9_-]+?)/?', 'g'), new RegExp('\\b/tag/([a-zA-Z0-9_-]+?)/?', 'g'), '#$1');
'#$1');
text = text.replace( text = text.replace(
new RegExp('\\b' + baseUrl + '/user/([a-zA-Z0-9_-]+?)/?', 'g'), new RegExp('\\b/user/([a-zA-Z0-9_-]+?)/?', 'g'), '+$1');
'+$1');
text = text.replace( text = text.replace(
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,

2731
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,26 +6,25 @@
"watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --no-vendor-js;c1=$c2;sleep 1;done" "watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --no-vendor-js;c1=$c2;sleep 1;done"
}, },
"dependencies": { "dependencies": {
"babel-polyfill": "^6.26.0", "font-awesome": "^4.7.0",
"babel-preset-es2015": "^6.24.1",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"camelcase": "^2.1.1",
"camelcase-keys": "^4.2.0",
"csso": "^1.8.0",
"font-awesome": "^4.6.1",
"glob": "^7.1.2",
"html-minifier": "^1.3.1",
"ios-inner-height": "^1.0.3", "ios-inner-height": "^1.0.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"js-yaml": "^3.10.0", "marked": "^0.4.0",
"marked": "^0.3.9", "mousetrap": "^1.6.2",
"merge": "^1.2.0",
"mousetrap": "^1.6.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"stylus": "^0.54.2", "superagent": "^3.8.3"
"superagent": "^1.8.3", },
"uglify-es": "^3.3.4", "devDependencies": {
"underscore": "^1.8.3" "babel-core": "^6.26.3",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babelify": "^8.0.0",
"browserify": "^16.2.2",
"csso": "^3.5.1",
"glob": "^7.1.2",
"html-minifier": "^3.5.18",
"stylus": "^0.54.5",
"terser": "^3.7.7",
"underscore": "^1.9.1"
} }
} }

View file

@ -2,12 +2,9 @@
# and override only what you need. # and override only what you need.
name: szurubooru # shown in the website title and on the front page name: szurubooru # shown in the website title and on the front page
debug: 0 # generate source maps for JS debugging? debug: 0 # generate server logs?
show_sql: 0 # show sql in server logs? show_sql: 0 # show sql in server logs?
transpile: 1 # generate bigger JS to support older browsers?
secret: change # used to salt the users' password hashes secret: change # used to salt the users' password hashes
api_url: # where frontend connects to, example: http://api.example.com/
base_url: # used to form links to frontend, example: http://example.com/
data_url: # used to form links to posts and avatars, example: http://example.com/data/ data_url: # used to form links to posts and avatars, example: http://example.com/data/
data_dir: # absolute path for posts and avatars storage, example: /srv/www/booru/client/public/data/ data_dir: # absolute path for posts and avatars storage, example: /srv/www/booru/client/public/data/
user_agent: # user agent name used to download files from the web on behalf of the api users user_agent: # user agent name used to download files from the web on behalf of the api users
@ -45,10 +42,10 @@ smtp:
port: # example: 25 port: # example: 25
user: # example: bot user: # example: bot
pass: # example: groovy123 pass: # example: groovy123
# host can be left empty, in which case it is recommended to fill contactEmail. # host can be left empty, in which case it is recommended to fill contact_email.
contactEmail: # example: bob@example.com. Meant for manual password reset procedures contact_email: # example: bob@example.com. Meant for manual password reset procedures
# used for reverse image search # used for reverse image search

View file

@ -42,7 +42,7 @@ def get_info(
'tagCategoryNameRegex': config.config['tag_category_name_regex'], 'tagCategoryNameRegex': config.config['tag_category_name_regex'],
'defaultUserRank': config.config['default_rank'], 'defaultUserRank': config.config['default_rank'],
'enableSafety': config.config['enable_safety'], 'enableSafety': config.config['enable_safety'],
'contactEmail': config.config['contactEmail'], 'contactEmail': config.config['contact_email'],
'canSendMails': bool(config.config['smtp']['host']), 'canSendMails': bool(config.config['smtp']['host']),
'privileges': 'privileges':
util.snake_case_to_lower_camel_case_keys( util.snake_case_to_lower_camel_case_keys(

View file

@ -13,7 +13,7 @@ MAIL_BODY = (
@rest.routes.get('/password-reset/(?P<user_name>[^/]+)/?') @rest.routes.get('/password-reset/(?P<user_name>[^/]+)/?')
def start_password_reset( def start_password_reset(
_ctx: rest.Context, params: Dict[str, str]) -> rest.Response: ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
user_name = params['user_name'] user_name = params['user_name']
user = users.get_user_by_name_or_email(user_name) user = users.get_user_by_name_or_email(user_name)
if not user.email: if not user.email:
@ -21,13 +21,19 @@ def start_password_reset(
'User %r hasn\'t supplied email. Cannot reset password.' % ( 'User %r hasn\'t supplied email. Cannot reset password.' % (
user_name)) user_name))
token = auth.generate_authentication_token(user) token = auth.generate_authentication_token(user)
url = '%s/password-reset/%s:%s' % (
config.config['base_url'].rstrip('/'), user.name, token) if 'HTTP_ORIGIN' in ctx.env:
url = ctx.env['HTTP_ORIGIN'].rstrip('/')
else:
url = ''
url += '/password-reset/%s:%s' % (user.name, token)
mailer.send_mail( mailer.send_mail(
'noreply@%s' % config.config['name'], 'noreply@%s' % config.config['name'],
user.email, user.email,
MAIL_SUBJECT.format(name=config.config['name']), MAIL_SUBJECT.format(name=config.config['name']),
MAIL_BODY.format(name=config.config['name'], url=url)) MAIL_BODY.format(name=config.config['name'], url=url))
return {} return {}

View file

@ -79,7 +79,7 @@ def validate_config() -> None:
'Default rank %r is not on the list of known ranks' % ( 'Default rank %r is not on the list of known ranks' % (
config.config['default_rank'])) config.config['default_rank']))
for key in ['base_url', 'api_url', 'data_url', 'data_dir']: for key in ['data_url', 'data_dir']:
if not config.config[key]: if not config.config[key]:
raise errors.ConfigError( raise errors.ConfigError(
'Service is not configured: %r is missing' % key) 'Service is not configured: %r is missing' % key)

View file

@ -105,7 +105,7 @@ def update_category_color(category: model.TagCategory, color: str) -> None:
assert category assert category
if not color: if not color:
raise InvalidTagCategoryColorError('Color cannot be empty.') raise InvalidTagCategoryColorError('Color cannot be empty.')
if not re.match(r'^#?[0-9A-Za-z]+$', color): if not re.match(r'^#?[0-9a-z]+$', color):
raise InvalidTagCategoryColorError('Invalid color.') raise InvalidTagCategoryColorError('Invalid color.')
if util.value_exceeds_column_size(color, model.TagCategory.color): if util.value_exceeds_column_size(color, model.TagCategory.color):
raise InvalidTagCategoryColorError('Color is too long.') raise InvalidTagCategoryColorError('Color is too long.')

View file

@ -63,7 +63,7 @@ def _create_context(env: Dict[str, Any]) -> context.Context:
'Could not decode the request body. The JSON ' 'Could not decode the request body. The JSON '
'was incorrect or was not encoded as UTF-8.') 'was incorrect or was not encoded as UTF-8.')
return context.Context(method, path, headers, params, files) return context.Context(env, method, path, headers, params, files)
def application( def application(

View file

@ -11,11 +11,13 @@ Response = Optional[Dict[str, Any]]
class Context: class Context:
def __init__( def __init__(
self, self,
env: Dict[str, Any],
method: str, method: str,
url: str, url: str,
headers: Dict[str, str] = None, headers: Dict[str, str] = None,
params: Request = None, params: Request = None,
files: Dict[str, bytes] = None) -> None: files: Dict[str, bytes] = None) -> None:
self.env = env
self.method = method self.method = method
self.url = url self.url = url
self._headers = headers or {} self._headers = headers or {}

View file

@ -10,6 +10,9 @@ def test_info_api(
auth_user = user_factory(rank=model.User.RANK_REGULAR) auth_user = user_factory(rank=model.User.RANK_REGULAR)
anon_user = user_factory(rank=model.User.RANK_ANONYMOUS) anon_user = user_factory(rank=model.User.RANK_ANONYMOUS)
config_injector({ config_injector({
'name': 'test installation',
'contact_email': 'test@example.com',
'enable_safety': True,
'data_dir': str(directory), 'data_dir': str(directory),
'user_name_regex': '1', 'user_name_regex': '1',
'password_regex': '2', 'password_regex': '2',
@ -21,11 +24,17 @@ def test_info_api(
'test_key2': 'test_value2', 'test_key2': 'test_value2',
'posts:view:featured': 'regular', 'posts:view:featured': 'regular',
}, },
'smtp': {
'host': 'example.com',
}
}) })
db.session.add_all([post_factory(), post_factory()]) db.session.add_all([post_factory(), post_factory()])
db.session.flush() db.session.flush()
expected_config_key = { expected_config_key = {
'name': 'test installation',
'contactEmail': 'test@example.com',
'enableSafety': True,
'userNameRegex': '1', 'userNameRegex': '1',
'passwordRegex': '2', 'passwordRegex': '2',
'tagNameRegex': '3', 'tagNameRegex': '3',
@ -36,6 +45,7 @@ def test_info_api(
'testKey2': 'test_value2', 'testKey2': 'test_value2',
'posts:view:featured': 'regular', 'posts:view:featured': 'regular',
}, },
'canSendMails': True
} }
with fake_datetime('2016-01-01 13:00'): with fake_datetime('2016-01-01 13:00'):

View file

@ -95,6 +95,7 @@ def session(query_logger): # pylint: disable=unused-argument
def context_factory(session): def context_factory(session):
def factory(params=None, files=None, user=None, headers=None): def factory(params=None, files=None, user=None, headers=None):
ctx = rest.Context( ctx = rest.Context(
env={'HTTP_ORIGIN': 'http://example.com'},
method=None, method=None,
url=None, url=None,
headers=headers or {}, headers=headers or {},

View file

@ -6,13 +6,14 @@ from szurubooru.func import net
def test_has_param(): def test_has_param():
ctx = rest.Context(method=None, url=None, params={'key': 'value'}) ctx = rest.Context(env={}, method=None, url=None, params={'key': 'value'})
assert ctx.has_param('key') assert ctx.has_param('key')
assert not ctx.has_param('non-existing') assert not ctx.has_param('non-existing')
def test_get_file(): def test_get_file():
ctx = rest.Context(method=None, url=None, files={'key': b'content'}) ctx = rest.Context(
env={}, method=None, url=None, files={'key': b'content'})
assert ctx.get_file('key') == b'content' assert ctx.get_file('key') == b'content'
with pytest.raises(errors.ValidationError): with pytest.raises(errors.ValidationError):
ctx.get_file('non-existing') ctx.get_file('non-existing')
@ -22,7 +23,7 @@ def test_get_file_from_url():
with unittest.mock.patch('szurubooru.func.net.download'): with unittest.mock.patch('szurubooru.func.net.download'):
net.download.return_value = b'content' net.download.return_value = b'content'
ctx = rest.Context( ctx = rest.Context(
method=None, url=None, params={'keyUrl': 'example.com'}) env={}, method=None, url=None, params={'keyUrl': 'example.com'})
assert ctx.get_file('key') == b'content' assert ctx.get_file('key') == b'content'
net.download.assert_called_once_with('example.com') net.download.assert_called_once_with('example.com')
with pytest.raises(errors.ValidationError): with pytest.raises(errors.ValidationError):
@ -31,6 +32,7 @@ def test_get_file_from_url():
def test_getting_list_parameter(): def test_getting_list_parameter():
ctx = rest.Context( ctx = rest.Context(
env={},
method=None, method=None,
url=None, url=None,
params={'key': 'value', 'list': ['1', '2', '3']}) params={'key': 'value', 'list': ['1', '2', '3']})
@ -43,6 +45,7 @@ def test_getting_list_parameter():
def test_getting_string_parameter(): def test_getting_string_parameter():
ctx = rest.Context( ctx = rest.Context(
env={},
method=None, method=None,
url=None, url=None,
params={'key': 'value', 'list': ['1', '2', '3']}) params={'key': 'value', 'list': ['1', '2', '3']})
@ -55,6 +58,7 @@ def test_getting_string_parameter():
def test_getting_int_parameter(): def test_getting_int_parameter():
ctx = rest.Context( ctx = rest.Context(
env={},
method=None, method=None,
url=None, url=None,
params={'key': '50', 'err': 'invalid', 'list': [1, 2, 3]}) params={'key': '50', 'err': 'invalid', 'list': [1, 2, 3]})
@ -76,7 +80,8 @@ def test_getting_int_parameter():
def test_getting_bool_parameter(): def test_getting_bool_parameter():
def test(value): def test(value):
ctx = rest.Context(method=None, url=None, params={'key': value}) ctx = rest.Context(
env={}, method=None, url=None, params={'key': value})
return ctx.get_param_as_bool('key') return ctx.get_param_as_bool('key')
assert test('1') is True assert test('1') is True
@ -104,7 +109,7 @@ def test_getting_bool_parameter():
with pytest.raises(errors.ValidationError): with pytest.raises(errors.ValidationError):
test(['1', '2']) test(['1', '2'])
ctx = rest.Context(method=None, url=None) ctx = rest.Context(env={}, method=None, url=None)
with pytest.raises(errors.ValidationError): with pytest.raises(errors.ValidationError):
ctx.get_param_as_bool('non-existing') ctx.get_param_as_bool('non-existing')
assert ctx.get_param_as_bool('non-existing', default=True) is True assert ctx.get_param_as_bool('non-existing', default=True) is True