API-v4 route added: 'api/v4/faq' fixes #11801 (#11905)

* feat(api-v4): new /faq route added

* refactor(server): change of function name in libs/content.js
This commit is contained in:
Denys Dorokhov
2020-04-14 23:14:53 +03:00
committed by GitHub
parent 551abf292c
commit 186b929e59
8 changed files with 181 additions and 10 deletions

View File

@@ -6,7 +6,7 @@ gulp.task('content:cache', done => {
// Requiring at runtime because these files access `common` // Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after // code which in production works only if transpiled so after
// gulp build:babel:common has run // gulp build:babel:common has run
const { CONTENT_CACHE_PATH, getLocalizedContent } = require('../website/server/libs/content'); // eslint-disable-line global-require const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
try { try {
@@ -23,7 +23,7 @@ gulp.task('content:cache', done => {
langCodes.forEach(langCode => { langCodes.forEach(langCode => {
fs.writeFileSync( fs.writeFileSync(
`${CONTENT_CACHE_PATH}${langCode}.json`, `${CONTENT_CACHE_PATH}${langCode}.json`,
getLocalizedContent(langCode), getLocalizedContentResponse(langCode),
'utf8', 'utf8',
); );
}); });

View File

@@ -8,9 +8,9 @@ describe('contentLib', () => {
}); });
}); });
describe('getLocalizedContent', () => { describe('getLocalizedContentResponse', () => {
it('clones, not modify, the original content data', () => { it('clones, not modify, the original content data', () => {
contentLib.getLocalizedContent(); contentLib.getLocalizedContentResponse();
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function'); expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
}); });
}); });

View File

@@ -0,0 +1,22 @@
import faq from '../../../../website/common/script/content/faq';
import common from '../../../../website/common';
import { localizeContentData } from '../../../../website/server/libs/content';
const { i18n } = common;
describe('localizeContentData', () => {
it('Should take a an object with localization identifiers and '
+ 'return an object with actual translations in English', () => {
const faqInEnglish = localizeContentData(faq, 'en');
expect(faqInEnglish).to.have.property('stillNeedHelp');
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'en'));
});
it('Should take an object with localization identifiers and '
+ 'return an object with actual translations in German', () => {
const faqInEnglish = localizeContentData(faq, 'de');
expect(faqInEnglish).to.have.property('stillNeedHelp');
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
});
});

View File

@@ -0,0 +1,65 @@
import {
requester,
translate,
} from '../../../helpers/api-integration/v4';
import i18n from '../../../../website/common/script/i18n';
describe('GET /faq', () => {
describe('language parameter', () => {
it('returns faq (and does not require authentication)', async () => {
const res = await requester().get('/faq');
expect(res).to.have.property('stillNeedHelp');
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
expect(res).to.have.property('questions');
expect(res.questions[0].question).to.equal(translate('faqQuestion0'));
});
it('returns faq not in English', async () => {
const res = await requester().get('/faq?language=de');
expect(res).to.have.nested.property('stillNeedHelp.ios');
expect(res.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
});
it('falls back to English if the desired language is not found', async () => {
const res = await requester().get('/faq?language=wrong');
expect(res).to.have.nested.property('stillNeedHelp.ios');
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
});
});
describe('platform parameter', () => {
it('returns faq with answers for ios platform only', async () => {
const res = await requester().get('/faq?platform=ios');
expect(res).to.have.property('stillNeedHelp');
expect(res.stillNeedHelp).to.eql({ ios: translate('iosFaqStillNeedHelp') });
expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({
question: translate('faqQuestion0'),
ios: translate('iosFaqAnswer0'),
});
});
it('returns an error when invalid platform parameter is specified', async () => {
const request = requester().get('/faq?platform=wrong');
await expect(request)
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: i18n.t('invalidReqParams'),
});
});
it('falls back to "web" description if there is no description for specified platform', async () => {
const res = await requester().get('/faq?platform=android');
expect(res).to.have.property('stillNeedHelp');
expect(res.stillNeedHelp).to.eql({ web: translate('webFaqStillNeedHelp') });
expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({
question: translate('faqQuestion0'),
android: translate('androidFaqAnswer0'),
});
});
});
});

View File

@@ -27,4 +27,6 @@ export default {
missingSubKey: 'Missing "req.query.sub"', missingSubKey: 'Missing "req.query.sub"',
ipAddressBlocked: 'This IP address has been blocked from accessing Habitica. This may be due to a breach of our Terms of Service or technical issue originating at this IP address. For details or to ask to be unblocked, please email admin@habitica.com or ask your parent or guardian to email them. Include your Habitica @ Username or User Id in the email if you have one.', ipAddressBlocked: 'This IP address has been blocked from accessing Habitica. This may be due to a breach of our Terms of Service or technical issue originating at this IP address. For details or to ask to be unblocked, please email admin@habitica.com or ask your parent or guardian to email them. Include your Habitica @ Username or User Id in the email if you have one.',
invalidPlatform: 'Invalid platform specified',
}; };

View File

@@ -1,6 +1,6 @@
import nconf from 'nconf'; import nconf from 'nconf';
import { langCodes } from '../../libs/i18n'; import { langCodes } from '../../libs/i18n';
import { CONTENT_CACHE_PATH, getLocalizedContent } from '../../libs/content'; import { CONTENT_CACHE_PATH, getLocalizedContentResponse } from '../../libs/content';
const IS_PROD = nconf.get('IS_PROD'); const IS_PROD = nconf.get('IS_PROD');
@@ -72,7 +72,7 @@ api.getContent = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });
const jsonResString = getLocalizedContent(language); const jsonResString = getLocalizedContentResponse(language);
res.status(200).send(jsonResString); res.status(200).send(jsonResString);
} }
}, },

View File

@@ -0,0 +1,77 @@
import _ from 'lodash';
import { query } from 'express-validator/check';
import { langCodes } from '../../libs/i18n';
import apiError from '../../libs/apiError';
import common from '../../../common';
import { localizeContentData } from '../../libs/content';
const { content } = common;
const { faq } = content;
const api = {};
function _deleteProperties (obj, keysToDelete, platform) {
// if there is no description for specified platform, use 'web' description by default
if (obj[platform] === undefined) {
delete obj.ios;
delete obj.android;
return;
}
keysToDelete.forEach(key => delete obj[key]);
}
function _deleteOtherPlatformsAnswers (faqObject, platform) {
const faqCopy = _.cloneDeep(faqObject);
const keysToDelete = _.without(['web', 'ios', 'android'], platform);
_deleteProperties(faqCopy.stillNeedHelp, keysToDelete, platform);
faqCopy.questions.forEach(question => {
_deleteProperties(question, keysToDelete, platform);
});
return faqCopy;
}
/**
* @api {get} /api/v4/faq Get faq in json format
* @apiDescription Does not require authentication.
* @apiName FaqGet
* @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 FAQ in a json format
*/
api.faq = {
method: 'GET',
url: '/faq',
middlewares: [
query('platform')
.optional()
.isIn(['web', 'android', 'ios']).withMessage(apiError('invalidPlatform')),
],
async handler (req, res) {
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const proposedLang = req.query.language && req.query.language.toString();
const language = langCodes.includes(proposedLang) ? proposedLang : 'en';
const { platform } = req.query;
const dataToLocalize = platform ? _deleteOtherPlatformsAnswers(faq, platform) : faq;
res.respond(200, localizeContentData(dataToLocalize, language));
},
};
export default api;

View File

@@ -14,8 +14,13 @@ function walkContent (obj, lang) {
}); });
} }
export function getLocalizedContent (langCode) { export function localizeContentData (data, langCode) {
const contentClone = _.cloneDeep(common.content); const dataClone = _.cloneDeep(data);
walkContent(contentClone, langCode); walkContent(dataClone, langCode);
return `{"success": true, "data": ${JSON.stringify(contentClone)}}`; return dataClone;
}
export function getLocalizedContentResponse (langCode) {
const localizedContent = localizeContentData(common.content, langCode);
return `{"success": true, "data": ${JSON.stringify(localizedContent)}}`;
} }