Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions test/api/v4/content/GET-content.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
requester,
translate as t,
generateUser,
} from '../../../helpers/api-integration/v4';
import i18n from '../../../../website/common/script/i18n';

describe('GET /content', () => {
it('returns content (and does not require authentication)', async () => {
const res = await requester().get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
});

it('returns content not in English', async () => {
const res = await requester().get('/content?language=de');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
});

it('falls back to English if the desired language is not found', async () => {
const res = await requester().get('/content?language=wrong');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
});

it('filters out starter task content by default', async () => {
const res = await requester().get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.have.nested.property('gear.tree');
expect(res).to.not.have.nested.property('userDefaults');
expect(res).to.not.have.nested.property('tasksByCategory');
expect(res).to.not.have.nested.property('userDefaultsMobile');
});

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');
});

describe('authenticated user', () => {
let user;
it('returns content in user\'s preferred language when no language parameter is provided', async () => {
user = await generateUser({ 'preferences.language': 'de' });
const res = await user.get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
});

it('respects language parameter over user\'s preferred language', async () => {
user = await generateUser({ 'preferences.language': 'de' });
const res = await user.get('/content?language=fr');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr'));
});

it('falls back to English if user\'s preferred language is invalid', async () => {
user = await generateUser({ 'preferences.language': 'invalid_lang' });
const res = await user.get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
});
});
});
99 changes: 99 additions & 0 deletions website/server/controllers/api-v4/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import nconf from 'nconf';
import { langCodes } from '../../libs/i18n';
import {
serveContent,
ANDROID_FILTER,
IOS_FILTER,
V4_FILTER,
} from '../../libs/content';
import { authWithHeaders } from '../../middlewares/auth';

const IS_PROD = nconf.get('IS_PROD');

const api = {};

/**
* @api {get} /api/v4/content Get all available content objects
* @apiDescription Does not require authentication.
* @apiName ContentGet
* @apiGroup Content
*
* @apiParam (Query) {String="bg","cs","da","de",
* "en","en@pirate","en_GB",
* "es","es_419","fr","he","hu",
* "id","it","ja","nl","pl","pt","pt_BR",
* "ro","ru","sk","sr","sv",
* "uk","zh","zh_TW"} [language=en] Language code used for the items'
* strings. If the authenticated user makes
* the request, the content will return with
* the user's configured language.
*
* @apiSuccess {Object} data Various data about the content of Habitica. The content route
* contains many keys, but the data listed below are the recommended data to use.
* @apiSuccess {Object} data.mystery The mystery sets awarded to paying subscribers.
* @apiSuccess {Object} data.gear The gear that can be equipped.
* @apiSuccess {Object} data.gear.tree Detailed information about the gear, organized by type.
* @apiSuccess {Object} data.gear.flat The full key of each equipment.
* @apiSuccess {Object} data.spells The skills organized by class. Includes cards and visual buffs.
* @apiSuccess {Object} data.potion Data about the health potion.
* @apiSuccess {Object} data.armoire Data about the armoire.
* @apiSuccess {Array} data.classes The available classes.
* @apiSuccess {Object} data.eggs All available eggs.
* @apiSuccess {Object} data.timeTravelStable The animals available
* in the Time Traveler's stable, separated
* into pets and mounts.
* @apiSuccess {Object} data.hatchingPotions All the hatching potions.
* @apiSuccess {Object} data.petInfo All the pets with extra info.
* @apiSuccess {Object} data.mountInfo All the mounts with extra info.
* @apiSuccess {Object} data.food All the food.
* @apiSuccess {Array} data.userCanOwnQuestCategories The types of quests that a user can own.
* @apiSuccess {Object} data.quests Data about the quests.
* @apiSuccess {Object} data.appearances Data about the appearance properties.
* @apiSuccess {Object} data.appearances.hair Data about available hair options.
* @apiSuccess {Object} data.appearances.shirt Data about available shirt options.
* @apiSuccess {Object} data.appearances.size Data about available body size options.
* @apiSuccess {Object} data.appearances.skin Data about available skin options.
* @apiSuccess {Object} data.appearances.chair Data about available chair options.
* @apiSuccess {Object} data.appearances.background Data about available background options.
* @apiSuccess {Object} data.backgrounds Data about the background sets.
* @apiSuccess {Object} data.subscriptionBlocks Data about the various subscriptions blocks.
*
*/
api.getContent = {
method: 'GET',
url: '/content',
noLanguage: true,
middlewares: [authWithHeaders({ optional: true })],
async handler (req, res) {
let language = 'en';
const proposedLang = req.query.language;

if (proposedLang && langCodes.includes(proposedLang)) {
language = proposedLang;
} else if (res.locals.user
&& res.locals.user.preferences
&& res.locals.user.preferences.language
) {
const userLang = res.locals.user.preferences.language;
if (langCodes.includes(userLang)) {
language = userLang;
}
}

let filter = req.query.filter || '';
// apply defaults for mobile clients
if (filter === '') {
if (req.headers['x-client'] === 'habitica-android') {
filter = [V4_FILTER, ANDROID_FILTER].join(',');
} else if (req.headers['x-client'] === 'habitica-ios') {
filter = [V4_FILTER, IOS_FILTER].join(',');
} else {
filter = V4_FILTER;
}
}

serveContent(res, language, filter, IS_PROD);
},
};

export default api;
1 change: 1 addition & 0 deletions website/server/libs/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

export const ANDROID_FILTER = [...MOBILE_FILTER, 'appearances.background'].join(',');
export const IOS_FILTER = [...MOBILE_FILTER, 'backgrounds'].join(',');
export const V4_FILTER = 'userDefaults,tasksByCategory,userDefaultsMobile';

function getDay (date) {
if (date === undefined) {
Expand Down Expand Up @@ -129,7 +130,7 @@
// Content is already cached, so just send it.
res.sendFile(`${CONTENT_CACHE_PATH}${cachedName}.json`);
} else {
console.log(`Caching content for language ${language} with filter ${filter}`);

Check warning on line 133 in website/server/libs/content.js

View workflow job for this annotation

GitHub Actions / lint (21.x)

Unexpected console statement
// Content is not cached, so cache it and send it.
res.set({
'Content-Type': 'application/json',
Expand Down
1 change: 1 addition & 0 deletions website/server/middlewares/appRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ app.use('/api/v3', v3RateLimiter, v3Router);
const v4RouterOverrides = [
// 'GET-/status', Example to override the GET /status api call
'POST-/user/auth/local/register',
'GET-/content',
'GET-/news',
'GET-/user',
'PUT-/user',
Expand Down
Loading