mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 21:57:22 +01:00
Add tests for content filtering
This commit is contained in:
committed by
Sabe Jones
parent
39252c7828
commit
ec0275e6f6
@@ -1,5 +1,9 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import * as contentLib from '../../../../website/server/libs/content';
|
import * as contentLib from '../../../../website/server/libs/content';
|
||||||
import content from '../../../../website/common/script/content';
|
import content from '../../../../website/common/script/content';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
|
||||||
describe('contentLib', () => {
|
describe('contentLib', () => {
|
||||||
describe('CONTENT_CACHE_PATH', () => {
|
describe('CONTENT_CACHE_PATH', () => {
|
||||||
@@ -13,5 +17,88 @@ describe('contentLib', () => {
|
|||||||
contentLib.getLocalizedContentResponse();
|
contentLib.getLocalizedContentResponse();
|
||||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes keys from the content data', () => {
|
||||||
|
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
|
||||||
|
expect(response.backgroundsFlat).to.not.exist;
|
||||||
|
expect(response.backgrounds).to.exist;
|
||||||
|
expect(response.dropHatchingPotions).to.not.exist;
|
||||||
|
expect(response.hatchingPotions).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes nested keys from the content data', () => {
|
||||||
|
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
|
||||||
|
expect(response.gear.tree).to.not.exist;
|
||||||
|
expect(response.gear.flat).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates a hash for a filter', () => {
|
||||||
|
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
|
||||||
|
expect(hash).to.equal('-1791877526');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves content', () => {
|
||||||
|
const resSpy = generateRes();
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', false);
|
||||||
|
expect(resSpy.send).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves filtered content', () => {
|
||||||
|
const resSpy = generateRes();
|
||||||
|
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
|
||||||
|
expect(resSpy.send).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('caches content', async () => {
|
||||||
|
let resSpy;
|
||||||
|
beforeEach(() => {
|
||||||
|
resSpy = generateRes();
|
||||||
|
fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
|
||||||
|
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not cache requests in development mode', async () => {
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', false);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches unfiltered requests', async () => {
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', true);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves cached requests', async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${contentLib.CONTENT_CACHE_PATH}en.json`,
|
||||||
|
'{"success": true, "data": {"all": {}}}',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', true);
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches filtered requests', async () => {
|
||||||
|
const filter = 'backgroundsFlat,gear.flat';
|
||||||
|
const hash = contentLib.hashForFilter(filter);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
|
||||||
|
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves filtered cached requests', async () => {
|
||||||
|
const filter = 'backgroundsFlat,gear.flat';
|
||||||
|
const hash = contentLib.hashForFilter(filter);
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
|
||||||
|
'{"success": true, "data": {}}',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,4 +22,38 @@ describe('GET /content', () => {
|
|||||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not filter content for regular requests', async () => {
|
||||||
|
const res = await requester().get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content automatically for iOS requests', async () => {
|
||||||
|
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
|
||||||
|
expect(res).to.have.nested.property('appearances.background.beach');
|
||||||
|
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.not.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content automatically for Android requests', async () => {
|
||||||
|
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
|
||||||
|
expect(res).to.not.have.nested.property('appearances.background.beach');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.not.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content if the request specifies a filter', async () => {
|
||||||
|
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.have.nested.property('gear.tree');
|
||||||
|
expect(res).to.not.have.nested.property('gear.flat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content if the request contains invalid filters', async () => {
|
||||||
|
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function generateRes (options = {}) {
|
|||||||
redirect: sandbox.stub(),
|
redirect: sandbox.stub(),
|
||||||
render: sandbox.stub(),
|
render: sandbox.stub(),
|
||||||
send: sandbox.stub(),
|
send: sandbox.stub(),
|
||||||
|
sendFile: sandbox.stub(),
|
||||||
sendStatus: sandbox.stub().returnsThis(),
|
sendStatus: sandbox.stub().returnsThis(),
|
||||||
set: sandbox.stub(),
|
set: sandbox.stub(),
|
||||||
status: sandbox.stub().returnsThis(),
|
status: sandbox.stub().returnsThis(),
|
||||||
|
|||||||
@@ -1,34 +1,16 @@
|
|||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import fs from 'fs';
|
|
||||||
import { langCodes } from '../../libs/i18n';
|
import { langCodes } from '../../libs/i18n';
|
||||||
import { CONTENT_CACHE_PATH, getLocalizedContentResponse } from '../../libs/content';
|
import { serveContent } from '../../libs/content';
|
||||||
|
|
||||||
const IS_PROD = nconf.get('IS_PROD');
|
const IS_PROD = nconf.get('IS_PROD');
|
||||||
|
|
||||||
const api = {};
|
const api = {};
|
||||||
|
|
||||||
const CACHED_HASHES = [
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
const MOBILE_FILTER = `achievements,questSeriesAchievements,animalColorAchievements,animalSetAchievements,stableAchievements,
|
const MOBILE_FILTER = `achievements,questSeriesAchievements,animalColorAchievements,animalSetAchievements,stableAchievements,
|
||||||
mystery,bundles,loginIncentives,pets,premiumPets,specialPets,questPets,wackyPets,mounts,premiumMounts,specialMounts,questMounts,
|
mystery,bundles,loginIncentives,pets,premiumPets,specialPets,questPets,wackyPets,mounts,premiumMounts,specialMounts,questMounts,
|
||||||
events,dropEggs,questEggs,dropHatchingPotions,premiumHatchingPotions,wackyHatchingPotions,backgroundsFlat,questsByLevel,gear.tree,
|
events,dropEggs,questEggs,dropHatchingPotions,premiumHatchingPotions,wackyHatchingPotions,backgroundsFlat,questsByLevel,gear.tree,
|
||||||
tasksByCategory,userDefaults,timeTravelStable,gearTypes,cardTypes`;
|
tasksByCategory,userDefaults,timeTravelStable,gearTypes,cardTypes`;
|
||||||
|
|
||||||
function hashForFilter (filter) {
|
|
||||||
let hash = 0;
|
|
||||||
let i; let
|
|
||||||
chr;
|
|
||||||
if (filter.length === 0) return '';
|
|
||||||
for (i = 0; i < filter.length; i++) { // eslint-disable-line
|
|
||||||
chr = filter.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + chr; // eslint-disable-line
|
|
||||||
hash |= 0; // eslint-disable-line
|
|
||||||
}
|
|
||||||
return String(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v3/content Get all available content objects
|
* @api {get} /api/v3/content Get all available content objects
|
||||||
* @apiDescription Does not require authentication.
|
* @apiDescription Does not require authentication.
|
||||||
@@ -92,52 +74,13 @@ api.getContent = {
|
|||||||
// apply defaults for mobile clients
|
// apply defaults for mobile clients
|
||||||
if (filter === '') {
|
if (filter === '') {
|
||||||
if (req.headers['x-client'] === 'habitica-android') {
|
if (req.headers['x-client'] === 'habitica-android') {
|
||||||
filter = `${MOBILE_FILTER},appearance.background`;
|
filter = `${MOBILE_FILTER},appearances.background`;
|
||||||
} else if (req.headers['x-client'] === 'habitica-ios') {
|
} else if (req.headers['x-client'] === 'habitica-ios') {
|
||||||
filter = `${MOBILE_FILTER},backgrounds`;
|
filter = `${MOBILE_FILTER},backgrounds`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build usable filter object
|
serveContent(res, language, filter, IS_PROD);
|
||||||
const filterObj = {};
|
|
||||||
filter.split(',').forEach(item => {
|
|
||||||
if (item.includes('.')) {
|
|
||||||
const [key, subkey] = item.split('.');
|
|
||||||
if (!filterObj[key]) {
|
|
||||||
filterObj[key] = {};
|
|
||||||
}
|
|
||||||
filterObj[key][subkey.trim()] = true;
|
|
||||||
} else {
|
|
||||||
filterObj[item.trim()] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IS_PROD) {
|
|
||||||
const filterHash = language + hashForFilter(filter);
|
|
||||||
if (CACHED_HASHES.includes(filterHash)) {
|
|
||||||
// Content is already cached, so just send it.
|
|
||||||
res.sendFile(`${CONTENT_CACHE_PATH}${filterHash}.json`);
|
|
||||||
} else {
|
|
||||||
// Content is not cached, so cache it and send it.
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
});
|
|
||||||
const jsonResString = getLocalizedContentResponse(language, filterObj);
|
|
||||||
fs.writeFileSync(
|
|
||||||
`${CONTENT_CACHE_PATH}${filterHash}.json`,
|
|
||||||
jsonResString,
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
CACHED_HASHES.push(filterHash);
|
|
||||||
res.status(200).send(jsonResString);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
});
|
|
||||||
const jsonResString = getLocalizedContentResponse(language, filterObj);
|
|
||||||
res.status(200).send(jsonResString);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import common from '../../common';
|
import common from '../../common';
|
||||||
import packageInfo from '../../../package.json';
|
import packageInfo from '../../../package.json';
|
||||||
|
|
||||||
export const CONTENT_CACHE_PATH = path.join(__dirname, '/../../../content_cache/');
|
export const CONTENT_CACHE_PATH = path.join(__dirname, '/../../../content_cache/');
|
||||||
|
|
||||||
|
const CACHED_HASHES = [
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
function walkContent (obj, lang, removedKeys = {}) {
|
function walkContent (obj, lang, removedKeys = {}) {
|
||||||
_.each(obj, (item, key, source) => {
|
_.each(obj, (item, key, source) => {
|
||||||
if (key in removedKeys && removedKeys[key] === true) {
|
if (key in removedKeys && removedKeys[key] === true) {
|
||||||
@@ -33,3 +38,59 @@ export function getLocalizedContentResponse (langCode, removedKeys = {}) {
|
|||||||
const localizedContent = localizeContentData(common.content, langCode, removedKeys);
|
const localizedContent = localizeContentData(common.content, langCode, removedKeys);
|
||||||
return `{"success": true, "data": ${JSON.stringify(localizedContent)}, "appVersion": "${packageInfo.version}"}`;
|
return `{"success": true, "data": ${JSON.stringify(localizedContent)}, "appVersion": "${packageInfo.version}"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hashForFilter (filter) {
|
||||||
|
let hash = 0;
|
||||||
|
let i; let
|
||||||
|
chr;
|
||||||
|
if (filter.length === 0) return '';
|
||||||
|
for (i = 0; i < filter.length; i++) { // eslint-disable-line
|
||||||
|
chr = filter.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr; // eslint-disable-line
|
||||||
|
hash |= 0; // eslint-disable-line
|
||||||
|
}
|
||||||
|
return String(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveContent (res, language, filter, isProd) {
|
||||||
|
// Build usable filter object
|
||||||
|
const filterObj = {};
|
||||||
|
filter.split(',').forEach(item => {
|
||||||
|
if (item.includes('.')) {
|
||||||
|
const [key, subkey] = item.split('.');
|
||||||
|
if (!filterObj[key]) {
|
||||||
|
filterObj[key] = {};
|
||||||
|
}
|
||||||
|
filterObj[key][subkey.trim()] = true;
|
||||||
|
} else {
|
||||||
|
filterObj[item.trim()] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isProd) {
|
||||||
|
const filterHash = language + hashForFilter(filter);
|
||||||
|
if (CACHED_HASHES.includes(filterHash)) {
|
||||||
|
// Content is already cached, so just send it.
|
||||||
|
res.sendFile(`${CONTENT_CACHE_PATH}${filterHash}.json`);
|
||||||
|
} else {
|
||||||
|
// Content is not cached, so cache it and send it.
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
});
|
||||||
|
const jsonResString = getLocalizedContentResponse(language, filterObj);
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${CONTENT_CACHE_PATH}${filterHash}.json`,
|
||||||
|
jsonResString,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
CACHED_HASHES.push(filterHash);
|
||||||
|
res.status(200).send(jsonResString);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
});
|
||||||
|
const jsonResString = getLocalizedContentResponse(language, filterObj);
|
||||||
|
res.status(200).send(jsonResString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user