7 Commits

18 changed files with 703 additions and 50 deletions

View File

@@ -0,0 +1,17 @@
{
"database": {
"host": ":memory:"
},
"webserver": {
"port": 31400
},
"tmdb": {
"apiKey": "bogus-api-key"
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

View File

@@ -3,16 +3,19 @@
"main": "webserver/server.js",
"scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js",
"test": "cross-env SEASONED_CONFIG=conf/development.json TESTING=true NODE_PATH=. mocha --recursive test/system",
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --recursive test",
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. istanbul cover -x script/autogenerate-documentation.js --include-all-sources --dir test/.coverage node_modules/mocha/bin/_mocha --recursive test/**/* -- --report lcovonly && cat test/.coverage/lcov.info | coveralls && rm -rf test/.coverage",
"lint": "./node_modules/.bin/eslint src/"
},
"dependencies": {
"bcrypt-nodejs": "^0.0.3",
"blanket": "^1.2.3",
"body-parser": "~1.0.1",
"codecov": "^3.0.0",
"cross-env": "^3.1.3",
"express": "~4.11.0",
"jsonwebtoken": "^8.0.1",
"mocha-lcov-reporter": "^1.3.0",
"mongoose": "^3.6.13",
"moviedb": "^0.2.10",
"node-cache": "^4.1.1",
@@ -28,7 +31,7 @@
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"istanbul": "^0.4.5",
"mocha": "^3.1.0",
"mocha": "^5.0.4",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.1"
}

View File

@@ -28,7 +28,7 @@ class Config {
const field = new Field(this.fields[section][option]);
if (field.value === '') {
const envField = process.env[[section.toUpperCase(), option.toUpperCase()].join('_')];
const envField = process.env[['SEASONED', section.toUpperCase(), option.toUpperCase()].join('_')];
if (envField !== undefined && envField.length !== 0) { return envField; }
}

View File

@@ -1,8 +1,7 @@
const configuration = require('src/config/configuration').getInstance();
const SqliteDatabase = require('src/database/sqliteDatabase');
const host = process.env.TESTING ? ':memory:' : configuration.get('database', 'host');
const database = new SqliteDatabase(host);
const database = new SqliteDatabase(configuration.get('database', 'host'));
/**
* This module establishes a connection to the database
* specified in the confgiuration file. It tries to setup

View File

@@ -18,8 +18,8 @@ class RequestRepository {
this.queries = {
insertRequest: `INSERT INTO requests(id,title,year,poster_path,background_path,requested_by,ip,user_agent,type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
fetchRequestedItems: 'SELECT * FROM requests ORDER BY date DESC',
fetchRequestedItemsByStatus: 'SELECT * FROM requests WHERE status IS ? AND type LIKE ?',
fetchRequestedItems: 'SELECT * FROM requests ORDER BY date DESC LIMIT 25 OFFSET ?*25-25',
fetchRequestedItemsByStatus: 'SELECT * FROM requests WHERE status IS ? AND type LIKE ? DESC LIMIT 25 OFFSET ?*25-25',
updateRequestedById: 'UPDATE requests SET status = ? WHERE id is ? AND type is ?',
checkIfIdRequested: 'SELECT * FROM requests WHERE id IS ? AND type IS ?',
userRequests: 'SELECT * FROM requests WHERE requested_by IS ?'
@@ -68,19 +68,19 @@ class RequestRepository {
return Promise.resolve()
.then(() => tmdb.lookup(identifier, type))
.then((movie) => {
const username = user == undefined ? undefined : user.username;
const username = user === undefined ? undefined : user.username;
// Add request to database
return this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster_path, movie.background_path, username, ip, user_agent, movie.type]);
});
}
fetchRequested(status, type = '%') {
fetchRequested(status, page = '1', type = '%') {
return Promise.resolve()
.then(() => {
if (status === 'requested' || status === 'downloading' || status === 'downloaded')
return this.database.all(this.queries.fetchRequestedItemsByStatus, [status, type]);
return this.database.all(this.queries.fetchRequestedItemsByStatus, [status, type, page]);
else
return this.database.all(this.queries.fetchRequestedItems);
return this.database.all(this.queries.fetchRequestedItems, page);
})
}

View File

@@ -29,6 +29,7 @@ class TMDB {
/**
* Retrieve a specific movie by id from TMDB.
* @param {Number} identifier of the movie you want to retrieve
* @param {String} type filter results by type (default movie).
* @returns {Promise} succeeds if movie was found
*/
lookup(identifier, type = 'movie') {
@@ -36,7 +37,7 @@ class TMDB {
const cacheKey = `${this.cacheTags.info}:${type}:${identifier}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(this.tmdbMethod('info', type), query))
.catch(() => this.tmdb(TMDB_METHODS['info'][type], query))
.catch(() => { throw new Error('Could not find a movie with that id.'); })
.then(response => this.cache.set(cacheKey, response))
.then((response) => {
@@ -50,70 +51,66 @@ class TMDB {
}
/**
* Retrive list of of items from TMDB matching the query and/or type given.
* @param {queryText, page, type} the page number to specify in the request for discover,
* Retrive search results from TMDB.
* @param {String} text query you want to search for
* @param {Number} page representing pagination of results
* @param {String} type filter results by type (default multi)
* @returns {Promise} dict with query results, current page and total_pages
*/
search(text, page = 1, type = 'multi') {
const query = { query: text, page };
const query = { query: text, page: page };
const cacheKey = `${this.cacheTags.search}:${page}:${type}:${text}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(this.tmdbMethod('search', type), query))
.catch(() => this.tmdb(TMDB_METHODS['search'][type], query))
.catch(() => { throw new Error('Could not search for movies/shows at tmdb.'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapResults(response))
.catch((error) => { throw new Error(error); })
.then(([mappedResults, pagenumber, totalpages, total_results]) => ({
results: mappedResults, page: pagenumber, total_results, total_pages: totalpages,
}));
}
/**
* Fetches a given list from tmdb.
* @param {listName} List we want to fetch.
* @param {type} The to specify in the request for discover (default 'movie').
* @param {id} When finding similar a id can be added to query
* @param {page} Page number we want to fetch.
* @param {String} listName Name of list
* @param {String} type filter results by type (default movie)
* @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages
*/
listSearch(listName, type = 'movie', id, page = '1') {
const params = { id, page };
const cacheKey = `${this.cacheTags[listName]}:${type}:${id}:${page}`;
listSearch(listName, type = 'movie', page = '1') {
const query = { page: page }
console.log(query)
const cacheKey = `${this.cacheTags[listName]}:${type}:${page}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(this.tmdbMethod(listName, type), params))
.catch(() => this.tmdb(TMDB_METHODS[listName][type], query))
.catch(() => { throw new Error('Error fetching list from tmdb.')})
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapResults(response, type))
.catch((error) => { throw new Error(error); })
.then(([mappedResults, pagenumber, totalpages, total_results]) => ({
results: mappedResults, page: pagenumber, total_pages: totalpages, total_results,
}));
}
tmdbMethod(apiMethod, type) {
const method = TMDB_METHODS[apiMethod][type];
if (method !== undefined) return method;
throw new Error('Could not find tmdb api method.');
}
/**
* Maps our response from tmdb api to a movie/show object.
* @param {response} JSON response from tmdb.
* @param {type} The type declared in listSearch.
* @param {String} response from tmdb.
* @param {String} The type declared in listSearch.
* @returns {Promise} dict with tmdb results, mapped as movie/show objects.
*/
mapResults(response, type) {
console.log(response.page)
return Promise.resolve()
.then(() => {
const mappedResults = response.results.filter((element) => {
return (element.media_type === 'movie' || element.media_type === 'tv' || element.media_type === undefined);
}).map((element) => convertTmdbToSeasoned(element, type));
return [mappedResults, response.page, response.total_pages, response.total_results];
return {results: mappedResults, page: response.page, total_pages: response.total_pages, total_results: response.total_results}
})
.catch((error) => { throw new Error(error); });
}
/**
* Wraps moviedb library to support Promises.
* @param {String} method function name in the library
* @param {Object} argument argument to function being called
* @returns {Promise} succeeds if callback succeeds
*/
tmdb(method, argument) {
return new Promise((resolve, reject) => {
const callback = (error, reponse) => {

View File

@@ -10,9 +10,9 @@ const requestRepository = new RequestRepository();
*/
function fetchRequestedController(req, res) {
// const user = req.loggedInUser;
const { status } = req.query;
const { status, page } = req.query;
requestRepository.fetchRequested(status)
requestRepository.fetchRequested(status, page)
.then((requestedItems) => {
res.send({ success: true, results: requestedItems, total_results: requestedItems.length });
})

View File

@@ -13,8 +13,8 @@ const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
*/
function listSearchController(req, res) {
const listname = req.params.listname;
const { type, id, page } = req.query;
tmdb.listSearch(listname, type, id, page)
const { type, page } = req.query;
tmdb.listSearch(listname, type, page)
.then((results) => {
res.send(results);
}).catch((error) => {

View File

@@ -0,0 +1,13 @@
{
"background_path": "/yIZ1xendyqKvY3FGeeUYUd5X9Mm.jpg",
"id": 329865,
"popularity": 26.978601,
"poster_path": "/hLudzvGfpi6JlwUnsNhXwKKg4j.jpg",
"release_status": "Released",
"score": 7.3,
"summary": "Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.",
"tagline": "Why are they here?",
"title": "Arrival",
"type": "movie",
"year": 2016
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ const popularMoviesSuccess = require('test/fixtures/popular-movies-success-respo
describe('As a user I want to get popular movies', () => {
before(() => resetDatabase());
before(() => createCacheEntry('p:movie::1', popularMoviesSuccess));
before(() => createCacheEntry('p:movie:1', popularMoviesSuccess));
it('should return 200 with the information', () =>
request(app)

View File

@@ -7,7 +7,7 @@ const popularShowsSuccess = require('test/fixtures/popular-show-success-response
describe('As a user I want to get popular shows', () => {
before(() => resetDatabase());
before(() => createCacheEntry('p:show::1', popularShowsSuccess));
before(() => createCacheEntry('p:show:1', popularShowsSuccess));
it('should return 200 with the information', () =>
request(app)

View File

@@ -1,14 +1,17 @@
const resetDatabase = require('test/helpers/resetDatabase');
const createCacheEntry = require('test/helpers/createCacheEntry');
const app = require('src/webserver/app');
const request = require('supertest-as-promised');
const createUser = require('test/helpers/createUser');
const createToken = require('test/helpers/createToken');
const infoMovieSuccess = require('test/fixtures/arrival-info-success-response.json');
describe('As a user I want to request a movie', () => {
before(() => {
return resetDatabase()
.then(() => createUser('test_user', 'test@gmail.com', 'password'));
})
before(() => createCacheEntry('i:movie:329865', infoMovieSuccess));
it('should return 200 when item is requested', () =>
request(app)

View File

@@ -6,7 +6,7 @@ const interstellarQuerySuccess = require('test/fixtures/interstellar-query-succe
describe('As an anonymous user I want to search for a movie', () => {
before(() => resetDatabase());
before(() => createCacheEntry('s:1:movie:interstellar', interstellarQuerySuccess));
before(() => createCacheEntry('se:1:multi:interstellar', interstellarQuerySuccess));
it('should return 200 with the search results even if user is not logged in', () =>
request(app)

View File

@@ -0,0 +1,63 @@
const assert = require('assert');
const Config = require('src/config/configuration.js');
describe('Config', () => {
before(() => {
this.backedUpEnvironmentVariables = Object.assign({}, process.env);
this.backedUpConfigFields = Object.assign({}, Config.getInstance().fields);
});
after(() => {
process.env = this.backedUpEnvironmentVariables;
Config.getInstance().fields = this.backedUpConfigFields;
});
it('should retrieve section and option from config file', () => {
Config.getInstance().fields = { 'webserver': { 'port': 1337 } };
assert.equal(Config.getInstance().get('webserver', 'port'), 1337);
});
it('should resolve to environment variables if option is filtered with env', () => {
Config.getInstance().fields = { 'webserver': { 'port': 'env|SEASONED_WEBSERVER_PORT' } };
process.env.SEASONED_WEBSERVER_PORT = '1338';
assert.equal(Config.getInstance().get('webserver', 'port'), 1338);
});
it('raises an exception if the environment variable does not exist', () => {
Config.getInstance().fields = { 'webserver': { 'port': 'env|DOES_NOT_EXIST' } };
process.env.SEASONED_WEBSERVER_PORT = '1338';
assert.throws(() => Config.getInstance().get('webserver', 'port'), /empty/);
});
it('raises an exception if the environment variable is empty', () => {
Config.getInstance().fields = { 'webserver': { 'port': 'env|SEASONED_WEBSERVER_PORT' } };
process.env.SEASONED_WEBSERVER_PORT = '';
assert.throws(() => Config.getInstance().get('webserver', 'port'), /empty/);
});
it('raises an exception if the section does not exist in the file', () => {
Config.getInstance().fields = { 'webserver': { 'port': '1338' } };
assert.throws(() => Config.getInstance().get('woops', 'port'), /does not exist/);
});
it('raises an exception if the option does not exist in the file', () => {
Config.getInstance().fields = { 'webserver': { 'port': '1338' } };
assert.throws(() => Config.getInstance().get('webserver', 'woops'), /does not exist/);
});
it('returns an array if field is an array', () => {
Config.getInstance().fields = { 'bouncer': { 'whitelist': [1, 2, 3] } };
assert.deepEqual(Config.getInstance().get('bouncer', 'whitelist'), [1, 2, 3]);
});
it('decodes field as base64 if base64| is before the variable', () => {
Config.getInstance().fields = { 'webserver': { 'port': 'base64|MTMzOA==' } };
assert.equal(Config.getInstance().get('webserver', 'port'), 1338);
});
it('decodes environment variable as base64 if BASE64= is before the variable', () => {
Config.getInstance().fields = { 'webserver': { 'port': 'env|base64|SEASONED_WEBSERVER_PORT' } };
process.env.SEASONED_WEBSERVER_PORT = 'MTMzOA==';
assert.equal(Config.getInstance().get('webserver', 'port'), 1338);
});
});

View File

@@ -0,0 +1,72 @@
const assert = require('assert');
const Field = require('src/config/field.js');
describe('Field', () => {
it('should return an array if it is an array', () => {
const field = new Field([1, 2, 3]);
assert.deepEqual(field.value, [1, 2, 3]);
});
it('should return the plain value if it is an ordinary field', () => {
const field = new Field('plain value');
assert.equal(field.value, 'plain value');
});
it('should return false if boolean false is field', () => {
const field = new Field(false);
assert.equal(field.value, false);
});
it('should not include any invalid filters', () => {
const field = new Field('invalid-filter|plain value');
assert.equal(field.value, 'plain value');
});
it('should return the decoded value if it is filtered through base64', () => {
const field = new Field('base64|ZW5jb2RlZCB2YWx1ZQ==');
assert.equal(field.value, 'encoded value');
});
it('should not decode the value if it missing the filter', () => {
const field = new Field('ZW5jb2RlZCB2YWx1ZQ==');
assert.equal(field.value, 'ZW5jb2RlZCB2YWx1ZQ==');
});
it('should retrieve the environment variable if env filter is used', () => {
const environmentVariables = { REDIS_URL: 'redis://127.0.0.1:1234' };
const field = new Field('env|REDIS_URL', environmentVariables);
assert.equal(field.value, 'redis://127.0.0.1:1234');
});
it('should return undefined if the environment variable does not exist', () => {
const environmentVariables = { HTTP_PORT: 8080 };
const field = new Field('env|REDIS_URL', environmentVariables);
assert.equal(field.value, undefined);
});
it('should return undefined if the environment variable is an empty string', () => {
const environmentVariables = { REDIS_URL: '' };
const field = new Field('env|REDIS_URL', environmentVariables);
assert.deepEqual(field.value, undefined);
});
describe('Multiple filters', () => {
it('should decode the environment variable if base64 and env filter are used', () => {
const environmentVariables = { REDIS_URL: 'cmVkaXM6Ly9kYWdibGFkZXQubm8vMTIzNA==' };
const field = new Field('env|base64|REDIS_URL', environmentVariables);
assert.equal(field.value, 'redis://dagbladet.no/1234');
});
it('should disregard the order of filters when env and base64 are used', () => {
const environmentVariables = { REDIS_URL: 'cmVkaXM6Ly9kYWdibGFkZXQubm8vMTIzNA==' };
const field = new Field('base64|env|REDIS_URL', environmentVariables);
assert.equal(field.value, 'redis://dagbladet.no/1234');
});
it('should return undefined if both filters are used and env var does not exist', () => {
const environmentVariables = { REDIS_URL: 'cmVkaXM6Ly9kYWdibGFkZXQubm8vMTIzNA==' };
const field = new Field('base64|env|REDIS_LOL', environmentVariables);
assert.equal(field.value, undefined);
});
});
});

View File

@@ -0,0 +1,34 @@
const assert = require('assert');
const Filters = require('src/config/filters.js');
describe('Filters', () => {
it('should extract base64 as filter if it is at start of string followed by pipe', () => {
const filters = new Filters('base64|');
assert.deepEqual(filters.filters, ['base64']);
});
it('should extract base64 and env as filters if both are separated by pipe', () => {
const filters = new Filters('base64|env|');
assert.deepEqual(filters.filters, ['base64', 'env']);
});
it('should not extract any filters if none are present', () => {
const filters = new Filters('base64');
assert.deepEqual(filters.filters, []);
});
it('should strip env filter from the value', () => {
const filters = new Filters('env|HELLO');
assert.deepEqual(filters.removeFiltersFromValue(), 'HELLO');
});
it('should strip env and base64 filter from the value', () => {
const filters = new Filters('env|base64|HELLO');
assert.deepEqual(filters.removeFiltersFromValue(), 'HELLO');
});
it('should strip no filters from the value if there are no filters', () => {
const filters = new Filters('HELLO');
assert.deepEqual(filters.removeFiltersFromValue(), 'HELLO');
});
});