Mhurd has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/330820 )
Change subject: Enwiki "On This Day" endpoints. ...................................................................... Enwiki "On This Day" endpoints. T143408 Endpoints: - 'births' - 'deaths' - 'events' - 'selected' - 'holidays' - 'all' Includes: - 'title hydration' - eTags Uses Parsoid html. Background: On enwiki there are pages for each day of the year such as: https://en.m.wikipedia.org/wiki/May_20 These pages contain lists of events, births, deaths and holidays. Editors also curate a smaller list of 'selected' items: https://en.m.wikipedia.org/wiki/Wikipedia%3ASelected_anniversaries%2FMay_20 Change-Id: I21565e73269b4ed5e717f692aebe2084401673a6 --- M lib/dateUtil.js M lib/mobile-util.js A routes/on-this-day.js A test/features/onthisday/onthisday.js 4 files changed, 864 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/mobileapps refs/changes/20/330820/1 diff --git a/lib/dateUtil.js b/lib/dateUtil.js index 6c19363..1795509 100644 --- a/lib/dateUtil.js +++ b/lib/dateUtil.js @@ -9,6 +9,8 @@ const dateUtil = {}; +dateUtil.monthNames = monthNames; + dateUtil.ONE_DAY = 86400000; /** diff --git a/lib/mobile-util.js b/lib/mobile-util.js index 11daa2c..eea6c6d 100644 --- a/lib/mobile-util.js +++ b/lib/mobile-util.js @@ -14,6 +14,8 @@ announcements: { name: 'announcements', version: '0.1.0' }, + onthisday: { name: 'onthisday', version: '0.1.0' }, + unpublished: { name: 'unpublished', version: '0.0.0' } }; diff --git a/routes/on-this-day.js b/routes/on-this-day.js new file mode 100644 index 0000000..ced5ad0 --- /dev/null +++ b/routes/on-this-day.js @@ -0,0 +1,473 @@ + +'use strict'; + +const domino = require('domino'); +const sUtil = require('../lib/util'); +const router = sUtil.router(); +const preq = require('preq'); +const dateUtil = require('../lib/dateUtil'); +const mUtil = require('../lib/mobile-util'); +const parsoid = require('../lib/parsoid-access'); + +let app; + +/* eslint-disable arrow-parens, max-len */ + + +/** + * Gets English day page titles, which are formatted as follows: 'May_20' + * @param {String} monthNumberString String for month number ranging from '1' to '12' + * @param {String} dayNumberString String number for day of month + * @return {String} Day page title. Example, inputs ('5', '20') returns 'May_20' + */ +function titleForDayPageFromMonthDayNumberStrings(monthNumberString, dayNumberString) { + return `${dateUtil.monthNames[parseInt(monthNumberString) - 1]}_${parseInt(dayNumberString)}`; +} + + +/** + * Gets day page Parsoid URI for day pages such as https://en.m.wikipedia.org/wiki/May_20 + * @param {Request} req Request containing month (req.params.mm) and day (req.params.dd) number string params + * @return {String} Day page URI for month and day number. Example, input mm '5' dd '20 returns https://en.wikipedia.org/api/rest_v1/page/html/May_20 + */ +function uriForDayPageRequest(req) { + const date = titleForDayPageFromMonthDayNumberStrings(req.params.mm, req.params.dd); + return `https://${req.params.domain}/api/rest_v1/page/html/${date}`; +} + + +/** + * Gets selected page Parsoid URI for selected pages such as https://en.m.wikipedia.org/wiki/Wikipedia%3ASelected_anniversaries%2FMay_20 + * ( These pages are also where 'Today' page https://en.m.wikipedia.org/wiki/Wikipedia:On_this_day/Today content comes from ) + * @param {Request} req Request containing month (req.params.mm) and day (req.params.dd) number string params + * @return {String} Selected page URI for month and day number. Example, input mm '5' dd '20 returns https://en.wikipedia.org/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2FMay_20 + */ +function uriForSelectedPageRequest(req) { + const date = titleForDayPageFromMonthDayNumberStrings(req.params.mm, req.params.dd); + return `https://${req.params.domain}/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2F${date}`; +} + + +/** + * WMFPage models a link to a page + * @param {String} title Page title, i.e. 'Goat' + * @param {Boolean} isTopic Events can have multiple links to pages, if this particular link is bolded, such as those seen on https://en.m.wikipedia.org/wiki/Wikipedia:On_this_day/Today, isTopic will be true. + */ +class WMFPage { + constructor(title, isTopic) { + this.title = title; + this.isTopic = isTopic; + } +} + + +/** + * WMFEvent models an historical event + * @param {String} text Event description + * @param {Array} pages Array of WMFPage's for the event + * @param {String} year Year. As string because some events occured 'BC', sometimes also denoted 'BCE' - i.e. '32 BC' or '200 BCE' + */ +class WMFEvent { + constructor(text, pages, year) { + this.text = text; + this.pages = pages; + this.year = year; + } +} + + +/** + * WMFHoliday models an annually occuring holiday + * @param {String} text Event description + * @param {Array} pages Array of WMFPage's for the event + */ +class WMFHoliday { + constructor(text, pages) { + this.text = text; + this.pages = pages; + } +} + + +/** + * Get a valid 'dbTitle' from a Parsoid anchor element + * ( No need to use 'getDbTitle' promise since we already have the 'dbTitle' in Parsoid anchor 'href' ) + * @param {AnchorElement} anchorElement Anchor to examine + * @return {String} A valid 'dbTitle' - i.e. title with underscores instead of spaces (and other changes) + */ +function dbTitleFromParsoidAnchorElement(anchorElement) { + return anchorElement.href.startsWith('/') ? anchorElement.href.substring(1) : anchorElement.href; +} + + +/** + * Converts document anchor element to WMFPage model + * @param {AnchorElement} anchorElement Anchor to convert + */ +function WMFPageFromAnchorElement(anchorElement) { + const title = dbTitleFromParsoidAnchorElement(anchorElement); + const isTopic = anchorElement.parentElement.tagName === 'B'; + return new WMFPage(title, isTopic); +} + + +/** + * Converts document list element to WMFEvent model + * @param {ListElement} listElement List element to convert + */ +function WMFEventFromListElement(listElement) { + const text = listElement.textContent.trim(); + const dashSpace = '– '; + const indexOfDashSpace = text.indexOf(dashSpace); + const year = text.substring(0, indexOfDashSpace).trim(); + const textAfterYear = text.substring(indexOfDashSpace + dashSpace.length); + + function isAnchorNotForYearNumber(anchor) { + return parseInt(anchor.title) !== parseInt(year); + } + + const pages = Array.from(listElement.querySelectorAll('a')) + .filter(isAnchorNotForYearNumber) + .map(WMFPageFromAnchorElement); + + return new WMFEvent(textAfterYear, pages, year); +} + + +/** + * Converts document list element to WMFHoliday model + * @param {ListElement} listElement List element to convert + */ +function WMFHolidayFromListElement(listElement) { + const text = listElement.textContent.trim(); + const pages = Array.from(listElement.querySelectorAll('a')).map(WMFPageFromAnchorElement); + return new WMFHoliday(text, pages); +} + + +/** + * Determines whether a list element is formatted as an event which occured during a year + * @param {ListElement} listElement List element to examine + * @return {Boolean} True if the list element is about an event which occured during a year + */ +function isYearListElement(listElement) { + // 'Year' list elements start with a year number followed by + // a dash and a space - ie: ' 1974 - ' or ' 23 BC - '. + return (listElement.textContent.match(/^\s*\d+\s*[BC|bc|BCE|bce]*\s*–\s/) !== null); +} + + +/** + * WMFEvent comparator which properly handles 'BC'/'BCE' years + * @param {WMFEvent} eventA First event + * @param {WMFEvent} eventB Second event + * @return {Integer} -1, 1 or 0 + */ +function bcAwareEventComparator(eventA, eventB) { + let yearA = eventA.year; + let yearB = eventB.year; + + function isBC(year) { + const upperCaseYear = year.toUpperCase(); + return (upperCaseYear.endsWith('BC') || upperCaseYear.endsWith('BCE')); + } + // For sorting treat BC years as negative numbers. + yearA = isBC(yearA) ? -parseInt(yearA) : parseInt(yearA); + yearB = isBC(yearB) ? -parseInt(yearB) : parseInt(yearB); + + if (yearB < yearA) { + return -1; + } else if (yearB > yearA) { + return 1; + } else { + return 0; + } +} + + +/** + * Gets chronologically sorted array of WMFEvent models from an array of list elements. + * @param {Array} listElements Array of document list elements + * @return {Array} Sorted array of WMFEvent models, one for each year list element found in 'listElements' argument + */ +function eventsForYearListElements(listElements) { + return listElements + .filter(isYearListElement) + .map(WMFEventFromListElement) + .sort(bcAwareEventComparator); +} + + +/** + * Gets array of WMFHoliday models from an array of list elements. + * @param {Array} listElements Array of document list elements + * @return {Array} Array of WMFHoliday models, one for each list element in 'listElements' argument + */ +function holidaysForHolidayListElements(listElements) { + return listElements.map(WMFHolidayFromListElement); +} + + +/** + * Gets array of WMFEvent models of births found in a document + * @param {Document} document Document to examine + * @return {Array} Array of WMFEvent models of births + */ +const birthsInDoc = (document) => { + return eventsForYearListElements( + document.querySelectorAll('h2#Births + ul li') + ); +}; + + +/** + * Gets array of WMFEvent models of deaths found in a document + * @param {Document} document Document to examine + * @return {Array} Array of WMFEvent models of deaths + */ +const deathsInDoc = (document) => { + return eventsForYearListElements( + document.querySelectorAll('h2#Deaths + ul li') + ); +}; + + +/** + * Gets array of WMFEvent models of events found in a document + * @param {Document} document Document to examine + * @return {Array} Array of WMFEvent models of events + */ +const eventsInDoc = (document) => { + return eventsForYearListElements( + document.querySelectorAll('h2#Events + ul li') + ); +}; + + +/** + * Gets array of WMFEvent models of holidays and observances found in a document + * @param {Document} document Document to examine + * @return {Array} Array of WMFEvent models of holidays and observances + */ +const holidaysInDoc = (document) => { + return holidaysForHolidayListElements( + document.querySelectorAll('h2#Holidays_and_observances + ul li') + ); +}; + + +/** + * Gets array of WMFEvent models of editor curated selected events found in a document + * @param {Document} document Document to examine + * @return {Array} Array of WMFEvent models of selections + */ +const selectionsInDoc = (document) => { + return eventsForYearListElements( + document.querySelectorAll('body > ul li') + ); +}; + + +/** + * Gets dictionary of arrays of WMFEvent models of all types: 'births', 'deaths', 'events', 'holidays' and 'selected' + * @param {Document} dayDoc Document of events on a given day + * @param {Document} selectionsDoc Document of editor curated events for a given day + * @return {Dictionary} Dictionary with keys for arrays of 'births', 'deaths', 'events', 'holidays' and 'selected' + */ +const everythingInDayAndSelectionsDocs = (dayDoc, selectionsDoc) => { + return { + selected: selectionsInDoc(selectionsDoc), + births: birthsInDoc(dayDoc), + deaths: deathsInDoc(dayDoc), + events: eventsInDoc(dayDoc), + holidays: holidaysInDoc(dayDoc) + }; +}; + + +/** + * Determines whether a dom object has a 'title' propery + * @param {Object} object Dom object to examine + * @return {Boolean} True if the object has a 'title' property + */ +function hasTitle(object) { + return ( + Object.prototype.hasOwnProperty.call(object, 'title') && + typeof object.title === 'string' + ); +} + + +/** + * Replaces 'title' property of a dom object with a '$merge' property set to the restbase url for that title + * @param {Object} object Dom object to examine + * @param {String} domain Domain + */ +function hydrateTitle(object, domain) { + const title = object.title; + delete object.title; + object.$merge = [ mUtil.getRbPageSummaryUrl(app.restbase_tpl, domain, title) ]; +} + + +/** + * Recursively hydrates all 'title' properties found in a dom object hierarchy + * @param {Object} object Dom object to examine + * @param {String} domain Domain + */ +function hydrateAllTitles(object, domain) { + for (const property in object) { + if (Object.prototype.hasOwnProperty.call(object, property)) { + if (typeof object[property] === 'object') { + hydrateAllTitles(object[property], domain); + } else if (hasTitle(object)) { + hydrateTitle(object, domain); + } + } + } +} + + +/** + * Ends a response. Hydrates titles and sets eTags, status etc. + * @param {Object} res Response to end + * @param {Object} output Payload to JSONify and deliver + * @param {String} domain Domain + * @param {String} etag eTag + */ +const endResponseWithOutput = (res, output, domain, etag) => { + // Hydrate titles just before responding. Otherwise you'd have to leak + // 'domain' details all the way down to the WMFPage constructor (which + // destroys promise chain simplicity). + hydrateAllTitles(output, domain); + + res.status(200); + mUtil.setETag(res, etag); + mUtil.setContentType(res, mUtil.CONTENT_TYPES.onthisday); + res.json(output).end(); +}; + + +/** + * Fetches document and eTag revision for URI + * @param {Object} req Request + * @param {Function} uriFunction Function reference for converting request to URI + * @return {Promise} Promise resolving to array containing [document, etagRevision] for URI + */ +function fetchDocAndEtagRevision(req, uriFunction) { + let etagRevision; + return preq.get(uriFunction(req)) + .then(response => { + etagRevision = parsoid.getRevisionFromEtag(response.headers); + return response.body; + }) + .then(domino.createDocument) + .then(doc => [doc, etagRevision]); +} + + +/** + * Fetches document for URI, extracts sought elements, responds + * @param {Object} req Request + * @param {Object} res Response + * @param {Function} uriFunction Function reference for converting request to URI + * @param {Function} extractionFunction Function reference for extracting sought elements (births, deaths, holidays, etc) + * @return {Promise} Promise resolving when response has completed + */ +function fetchAndRespond(req, res, uriFunction, extractionFunction) { + return fetchDocAndEtagRevision(req, uriFunction) + .then(([doc, etagRevision]) => { + const output = extractionFunction(doc); + endResponseWithOutput(res, output, req.params.domain, etagRevision); + }); +} + + +/** + * ENDPOINT for 'births' from 'Births' section of 'day' pages like: https://en.m.wikipedia.org/wiki/May_20 + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/births/01/30 + */ +router.get('/births/:mm/:dd', (req, res) => { + return fetchAndRespond(req, res, uriForDayPageRequest, birthsInDoc); +}); + + +/** + * ENDPOINT for 'deaths' from 'Deaths' section of 'day' pages like: https://en.m.wikipedia.org/wiki/May_20 + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/deaths/01/30 + */ +router.get('/deaths/:mm/:dd', (req, res) => { + return fetchAndRespond(req, res, uriForDayPageRequest, deathsInDoc); +}); + + +/** + * ENDPOINT for 'events' from 'Events' section of 'day' pages like: https://en.m.wikipedia.org/wiki/May_20 + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/events/01/30 + */ +router.get('/events/:mm/:dd', (req, res) => { + return fetchAndRespond(req, res, uriForDayPageRequest, eventsInDoc); +}); + + +/** + * ENDPOINT for 'holidays' from 'Holiday and observances' section of 'day' pages like: https://en.m.wikipedia.org/wiki/May_20 + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/holidays/01/30 + */ +router.get('/holidays/:mm/:dd', (req, res) => { + return fetchAndRespond(req, res, uriForDayPageRequest, holidaysInDoc); +}); + + +/** + * ENDPOINT for 'selected' editor curated events from pages like: https://en.m.wikipedia.org/wiki/Wikipedia:On_this_day/Today + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/selected/01/30 + */ +router.get('/selected/:mm/:dd', (req, res) => { + return fetchAndRespond(req, res, uriForSelectedPageRequest, selectionsInDoc); +}); + + +/** + * ENDPOINT for 'all' - everything ('births', 'deaths', 'events', 'holidays' and 'selected') all in one go + * Example: http://localhost:6927/en.wikipedia.org/v1/onthisday/all/01/30 + */ +router.get('/all/:mm/:dd', (req, res) => { + return Promise.all([ + fetchDocAndEtagRevision(req, uriForDayPageRequest), + fetchDocAndEtagRevision(req, uriForSelectedPageRequest) + ]) + .then(([[dayDoc, dayEtagRevision], [selectionsDoc, selectionsEtagRevision]]) => { + const etag = Math.max(dayEtagRevision, selectionsEtagRevision); + const output = everythingInDayAndSelectionsDocs(dayDoc, selectionsDoc); + endResponseWithOutput(res, output, req.params.domain, etag); + }); +}); + + +module.exports = function(appObj) { + app = appObj; + return { + path: '/onthisday', + api_version: 1, + router, + testing: { // Testing namespace + uriForDayPageRequest, + uriForSelectedPageRequest, + titleForDayPageFromMonthDayNumberStrings, + WMFPage, + WMFEvent, + WMFHoliday, + WMFHolidayFromListElement, + WMFEventFromListElement, + WMFPageFromAnchorElement, + eventsForYearListElements, + isYearListElement, + bcAwareEventComparator, + hydrateAllTitles + } + }; +}; + + +/* eslint-enable arrow-parens, max-len */ diff --git a/test/features/onthisday/onthisday.js b/test/features/onthisday/onthisday.js new file mode 100644 index 0000000..4ccdfc5 --- /dev/null +++ b/test/features/onthisday/onthisday.js @@ -0,0 +1,387 @@ +'use strict'; + +/* eslint-disable max-len, arrow-parens, new-cap */ + +const server = require('../../utils/server.js'); +const onThisDay = require('../../../routes/on-this-day.js')(); +const assert = require('../../utils/assert.js'); +const domino = require('domino'); +const preq = require('preq'); +const headers = require('../../utils/headers'); + + +// MOCK REQUESTS + +const REQUEST_FOR_EN_01_30 = { + params: { + mm: '01', + dd: '30', + domain: 'en.wikipedia.org' + } +}; + +const REQUEST_FOR_EN_12_01 = { + params: { + mm: '12', + dd: '01', + domain: 'en.wikipedia.org' + } +}; + +const REQUEST_FOR_EN_1_1 = { + params: { + mm: '1', + dd: '1', + domain: 'en.wikipedia.org' + } +}; + + +// MOCK ANCHORS + +// Events on selected anniversary pages (like https://en.wikipedia.org/wiki/Wikipedia:Selected_anniversaries/January_30 ) often +// have certain anchors bolded to signify they refer to the main "topic" of the event. We mock a document here with a topical and +// a non-topical anchor for testing our model objects. +const DOCUMENT_WITH_TOPIC_AND_NON_TOPIC_ANCHORS = domino.createDocument( + '<html><body>' + + '<b><a href="./TOPIC_DBTITLE" title="TOPIC_ANCHOR_TITLE">TOPIC_ANCHOR_TEXT</a></b>' + + '<a href="./NON_TOPIC_DBTITLE" title="NON_TOPIC_ANCHOR_TITLE">NON_TOPIC_ANCHOR_TEXT</a>' + + '</body></html>' +); +const MOCK_ANCHORS = Array.from(DOCUMENT_WITH_TOPIC_AND_NON_TOPIC_ANCHORS.querySelectorAll('a')); +const TOPIC_ANCHOR = MOCK_ANCHORS[0]; +const NON_TOPIC_ANCHOR = MOCK_ANCHORS[1]; + + +// MOCK LIST ELEMENTS + +const DOCUMENT_WITH_EVENT_AND_HOLIDAY_LIST_ELEMENTS = domino.createDocument( + '<html><body><ul>' + + // From: https://en.wikipedia.org/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2FJanuary_3 + '<li> <a rel="mw:WikiLink" href="./1946" title="1946">1946</a> – Canadian-American <a rel="mw:WikiLink" href="./Jockey" title="Jockey">jockey</a> <b><a rel="mw:WikiLink" href="./George_Woolf" title="George Woolf">George Woolf</a></b>, who rode <a rel="mw:WikiLink" href="./Seabiscuit" title="Seabiscuit">Seabiscuit</a> to a famous victory over <a rel="mw:WikiLink" href="./War_Admiral" title="War Admiral">War Admiral</a> in 1938, was fatally injured when he fell from his horse during a race.</li>' + + // From: https://en.wikipedia.org/api/rest_v1/page/html/January_30#Births + '<li id="mwAR8"><a rel="mw:WikiLink" href="./58_BC" title="58 BC" id="mwASA">58 BC</a><span typeof="mw:Entity" id="mwASE">–</span> <a rel="mw:WikiLink" href="./Livia" title="Livia" id="mwASI">Livia</a>, Roman wife of <a rel="mw:WikiLink" href="./Augustus" title="Augustus" id="mwASM">Augustus</a> (d. 29)</li>' + + // From: https://en.wikipedia.org/api/rest_v1/page/html/January_30#Events + '<li id="mwCQ"> <a rel="mw:WikiLink" href="./516_BCE" title="516 BCE" id="mwCg">516 BCE</a> <span typeof="mw:Entity" id="mwCw">–</span> The <a rel="mw:WikiLink" href="./Second_Temple" title="Second Temple" id="mwDA">Second Temple</a> of Jerusalem finishes construction. </li>' + + // From: https://en.wikipedia.org/api/rest_v1/page/html/January_30#Deaths + '<li id="mwA_0"> 1948 <span typeof="mw:Entity" id="mwA_4">–</span> <a rel="mw:WikiLink" href="./Mahatma_Gandhi" title="Mahatma Gandhi" id="mwA_8">Mahatma Gandhi</a>, Indian lawyer, philosopher, and activist (b. 1869)</li>' + + // From: https://en.wikipedia.org/api/rest_v1/page/html/January_30#Holidays_and_observances + '<li id="mwBPE"> Martyrdom of <a rel="mw:WikiLink" href="./Mahatma_Gandhi" title="Mahatma Gandhi" id="mwBPI">Mahatma Gandhi</a>-related observances:' + + '<ul id="mwBPM">' + + '\n<li id="mwBPQ"> <a rel="mw:WikiLink" href="./Martyrs\'_Day_(India)" title="Martyrs\' Day (India)" id="mwBPU">Martyrs\' Day (India)</a></li>' + + '\n<li id="mwBPY"> <a rel="mw:WikiLink" href="./School_Day_of_Non-violence_and_Peace" title="School Day of Non-violence and Peace" id="mwBPc">School Day of Non-violence and Peace</a> (<a rel="mw:WikiLink" href="./Spain" title="Spain" id="mwBPg">Spain</a>)</li>' + + '\n<li id="mwBPk"> Start of the <a rel="mw:WikiLink" href="./Season_for_Nonviolence" title="Season for Nonviolence" id="mwBPo">Season for Nonviolence</a> January 30-April 4</li>' + + '</ul>' + + '</li>' + + '</ul></body></html>' +); +const MOCK_EVENT_LIST_ELEMENTS = Array.from(DOCUMENT_WITH_EVENT_AND_HOLIDAY_LIST_ELEMENTS.querySelectorAll('li')); +const SEABISCUIT_SELECTED_LIST_ELEMENT = MOCK_EVENT_LIST_ELEMENTS[0]; +const LIVIA_BIRTH_LIST_ELEMENT = MOCK_EVENT_LIST_ELEMENTS[1]; +const TEMPLE_EVENT_LIST_ELEMENT = MOCK_EVENT_LIST_ELEMENTS[2]; +const GANDHI_DEATH_LIST_ELEMENT = MOCK_EVENT_LIST_ELEMENTS[3]; +const MARTYRDOM_HOLIDAY_LIST_ELEMENT = MOCK_EVENT_LIST_ELEMENTS[4]; + + +// TESTS + +describe('onthisday', function() { + + this.timeout(20000); // eslint-disable-line no-invalid-this + + before(() => { return server.start(); }); + + + it('"births" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/births/01/01`); + }); + it('"deaths" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/deaths/01/01`); + }); + it('"events" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/events/01/01`); + }); + it('"holidays" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/holidays/01/01`); + }); + it('"selected" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/selected/01/01`); + }); + it('"all" should respond to GET request with expected headers, incl. CORS and CSP headers', () => { + headers.checkHeaders(`${server.config.uri}en.wikipedia.org/v1/onthisday/all/01/01`); + }); + + + // TEST PAGE TITLE GENERATION + + it('titleForDayPageFromMonthDayNumberStrings returns expected title for 1 digit month and 1 digit day', () => { + assert.deepEqual(onThisDay.testing.titleForDayPageFromMonthDayNumberStrings('1', '1'), 'January_1'); + }); + it('titleForDayPageFromMonthDayNumberStrings returns expected title for 0 padded month and 1 digit day', () => { + assert.deepEqual(onThisDay.testing.titleForDayPageFromMonthDayNumberStrings('01', '1'), 'January_1'); + }); + it('titleForDayPageFromMonthDayNumberStrings returns expected title for 0 padded month and 0 padded day', () => { + assert.deepEqual(onThisDay.testing.titleForDayPageFromMonthDayNumberStrings('01', '01'), 'January_1'); + }); + + + // TEST DAY PAGE URI GENERATION + + it('uriForDayPageRequest returns expected URI for 0 padded month and 2 digit day', () => { + assert.deepEqual(onThisDay.testing.uriForDayPageRequest(REQUEST_FOR_EN_01_30), 'https://en.wikipedia.org/api/rest_v1/page/html/January_30'); + }); + it('uriForDayPageRequest returns expected URI for 2 digit month and 0 padded day', () => { + assert.deepEqual(onThisDay.testing.uriForDayPageRequest(REQUEST_FOR_EN_12_01), 'https://en.wikipedia.org/api/rest_v1/page/html/December_1'); + }); + it('uriForDayPageRequest returns expected URI for 1 digit month and 1 digit day', () => { + assert.deepEqual(onThisDay.testing.uriForDayPageRequest(REQUEST_FOR_EN_1_1), 'https://en.wikipedia.org/api/rest_v1/page/html/January_1'); + }); + + + // TEST SELECTED PAGE URI GENERATION + + it('uriForSelectedPageRequest returns expected URI for 0 padded month and 2 digit day', () => { + assert.deepEqual(onThisDay.testing.uriForSelectedPageRequest(REQUEST_FOR_EN_01_30), 'https://en.wikipedia.org/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2FJanuary_30'); + }); + it('uriForSelectedPageRequest returns expected URI for 2 digit month and 0 padded day', () => { + assert.deepEqual(onThisDay.testing.uriForSelectedPageRequest(REQUEST_FOR_EN_12_01), 'https://en.wikipedia.org/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2FDecember_1'); + }); + it('uriForSelectedPageRequest returns expected URI for 1 digit month and 1 digit day', () => { + assert.deepEqual(onThisDay.testing.uriForSelectedPageRequest(REQUEST_FOR_EN_1_1), 'https://en.wikipedia.org/api/rest_v1/page/html/Wikipedia%3ASelected_anniversaries%2FJanuary_1'); + }); + + + // TEST ANCHOR TO WMFPage TRANSFORMS + + it('WMFPage model object is correctly created from a topic anchor', () => { + assert.deepEqual(onThisDay.testing.WMFPageFromAnchorElement(TOPIC_ANCHOR), { + title: 'TOPIC_DBTITLE', + isTopic: true + }); + }); + + it('WMFPage model object is correctly created from a non-topic anchor', () => { + assert.deepEqual(onThisDay.testing.WMFPageFromAnchorElement(NON_TOPIC_ANCHOR), { + title: 'NON_TOPIC_DBTITLE', + isTopic: false + }); + }); + + + // TEST LIST ELEMENT TO WMFEvent TRANSFORMS + + it('WMFEvent model object is correctly created from a selected list element', () => { + assert.deepEqual(onThisDay.testing.WMFEventFromListElement(SEABISCUIT_SELECTED_LIST_ELEMENT), { + "text": "Canadian-American jockey George Woolf, who rode Seabiscuit to a famous victory over War Admiral in 1938, was fatally injured when he fell from his horse during a race.", + "pages": [ + { + "title": "Jockey", + "isTopic": false + }, + { + "title": "George_Woolf", + "isTopic": true + }, + { + "title": "Seabiscuit", + "isTopic": false + }, + { + "title": "War_Admiral", + "isTopic": false + } + ], + "year": "1946" + }); + }); + + it('WMFEvent model object is correctly created from a birth list element', () => { + assert.deepEqual(onThisDay.testing.WMFEventFromListElement(LIVIA_BIRTH_LIST_ELEMENT), { + "text": "Livia, Roman wife of Augustus (d. 29)", + "pages": [ + { + "title": "Livia", + "isTopic": false + }, + { + "title": "Augustus", + "isTopic": false + } + ], + "year": "58 BC" + }); + }); + + it('WMFEvent model object is correctly created from an event list element', () => { + assert.deepEqual(onThisDay.testing.WMFEventFromListElement(TEMPLE_EVENT_LIST_ELEMENT), { + "text": "The Second Temple of Jerusalem finishes construction.", + "pages": [ + { + "title": "Second_Temple", + "isTopic": false + } + ], + "year": "516 BCE" + }); + }); + + it('WMFEvent model object is correctly created from a death list element', () => { + assert.deepEqual(onThisDay.testing.WMFEventFromListElement(GANDHI_DEATH_LIST_ELEMENT), { + "text": "Mahatma Gandhi, Indian lawyer, philosopher, and activist (b. 1869)", + "pages": [ + { + "title": "Mahatma_Gandhi", + "isTopic": false + } + ], + "year": "1948" + }); + }); + + it('WMFHoliday model object is correctly created from a holiday list element', () => { + assert.deepEqual(onThisDay.testing.WMFHolidayFromListElement(MARTYRDOM_HOLIDAY_LIST_ELEMENT), { + "text": "Martyrdom of Mahatma Gandhi-related observances:\n Martyrs' Day (India)\n School Day of Non-violence and Peace (Spain)\n Start of the Season for Nonviolence January 30-April 4", + "pages": [ + { + "title": "Mahatma_Gandhi", + "isTopic": false + }, + { + "title": "Martyrs'_Day_(India)", + "isTopic": false + }, + { + "title": "School_Day_of_Non-violence_and_Peace", + "isTopic": false + }, + { + "title": "Spain", + "isTopic": false + }, + { + "title": "Season_for_Nonviolence", + "isTopic": false + } + ] + }); + }); + + + // LIVE TEST ENDPOINT INTERNALS PRODUCE AT LEAST SOME RESULTS FOR A GIVEN DAY. + // DO NOT TEST FOR EXACT RESULT COUNT - THESE CHANGE AS PAGES ARE EDITED. + // INSTEAD TEST THAT AT LEAST SOME RESULTS ARE RETURNED. + + function january30uriForEndpointName(endpointName) { + return `${server.config.uri}en.wikipedia.org/v1/onthisday/${endpointName}/01/30/`; + } + function getJanuary30ResponseForEndpointName(endpointName) { + return preq.get(january30uriForEndpointName(endpointName)); + } + function verifyNonZeroEndpointResults(response, endpointName) { + assert.ok(response.body.length > 0, `${endpointName} should have fetched some results`); + } + function fetchAndVerifyNonZeroResultsForEndpointName(endpointName) { + return getJanuary30ResponseForEndpointName(endpointName) + .then((response) => { + verifyNonZeroEndpointResults(response, endpointName); + }); + } + it('BIRTHS fetches some results', () => { + return fetchAndVerifyNonZeroResultsForEndpointName('births'); + }); + + it('DEATHS fetches some results', () => { + return fetchAndVerifyNonZeroResultsForEndpointName('deaths'); + }); + + it('EVENTS fetches some results', () => { + return fetchAndVerifyNonZeroResultsForEndpointName('events'); + }); + + it('HOLIDAYS fetches some results', () => { + return fetchAndVerifyNonZeroResultsForEndpointName('holidays'); + }); + + it('SELECTED fetches some results', () => { + return fetchAndVerifyNonZeroResultsForEndpointName('selected'); + }); + + it('ALL fetches some results for births, deaths, events, holidays and selected', () => { + return getJanuary30ResponseForEndpointName('all') + .then((response) => { + assert.ok(response.body.births.length > 0, 'ALL should return some births'); + assert.ok(response.body.deaths.length > 0, 'ALL should return some deaths'); + assert.ok(response.body.events.length > 0, 'ALL should return some events'); + assert.ok(response.body.holidays.length > 0, 'ALL should return some holidays'); + assert.ok(response.body.selected.length > 0, 'ALL should return some selected'); + }); + }); + + + it('eventsForYearListElements returns a WMFEvent for only year list elements', () => { + assert.ok(onThisDay.testing.eventsForYearListElements(MOCK_EVENT_LIST_ELEMENTS).length === 4, 'Should return WMFEvent for each of 4 year list elements'); + }); + + + it('Correct year list element determination', () => { + assert.ok(onThisDay.testing.isYearListElement(SEABISCUIT_SELECTED_LIST_ELEMENT)); + }); + it('Correct year list element determination', () => { + assert.ok(onThisDay.testing.isYearListElement(LIVIA_BIRTH_LIST_ELEMENT)); + }); + it('Correct year list element determination', () => { + assert.ok(onThisDay.testing.isYearListElement(TEMPLE_EVENT_LIST_ELEMENT)); + }); + it('Correct year list element determination', () => { + assert.ok(onThisDay.testing.isYearListElement(GANDHI_DEATH_LIST_ELEMENT)); + }); + it('Correct year list element determination', () => { + assert.ok(!onThisDay.testing.isYearListElement(MARTYRDOM_HOLIDAY_LIST_ELEMENT)); + }); + + + it('Sort year list events in correct BC[E] aware manner', () => { + const sortedEvents = onThisDay.testing.eventsForYearListElements(MOCK_EVENT_LIST_ELEMENTS).sort(onThisDay.testing.bcAwareEventComparator); + assert.ok(sortedEvents[0].year === '1948'); + assert.ok(sortedEvents[1].year === '1946'); + assert.ok(sortedEvents[2].year === '58 BC'); + assert.ok(sortedEvents[3].year === '516 BCE'); + }); + + + it('Hydration should replace each \'title\' key with \'$merge\' key', () => { + const events = onThisDay.testing.eventsForYearListElements(MOCK_EVENT_LIST_ELEMENTS); + events.push(onThisDay.testing.WMFHolidayFromListElement(MARTYRDOM_HOLIDAY_LIST_ELEMENT)); + + // Initially each page should have a title key but no $merge key + for (const event of events) { + for (const page of event.pages) { + assert.ok(Object.prototype.hasOwnProperty.call(page, 'title')); + assert.ok(Object.prototype.hasOwnProperty.call(page, '$merge') === false); + assert.ok(page.title !== null); + } + } + + // Hydrate! + onThisDay.testing.hydrateAllTitles(events, REQUEST_FOR_EN_01_30); + + // After hydration each page should have a $merge key but no title key + for (const event of events) { + for (const page of event.pages) { + assert.ok(Object.prototype.hasOwnProperty.call(page, '$merge')); + assert.ok(Object.prototype.hasOwnProperty.call(page, 'title') === false); + assert.ok(page.$merge !== null); + } + } + + // Confirm expected number of pages exist post-hydration + assert.ok(events[0].pages.length === 1); + assert.ok(events[1].pages.length === 4); + assert.ok(events[2].pages.length === 2); + assert.ok(events[3].pages.length === 1); + assert.ok(events[4].pages.length === 5); + }); +}); + + +/* eslint-enable max-len, arrow-parens, new-cap */ -- To view, visit https://gerrit.wikimedia.org/r/330820 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I21565e73269b4ed5e717f692aebe2084401673a6 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/services/mobileapps Gerrit-Branch: master Gerrit-Owner: Mhurd <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
