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

Reply via email to