diff --git a/cms/__tests__/routes/api/items.test.js b/cms/__tests__/routes/api/items.test.js new file mode 100644 index 00000000..cd9d2293 --- /dev/null +++ b/cms/__tests__/routes/api/items.test.js @@ -0,0 +1,93 @@ +jest.mock("keystone", () => { + const item = { + model: { + find: jest.fn() + } + }; + + return { + list: () => item + }; +}); + +describe("items", () => { + let keystone, daily, Item, request, response, json; + + beforeEach(() => { + jest.resetModules(); + + keystone = require("keystone"); + Item = keystone.list("Item"); + daily = require("../../../routes/api/items").daily; + request = { params: {} }; + json = jest.fn(); + response = { + json, + badRequest: jest.fn(), + error: jest.fn() + }; + }); + + describe("daily", () => {}); + + it("should return a 400 bad request when the date format is invalid", async () => { + const date = "not a date"; + await daily({ ...request, ...{ params: { date } } }, response); + + expect(response.badRequest).toHaveBeenCalledWith( + "Couldn't get daily items", + "Date 'not a date' is not in the format YYYY-M-D", + true + ); + }); + + it("should search for daily items between start and end of the given date", async () => { + const date = "2020-01-09"; + await daily({ ...request, ...{ params: { date } } }, response); + + expect(Item.model.find).toHaveBeenCalledWith({ + createdAt: { + $gte: new Date(Date.parse("2020-01-09")), + $lt: new Date(Date.parse("2020-01-09 23:59:59.999Z")) + } + }); + }); + + it("should return a 500 JSON error response when there's an error getting the daily items", async () => { + const error = new Error("An error getting daily items"); + + Item.model.find.mockImplementation(() => { + throw error; + }); + + await daily( + { ...request, ...{ params: { date: "2020-01-09" } } }, + response + ); + + expect(response.error).toHaveBeenCalledWith(error, true); + }); + + it("should return the found item as json with whitelist of fields", async () => { + const items = [{ title: "test", excludedField: "not used" }]; + + Item.model.find.mockImplementation(() => { + return { + populate: () => ({ + populate: () => ({ + populate: () => ({ + exec: () => items + }) + }) + }) + }; + }); + + await daily( + { ...request, ...{ params: { date: "2020-01-09" } } }, + response + ); + + expect(json).toHaveBeenCalledWith([{ title: "test" }]); + }); +}); diff --git a/cms/evidence-types.jsonld b/cms/evidence-types.jsonld new file mode 100644 index 00000000..9c9320b2 --- /dev/null +++ b/cms/evidence-types.jsonld @@ -0,0 +1,371 @@ +{ + "@graph" : [ { + "@id" : "mas_evidence_types:Audit%20reports", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Audit reports" + } + }, { + "@id" : "mas_evidence_types:CONCEPT-Evidence_type", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "prefLabel" : { + "@language" : "en", + "@value" : "Evidence type" + } + }, { + "@id" : "mas_evidence_types:CONCEPT-Guidance_and_advice", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Guidance and advice" + } + }, { + "@id" : "mas_evidence_types:Care%20pathways", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Care pathways" + } + }, { + "@id" : "mas_evidence_types:Commissioning%20guides", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Guidance_and_advice", + "prefLabel" : { + "@language" : "en", + "@value" : "Commissioning guides" + } + }, { + "@id" : "mas_evidence_types:Drug%20best%20practice%20guidance", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Guidance_and_advice", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug best practice guidance" + } + }, { + "@id" : "mas_evidence_types:Drug%20costs", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug costs" + } + }, { + "@id" : "mas_evidence_types:Drug%20horizon%20scanning", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug horizon scanning" + } + }, { + "@id" : "mas_evidence_types:Drug%20prescribing", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Guidance_and_advice", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug prescribing" + } + }, { + "@id" : "mas_evidence_types:Drug%20regulatory%20and%20marketing", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug regulatory and marketing" + } + }, { + "@id" : "mas_evidence_types:Drug%2Fmedicines%20management", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Drug/medicines management" + } + }, { + "@id" : "mas_evidence_types:Effective%20practice%20examples", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Effective practice examples" + } + }, { + "@id" : "mas_evidence_types:Evidence%20summaries", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Evidence%20summary", + "prefLabel" : { + "@language" : "en", + "@value" : "Evidence summaries" + } + }, { + "@id" : "mas_evidence_types:Evidence%20summary", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Evidence summary" + } + }, { + "@id" : "mas_evidence_types:Evidence%20updates", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Evidence%20summary", + "prefLabel" : { + "@language" : "en", + "@value" : "Evidence updates" + } + }, { + "@id" : "mas_evidence_types:Evidence-based%20management%20reports", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Evidence-based management reports" + } + }, { + "@id" : "mas_evidence_types:Eyes%20on%20evidence%20commentaries", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Evidence%20summary", + "prefLabel" : { + "@language" : "en", + "@value" : "Eyes on evidence commentaries" + } + }, { + "@id" : "mas_evidence_types:Guidance", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Guidance_and_advice", + "prefLabel" : { + "@language" : "en", + "@value" : "Guidance" + } + }, { + "@id" : "mas_evidence_types:Health%20technology%20assessments", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Systematic%20review", + "prefLabel" : { + "@language" : "en", + "@value" : "Health technology assessments" + } + }, { + "@id" : "mas_evidence_types:Implementation%20support%20tools", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Implementation support tools" + } + }, { + "@id" : "mas_evidence_types:Learning%20materials", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Learning materials" + } + }, { + "@id" : "mas_evidence_types:MAS_evidence_type", + "@type" : "http://www.w3.org/2004/02/skos/core#ConceptScheme", + "hasTopConcept" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "MAS evidence type" + } + }, { + "@id" : "mas_evidence_types:Media%20and%20commentaries", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Media and commentaries" + } + }, { + "@id" : "mas_evidence_types:Medicines%20Q%20%26%20A", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Evidence%20summary", + "prefLabel" : { + "@language" : "en", + "@value" : "Medicines Q & A" + } + }, { + "@id" : "mas_evidence_types:Medicines%20evidence%20commentaries", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Evidence%20summary", + "prefLabel" : { + "@language" : "en", + "@value" : "Medicines evidence commentaries" + } + }, { + "@id" : "mas_evidence_types:Ongoing%20or%20unpublished%20research", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Primary%20research", + "prefLabel" : { + "@language" : "en", + "@value" : "Ongoing or unpublished research" + } + }, { + "@id" : "mas_evidence_types:Other%20economic%20evaluations", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Other economic evaluations" + } + }, { + "@id" : "mas_evidence_types:Other%20evidence", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Other evidence" + } + }, { + "@id" : "mas_evidence_types:Other%20primary%20research", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Primary%20research", + "prefLabel" : { + "@language" : "en", + "@value" : "Other primary research" + } + }, { + "@id" : "mas_evidence_types:Patient%20decision%20aids", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Patient decision aids" + } + }, { + "@id" : "mas_evidence_types:Patient%20information", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Patient information" + } + }, { + "@id" : "mas_evidence_types:Policy", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Policy" + } + }, { + "@id" : "mas_evidence_types:Population%20intelligence", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Population intelligence" + } + }, { + "@id" : "mas_evidence_types:Population%20needs%20assessment", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Population needs assessment" + } + }, { + "@id" : "mas_evidence_types:Primary%20research", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Primary research" + } + }, { + "@id" : "mas_evidence_types:Quality%20measures", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Other%20evidence", + "prefLabel" : { + "@language" : "en", + "@value" : "Quality measures" + } + }, { + "@id" : "mas_evidence_types:Randomised%20controlled%20trials", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Primary%20research", + "prefLabel" : { + "@language" : "en", + "@value" : "Randomised controlled trials" + } + }, { + "@id" : "mas_evidence_types:Safety%20alerts", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Safety alerts" + } + }, { + "@id" : "mas_evidence_types:Systematic%20review", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:CONCEPT-Evidence_type", + "prefLabel" : { + "@language" : "en", + "@value" : "Systematic review" + } + }, { + "@id" : "mas_evidence_types:Systematic%20reviews", + "@type" : "http://www.w3.org/2004/02/skos/core#Concept", + "broader" : "mas_evidence_types:Systematic%20review", + "prefLabel" : { + "@language" : "en", + "@value" : "Systematic reviews" + } + }, { + "@id" : "urn:x-evn-master:mas_evidence_types", + "@type" : "owl:Ontology", + "status" : "metadata:UnderDevelopmentStatus", + "defaultNamespace" : "http://nice.org.uk/MAS/evidence_type/", + "newInstancesUserCannotModifyURI" : "0", + "comment" : "A list of evidence types includes in the Medicines Awareness Service", + "label" : "MAS evidence types", + "imports" : [ "http://topbraid.org/skos.shapes", "http://topbraid.org/imported" ] + } ], + "@context" : { + "prefLabel" : { + "@id" : "http://www.w3.org/2004/02/skos/core#prefLabel" + }, + "broader" : { + "@id" : "http://www.w3.org/2004/02/skos/core#broader", + "@type" : "@id" + }, + "imports" : { + "@id" : "http://www.w3.org/2002/07/owl#imports", + "@type" : "@id" + }, + "status" : { + "@id" : "http://topbraid.org/metadata#status", + "@type" : "@id" + }, + "label" : { + "@id" : "http://www.w3.org/2000/01/rdf-schema#label" + }, + "newInstancesUserCannotModifyURI" : { + "@id" : "http://topbraid.org/teamwork#newInstancesUserCannotModifyURI", + "@type" : "http://www.w3.org/2001/XMLSchema#boolean" + }, + "comment" : { + "@id" : "http://www.w3.org/2000/01/rdf-schema#comment" + }, + "defaultNamespace" : { + "@id" : "http://topbraid.org/swa#defaultNamespace" + }, + "hasTopConcept" : { + "@id" : "http://www.w3.org/2004/02/skos/core#hasTopConcept", + "@type" : "@id" + }, + "metadata" : "http://topbraid.org/metadata#", + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "owl" : "http://www.w3.org/2002/07/owl#", + "teamwork" : "http://topbraid.org/teamwork#", + "mas_evidence_types" : "http://nice.org.uk/MAS/evidence_type/", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#" + } +} diff --git a/cms/models/EvidenceType.js b/cms/models/EvidenceType.js index d67e5084..db05f9d4 100644 --- a/cms/models/EvidenceType.js +++ b/cms/models/EvidenceType.js @@ -1,4 +1,25 @@ -const keystone = require("keystone"); +const keystone = require("keystone"), + fs = require("fs"), + path = require("path"), + log4js = require("log4js"); + +const logger = log4js.getLogger("Item.js"); + +// Load the JSONLD for evidence types on application load +// so we don't have to hit the file system again and again +let evidenceTypesJsonLd; +fs.readFile( + path.resolve(__dirname, "../evidence-types.jsonld"), + "UTF-8", + function(err, contents) { + if (err) { + logger.error(err); + throw err; + } + + evidenceTypesJsonLd = JSON.parse(contents); + } +); const Types = keystone.Field.Types; @@ -25,6 +46,18 @@ EvidenceType.relationship({ refPath: "evidenceType" }); +EvidenceType.schema.virtual("broaderTitle").get(function() { + const key = this.key, + evidenceTypes = evidenceTypesJsonLd["@graph"], + concept = evidenceTypes.find(e => e["@id"] === key); + + // Assume only 2 levels + const broaderConcept = evidenceTypes.find(e => e["@id"] === concept.broader); + + if (broaderConcept.broader) return broaderConcept.prefLabel["@value"]; + else return concept.prefLabel["@value"]; +}); + EvidenceType.defaultColumns = "title, oldEPiServerId, key"; EvidenceType.register(); diff --git a/cms/models/Item.js b/cms/models/Item.js index 3b63fbc0..356fe167 100644 --- a/cms/models/Item.js +++ b/cms/models/Item.js @@ -3,7 +3,8 @@ const keystone = require("keystone"), https = require("https"), http = require("http"), log4js = require("log4js"), - utils = require("keystone-utils"); + utils = require("keystone-utils"), + _ = require("lodash"); const Types = keystone.Field.Types; @@ -107,6 +108,27 @@ Item.add({ } }); +Item.fullResponseFields = [ + "_id", + "title", + "slug", + "url", + "shortSummary", + "comment", + "resourceLinks", + "staticPath", + "source._id", + "source.title", + "specialities", + "evidenceType._id", + "evidenceType.title", + "evidenceType.key", + "evidenceType.broaderTitle", + "publicationDate", + "updatedAt", + "createdAt" +]; + Item.schema.pre("validate", function(next) { if (this.isInitial) { this.isInitial = false; @@ -155,7 +177,7 @@ const createWeeklyIfNeeded = async () => { Item.schema.post("save", async function(doc, next) { await createWeeklyIfNeeded(); - logger.info("Post save, sending request...", doc); + logger.info("Post save, sending request..."); let item; @@ -165,6 +187,8 @@ Item.schema.post("save", async function(doc, next) { .model.findById(doc._id) .populate("source") .populate("evidenceType") + .populate("specialities") + .select(Item.fullResponseFields.join(" ")) .exec(); } catch (err) { logger.error("An error occurred finding item: ", err.message); @@ -175,7 +199,8 @@ Item.schema.post("save", async function(doc, next) { const hostname = process.env.HOST_NAME; const hostport = process.env.HOST_PORT; - const data = JSON.stringify(item); + const data = JSON.stringify(_.pick(item, Item.fullResponseFields)); + logger.debug("Sending: ", data); var options = { hostname: hostname, diff --git a/cms/routes/api/items.js b/cms/routes/api/items.js index 589cbf66..a1f7a5a0 100644 --- a/cms/routes/api/items.js +++ b/cms/routes/api/items.js @@ -1,14 +1,22 @@ const keystone = require("keystone"), + _ = require("lodash"), + moment = require("moment"), Items = keystone.list("Item"); const log4js = require("log4js"), logger = log4js.getLogger(); -exports.single = function(req, res, next) { +/** + * Single item, given an id + * /api/items/5e205dadc12944a35f24572b + */ +exports.single = function(req, res) { Items.model .findById(req.params.itemId) .populate("source") .populate("evidenceType") + .populate("specialities") + .select(Items.fullResponseFields.join(" ")) .exec(function(err, item) { if (err) { logger.error(`Error getting item with id ${req.params.itemId}`, err); @@ -21,19 +29,20 @@ exports.single = function(req, res, next) { return res.notfound("Item not found", notFoundMsg, true); } - res.json(item); + const obj = _.pick(item, Items.fullResponseFields); + + return res.json(obj); }); }; /** - * List Items + * List of all items + * /api/items */ exports.list = function(req, res) { - // TODO: Pagination Items.model .find() - .populate("source") - .populate("evidenceType") + .select("title slug") .exec(function(err, items) { if (err) { logger.error(`Failed to get list of items`, err); @@ -43,3 +52,44 @@ exports.list = function(req, res) { res.json(items); }); }; + +/** + * Daily items for a given date + * /api/items/daily/2020-01-16 + */ +exports.daily = async function(req, res) { + const dateStr = req.params.date, + date = moment(dateStr, "YYYY-M-D", true); + + if (!date.isValid()) { + const errorMessage = `Date '${dateStr}' is not in the format YYYY-M-D`; + logger.error(errorMessage); + return res.badRequest("Couldn't get daily items", errorMessage, true); + } + + const startOfDay = date.clone().startOf("day"), + endOfDay = date.clone().endOf("day"); + + let items; + try { + items = await Items.model + .find({ + createdAt: { $gte: startOfDay.toDate(), $lt: endOfDay.toDate() } + }) + .populate("source") + .populate("evidenceType") + .populate("specialities") + .select(Items.fullResponseFields.join(" ")) + .exec(); + } catch (err) { + logger.error(`Error getting daily items for date ${dateStr}`, err); + return res.error(err, true); + } + + if (items.length === 0) + logger.warn(`Zero daily items found for date ${dateStr}`); + + const obj = _.map(items, _.partialRight(_.pick, Items.fullResponseFields)); + + res.json(obj); +}; diff --git a/cms/routes/index.js b/cms/routes/index.js index 3b4488c5..4559b728 100644 --- a/cms/routes/index.js +++ b/cms/routes/index.js @@ -44,6 +44,7 @@ exports = module.exports = function(app) { // Views app.get("/", routes.views.index); app.get("/api/items/:itemId", routes.api.items.single); + app.get("/api/items/daily/:date", routes.api.items.daily); app.get("/api/items", routes.api.items.list); app.get("/api/specialities/:specialityId", routes.api.specialities.single); app.get("/api/specialities", routes.api.specialities.list); diff --git a/cms/routes/middleware.js b/cms/routes/middleware.js index d94e6d6a..54d68ad1 100644 --- a/cms/routes/middleware.js +++ b/cms/routes/middleware.js @@ -108,5 +108,13 @@ exports.initErrorHandlers = function(req, res, next) { return res.status(404).send(keystone.wrapHTMLError(title, message)); }; + res.badRequest = function(title, message, useJson) { + if (useJson || req.is("json")) { + return res.status(400).json({ title, message }); + } + + return res.status(400).send(keystone.wrapHTMLError(title, message)); + }; + next(); }; diff --git a/lambda/MAS.Tests/Feeds/multiple-items.json b/lambda/MAS.Tests/Feeds/all-items.json similarity index 68% rename from lambda/MAS.Tests/Feeds/multiple-items.json rename to lambda/MAS.Tests/Feeds/all-items.json index c68a3ab5..ef0788d5 100644 --- a/lambda/MAS.Tests/Feeds/multiple-items.json +++ b/lambda/MAS.Tests/Feeds/all-items.json @@ -17,7 +17,18 @@ "publicationDate": "2019-11-25T13:48:36.000Z", "createdDate": "2019-10-22T15:05:05.927Z", "specialities": [ - "5daf1a5c8a34d485cb05b5ba" + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } ], "evidenceType": { "_id": "5df7abf383138898ee1f67ef", @@ -46,7 +57,18 @@ "publicationDate": "2019-11-23T13:48:36.000Z", "createdDate": "2019-10-23T10:38:43.000Z", "specialities": [ - "5daf1a5c8a34d478fd05b5d4" + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } ], "evidenceType": { "_id": "5df7abf383138898ee1f67ef", @@ -75,7 +97,18 @@ "publicationDate": "2019-10-23T00:00:00.000Z", "createdDate": "2019-10-23T10:56:40.000Z", "specialities": [ - "5daf1a5c8a34d4d6e505b5b9" + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } ], "evidenceType": { "_id": "5df7abf383138898ee1f67ef", @@ -104,7 +137,18 @@ "publicationDate": "2019-10-28T00:00:00.000Z", "createdDate": "2019-10-28T11:07:11.000Z", "specialities": [ - "5daf1a5c8a34d485cb05b5ba" + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } ], "evidenceType": { "_id": "5df7abf383138898ee1f67ef", diff --git a/lambda/MAS.Tests/Feeds/daily-items-2020-01-01.json b/lambda/MAS.Tests/Feeds/daily-items-2020-01-01.json new file mode 100644 index 00000000..f6e325a6 --- /dev/null +++ b/lambda/MAS.Tests/Feeds/daily-items-2020-01-01.json @@ -0,0 +1,95 @@ +[ + { + "_id": "5e208e42306e5d4f5ff39659", + "title": "Item 1", + "slug": "item-1", + "url": "https://www.abc.com", + "shortSummary": "This is some short summary", + "comment": "

This is some comment

", + "resourceLinks": "

https://www.abc.com

", + "staticPath": "item-1.html", + "source": { + "_id": "5e1ddf2a306e5d5250f3904a", + "title": "2020health" + }, + "specialities": [ + { + "_id": "5e1ddf2c306e5d0fa9f39611", + "key": "8f13726e-5635-471f-ad3c-fc910a6ac2b1", + "title": "Cancers", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5e1ddf2c306e5d6eabf39641", + "title": "Evidence summary - Evidence summaries", + "key": "mas_evidence_types:Evidence%20summaries", + "broaderTitle": "Evidence summary" + }, + "publicationDate": "2020-01-16T00:00:00.000Z", + "updatedAt": "2020-01-16T16:25:37.188Z", + "createdAt": "2020-01-16T16:24:34.985Z" + }, + { + "_id": "5e208f7f0348d6060f8d448c", + "title": "Item 2", + "slug": "item-2", + "url": "https://www.google.com", + "shortSummary": "Some short summary", + "comment": "

This is some comment

", + "resourceLinks": "

https://www.google.com/

", + "staticPath": "item-2.html", + "source": { + "_id": "5e1ddf2a306e5da2b0f3904d", + "title": "ABL Health Ltd" + }, + "specialities": [ + { + "_id": "5e1ddf2c306e5d4e6cf39631", + "key": "d0ea592c-932a-462f-b206-cf454e40fa20", + "title": "Stroke", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5e1ddf2c306e5d6eabf39641", + "title": "Evidence summary - Evidence summaries", + "key": "mas_evidence_types:Evidence%20summaries", + "broaderTitle": "Evidence summary" + }, + "publicationDate": "2020-01-16T00:00:00.000Z", + "updatedAt": "2020-01-16T16:31:11.333Z", + "createdAt": "2020-01-16T16:29:51.539Z" + }, + { + "_id": "5e208fe00348d67ca08d448d", + "title": "Item 3", + "slug": "item-3", + "url": "https://www.google.com/", + "shortSummary": "Some short summary", + "comment": "

Some comment

", + "resourceLinks": "

https://www.google.com/

\r\n

https://www.google.com/

\r\n

https://www.google.com/

", + "staticPath": "item-3.html", + "source": { + "_id": "5e1ddf2a306e5da2b0f3904d", + "title": "ABL Health Ltd" + }, + "specialities": [ + { + "_id": "5e1ddf2c306e5d7e78f3961f", + "key": "cb0faa85-1781-484e-a265-2715621c1be9", + "title": "Later life", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5e1ddf2c306e5dcf61f39644", + "title": "Evidence summary - Eyes on evidence commentaries", + "key": "mas_evidence_types:Eyes%20on%20evidence%20commentaries", + "broaderTitle": "Evidence summary" + }, + "publicationDate": "2020-01-16T00:00:00.000Z", + "updatedAt": "2020-01-16T16:32:02.535Z", + "createdAt": "2020-01-16T16:31:28.754Z" + } +] \ No newline at end of file diff --git a/lambda/MAS.Tests/Feeds/daily-items-single.json b/lambda/MAS.Tests/Feeds/daily-items-single.json new file mode 100644 index 00000000..e1537f99 --- /dev/null +++ b/lambda/MAS.Tests/Feeds/daily-items-single.json @@ -0,0 +1,42 @@ +[ + { + "_id": "5daf1aa18a34d4bb8405b5e0", + "slug": "testing-789", + "source": { + "_id": "5de526fc43e373581b7810c8", + "oldEPiServerId": 2, + "title": "ACP Journal Club", + "__v": 0 + }, + "shortSummary": "testing", + "resourceLinks": "http://localhost:3010/keystone/items", + "url": "https://www.nhs.uk/", + "title": "Wonder Drug", + "__v": 0, + "comment": "

A

", + "publicationDate": "2019-11-25T13:48:36.000Z", + "createdDate": "2019-10-22T15:05:05.927Z", + "specialities": [ + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5df7abf383138898ee1f67ef", + "title": "Safety alerts", + "key": "mas_evidence_types:Safety%20alerts", + "oldEPiServerId": 778708, + "__v": 0 + }, + "isInitial": false + } +] \ No newline at end of file diff --git a/lambda/MAS.Tests/Feeds/daily-items.json b/lambda/MAS.Tests/Feeds/daily-items.json new file mode 100644 index 00000000..ef0788d5 --- /dev/null +++ b/lambda/MAS.Tests/Feeds/daily-items.json @@ -0,0 +1,162 @@ +[ + { + "_id": "5daf1aa18a34d4bb8405b5e0", + "slug": "testing-789", + "source": { + "_id": "5de526fc43e373581b7810c8", + "oldEPiServerId": 2, + "title": "ACP Journal Club", + "__v": 0 + }, + "shortSummary": "testing", + "resourceLinks": "http://localhost:3010/keystone/items", + "url": "https://www.nhs.uk/", + "title": "Wonder Drug", + "__v": 0, + "comment": "

A

", + "publicationDate": "2019-11-25T13:48:36.000Z", + "createdDate": "2019-10-22T15:05:05.927Z", + "specialities": [ + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5df7abf383138898ee1f67ef", + "title": "Safety alerts", + "key": "mas_evidence_types:Safety%20alerts", + "oldEPiServerId": 778708, + "__v": 0 + }, + "isInitial": false + }, + { + "_id": "5db02d6d8a34d43bde05b5e1", + "slug": "my-medicine", + "source": { + "_id": "5de5261b8ce66f142e44cd08", + "oldEPiServerId": 1394, + "title": "ADHD Awareness Month", + "__v": 0 + }, + "shortSummary": "this is my medicine", + "resourceLinks": "http://localhost:3010/keystone/items", + "url": "https://www.nice.org.uk/", + "title": "MY MEDICINE", + "__v": 0, + "comment": "

this is my comment

", + "publicationDate": "2019-11-23T13:48:36.000Z", + "createdDate": "2019-10-23T10:38:43.000Z", + "specialities": [ + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5df7abf383138898ee1f67ef", + "title": "Safety alerts", + "key": "mas_evidence_types:Safety%20alerts", + "oldEPiServerId": 778708, + "__v": 0 + }, + "isInitial": false + }, + { + "_id": "5db031d78a34d4a69905b5e3", + "slug": "hello-world", + "source": { + "_id": "5de52a02c676eee677e97f41", + "oldEPiServerId": 2, + "title": "Substance Misuse Management in General Practice", + "__v": 0 + }, + "shortSummary": "test", + "resourceLinks": "http://localhost:3010/keystone/items", + "url": "https://www.bbc.co.uk/", + "title": "Hello world", + "__v": 0, + "comment": "

comment

", + "publicationDate": "2019-10-23T00:00:00.000Z", + "createdDate": "2019-10-23T10:56:40.000Z", + "specialities": [ + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5df7abf383138898ee1f67ef", + "title": "Safety alerts", + "key": "mas_evidence_types:Safety%20alerts", + "oldEPiServerId": 778708, + "__v": 0 + }, + "isInitial": false + }, + { + "_id": "5db6cbcc8a34d4ca5905b5e4", + "slug": "hello-there", + "source": { + "_id": "5de52a01c676ee589ae97d2a", + "oldEPiServerId": 2, + "title": "Journal of Surgical Oncology", + "__v": 0 + }, + "shortSummary": "hello", + "resourceLinks": "http://localhost:3010/keystone/items", + "url": "https://www.google.com/", + "title": "A Placebo", + "__v": 0, + "comment": "

test

", + "publicationDate": "2019-10-28T00:00:00.000Z", + "createdDate": "2019-10-28T11:07:11.000Z", + "specialities": [ + { + "_id": "5e0e4331200a585f718e1ee5", + "key": "be1c2e2f-745e-4a82-b5aa-d4cef4d31a1b", + "title": "Anaesthesia and pain", + "__v": 0 + }, + { + "_id": "5e0e4331200a58dead8e1ee8", + "key": "53fc67a4-46d8-4171-9447-7fcf216c8749", + "title": "Complementary and alternative therapies", + "__v": 0 + } + ], + "evidenceType": { + "_id": "5df7abf383138898ee1f67ef", + "title": "Safety alerts", + "key": "mas_evidence_types:Safety%20alerts", + "oldEPiServerId": 778708, + "__v": 0 + }, + "isInitial": false + } +] \ No newline at end of file diff --git a/lambda/MAS.Tests/Infrastructure/MASWebApplicationFactory.cs b/lambda/MAS.Tests/Infrastructure/MASWebApplicationFactory.cs new file mode 100644 index 00000000..38b75bca --- /dev/null +++ b/lambda/MAS.Tests/Infrastructure/MASWebApplicationFactory.cs @@ -0,0 +1,46 @@ +using MAS.Configuration; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace MAS.Tests.Infrastructure +{ + /// + /// See https://github.com/aspnet/AspNetCore.Docs/issues/7063#issuecomment-414661566 + /// + public class MASWebApplicationFactory : WebApplicationFactory + { + protected override IWebHostBuilder CreateWebHostBuilder() + { + return WebHost.CreateDefaultBuilder() + .UseStartup() + .UseLambdaServer(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + var projectDir = Directory.GetCurrentDirectory(); + + builder.ConfigureAppConfiguration((context, conf) => + { + conf.AddJsonFile(Path.Combine(projectDir, "appsettings.test.json")); + }); + + builder.ConfigureTestServices(services => { + AppSettings.CMSConfig.BaseUrl = new Uri("file://" + projectDir + "/Feeds").ToString(); + }); + + base.ConfigureWebHost(builder); + } + } +} diff --git a/lambda/MAS.Tests/Infrastructure/TestAppSettings.cs b/lambda/MAS.Tests/Infrastructure/TestAppSettings.cs index 20f00c4c..1fb859af 100644 --- a/lambda/MAS.Tests/Infrastructure/TestAppSettings.cs +++ b/lambda/MAS.Tests/Infrastructure/TestAppSettings.cs @@ -4,22 +4,36 @@ namespace MAS.Tests.Infrastructure { - public class TestAppSettings + /// + /// Use this in integration tests to override CMS config per test e.g. AppSettings.CMSConfig = TestAppSettings.CMS.InvalidURI; + /// + public static class TestAppSettings { - public static CMSConfig GetInvalidURI() + public static class CMS { - return new CMSConfig() + public static CMSConfig Default { - URI = new Uri("file://" + Directory.GetCurrentDirectory() + "/Feeds/nonexistanturl").ToString() - }; - } + get + { + return new CMSConfig() + { + BaseUrl = new Uri("file://" + Directory.GetCurrentDirectory() + "/Feeds").ToString(), + AllItemsPath = "/all-items.json", + DailyItemsPath = "/daily-items.json", + }; + } + } - public static CMSConfig GetMultipleItemsFeed() - { - return new CMSConfig() + public static CMSConfig InvalidURI { - URI = new Uri("file://" + Directory.GetCurrentDirectory() + "/Feeds/multiple-items.json").ToString() - }; + get + { + return new CMSConfig() + { + BaseUrl = new Uri("file://" + Directory.GetCurrentDirectory() + "/Feeds/nonexistanturl").ToString() + }; + } + } } } } diff --git a/lambda/MAS.Tests/Infrastructure/TestBase.cs b/lambda/MAS.Tests/Infrastructure/TestBase.cs index e2264ec9..bec7c97b 100644 --- a/lambda/MAS.Tests/Infrastructure/TestBase.cs +++ b/lambda/MAS.Tests/Infrastructure/TestBase.cs @@ -1,34 +1,46 @@ using MAS.Configuration; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; +using System; using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace MAS.Tests.Infrastructure { - public class TestBase + public abstract class TestBase : IDisposable { - protected readonly TestServer _server; - protected readonly HttpClient _client; - protected readonly IConfigurationRoot _config; + protected readonly MASWebApplicationFactory _factory; public TestBase() { - _config = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .AddUserSecrets("adafe3d8-65fb-49fd-885e-03341a36dc88") - .Build(); + _factory = new MASWebApplicationFactory(); + } - var builder = new WebHostBuilder() - .UseContentRoot("../../../../MAS") - .ConfigureServices(services => + protected WebApplicationFactory WithImplementation(TService implementation) + { + return _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { - AppSettings.Configure(services, _config); - }) - .UseEnvironment("Production") - .UseStartup(typeof(Startup)); - _server = new TestServer(builder); - _client = _server.CreateClient(); + var serviceProvider = services.BuildServiceProvider(); + + var descriptor = + new ServiceDescriptor( + typeof(TService), implementation); + + services.Replace(descriptor); + }); + }); + } + + public virtual void Dispose() + { + _factory.Dispose(); } } } diff --git a/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApproved.approved.txt b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApproved.approved.txt new file mode 100644 index 00000000..aed03a43 --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApproved.approved.txt @@ -0,0 +1,53 @@ + +

DailyEmail

+ + +
+ Safety alerts + +
+ Wonder Drug +
+ ACP Journal Club +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ testing +
+ SPS Comment +
+
+ MY MEDICINE +
+ ADHD Awareness Month +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ this is my medicine +
+ SPS Comment +
+
+ Hello world +
+ Substance Misuse Management in General Practice +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ test +
+ SPS Comment +
+
+ A Placebo +
+ Journal of Surgical Oncology +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ hello +
+ SPS Comment +
+
+ diff --git a/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItem.approved.txt b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItem.approved.txt new file mode 100644 index 00000000..634797a8 --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItem.approved.txt @@ -0,0 +1,20 @@ + +

DailyEmail

+ + +
+ Safety alerts + +
+ Wonder Drug +
+ ACP Journal Club +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ testing +
+ SPS Comment +
+
+ diff --git a/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItemWithSpecificDate.approved.txt b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItemWithSpecificDate.approved.txt new file mode 100644 index 00000000..634797a8 --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.EmailBodyHtmlMatchesApprovedForSingleItemWithSpecificDate.approved.txt @@ -0,0 +1,20 @@ + +

DailyEmail

+ + +
+ Safety alerts + +
+ Wonder Drug +
+ ACP Journal Club +
+ Anaesthesia and pain | Complementary and alternative therapies +
+ testing +
+ SPS Comment +
+
+ diff --git a/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.cs b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.cs new file mode 100644 index 00000000..363780a9 --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/DailyEmailTests.cs @@ -0,0 +1,121 @@ +using MailChimp.Net.Core; +using MailChimp.Net.Interfaces; +using MailChimp.Net.Models; +using MAS.Configuration; +using MAS.Models; +using MAS.Services; +using MAS.Tests.Infrastructure; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Moq; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Mvc; +using Xunit; +using Source = MAS.Models.Source; + +namespace MAS.Tests.IntegrationTests +{ + public class TestMailService : IMailService + { + public Task CreateAndSendDailyAsync(string subject, string previewText, string body) + { + throw new NotImplementedException(); + } + } + + public class DailyEmailTests : TestBase + { + [Fact] + public async void EmailBodyHtmlMatchesApproved() + { + // Arrange + var fakeMailService = new Mock(); + + string bodyHtml = string.Empty; + fakeMailService.Setup(s => s.CreateAndSendDailyAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((subject, previewText, body) => bodyHtml = body) + .ReturnsAsync("1234"); + + var client = WithImplementation(fakeMailService.Object).CreateClient(); + + // Act + var response = await client.PutAsync("/api/mail/daily", null); + + // Assert + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + + var responseText = await response.Content.ReadAsStringAsync(); + responseText.ShouldBe("1234"); + + bodyHtml.ShouldMatchApproved(); + } + + [Fact] + public async void EmailBodyHtmlMatchesApprovedForSingleItem() + { + // Arrange + var fakeMailService = new Mock(); + + string bodyHtml = string.Empty; + fakeMailService.Setup(s => s.CreateAndSendDailyAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((subject, previewText, body) => bodyHtml = body) + .ReturnsAsync("1234"); + + var client = WithImplementation(fakeMailService.Object).CreateClient(); + + AppSettings.CMSConfig.DailyItemsPath = "/daily-items-single.json"; + + // Act + var response = await client.PutAsync("/api/mail/daily", null); + + // Assert + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + + var responseText = await response.Content.ReadAsStringAsync(); + responseText.ShouldBe("1234"); + + bodyHtml.ShouldMatchApproved(); + } + + [Fact] + public async void EmailBodyHtmlMatchesApprovedForSingleItemWithSpecificDate() + { + // Arrange + var fakeMailService = new Mock(); + + string bodyHtml = string.Empty; + fakeMailService.Setup(s => s.CreateAndSendDailyAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((subject, previewText, body) => bodyHtml = body) + .ReturnsAsync("1234"); + + var client = WithImplementation(fakeMailService.Object).CreateClient(); + + AppSettings.CMSConfig.DailyItemsPath = "/daily-items-{0}.json"; + + // Act + var response = await client.PutAsync("/api/mail/daily?date=01-01-2020", null); + + // Assert + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + + var responseText = await response.Content.ReadAsStringAsync(); + responseText.ShouldBe("1234"); + + bodyHtml.ShouldMatchApproved(); + } + + } +} diff --git a/lambda/MAS.Tests/IntegrationTests/MailControllerTests.cs b/lambda/MAS.Tests/IntegrationTests/MailControllerTests.cs new file mode 100644 index 00000000..9616a263 --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/MailControllerTests.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Xunit; +using Shouldly; +using MAS.Tests.Infrastructure; +using System.Text; +using System; +using MAS.Configuration; +using System.Net.Http.Headers; +using System.Net.Http; +using MailChimp.Net.Models; +using Newtonsoft.Json; + +namespace MAS.Tests.IntegrationTests.Mail +{ + //public class MailControllerTests : TestBase + //{ + // [Fact] + // public async Task PutRequestCreatesAndSendsCampaign() + // { + // //Arrange + // const string mailChimpCampaignsURI = "https://us5.api.mailchimp.com/3.0/campaigns/"; + + // var authValue = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"anystring:{AppSettings.MailConfig.ApiKey}"))); + // var client = new HttpClient() + // { + // DefaultRequestHeaders = { Authorization = authValue } + // }; + + // //Act + // var response = await _client.PutAsync("/api/mail/daily", null); + + // //Get campaign to check if it saved + // var campaignId = await response.Content.ReadAsStringAsync(); + // var campaign = await client.GetAsync(mailChimpCampaignsURI + campaignId); + // var campaignJson = await campaign.Content.ReadAsStringAsync(); + // var campaignResult = JsonConvert.DeserializeObject(campaignJson); + + // // Assert + // response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + // campaignResult.Status.ShouldNotBeNull(); + // campaignResult.Status.ShouldNotBe("Draft"); + // } + //} +} diff --git a/lambda/MAS.Tests/IntergrationTests/ContentControllerTests.PutCMSItemSavesItemIntoS3.approved.txt b/lambda/MAS.Tests/IntegrationTests/StaticContentTests.PutCMSItemSavesItemIntoS3.approved.txt similarity index 96% rename from lambda/MAS.Tests/IntergrationTests/ContentControllerTests.PutCMSItemSavesItemIntoS3.approved.txt rename to lambda/MAS.Tests/IntegrationTests/StaticContentTests.PutCMSItemSavesItemIntoS3.approved.txt index 4f56ef60..d1a2aef8 100644 --- a/lambda/MAS.Tests/IntergrationTests/ContentControllerTests.PutCMSItemSavesItemIntoS3.approved.txt +++ b/lambda/MAS.Tests/IntegrationTests/StaticContentTests.PutCMSItemSavesItemIntoS3.approved.txt @@ -18,10 +18,10 @@ - + - + diff --git a/lambda/MAS.Tests/IntegrationTests/StaticContentTests.cs b/lambda/MAS.Tests/IntegrationTests/StaticContentTests.cs new file mode 100644 index 00000000..39b8957a --- /dev/null +++ b/lambda/MAS.Tests/IntegrationTests/StaticContentTests.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Xunit; +using MAS.Tests.Infrastructure; +using Shouldly; +using Amazon.S3; +using Amazon; +using MAS.Configuration; +using System.IO; +using MAS.Models; +using Amazon.S3.Model; +using Newtonsoft.Json; +using System.Text; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Moq; +using System.Threading; + +namespace MAS.Tests.IntegrationTests +{ + public class StaticContentTests : TestBase + { + private Item item = new Item() + { + Id = "1234", + Slug = "effect-of-vit-d", + URL = "www.website.com", + Title = "Effect of Vitamin D and Omega-3 Fatty Acid Supplementation on Kidney Function in Patients With Type 2 Diabetes: A Randomized Clinical Trial", + ShortSummary = "RCT (n=1,312) found that among adults with type 2 diabetes, supplementation with vitamin D3 or omega-3 fatty acids, compared with placebo, resulted in no significant difference in change in eGFR at 5 years.", + Source = new Source() + { + Id = "789", + Title = "Journal of the American Medical Association" + }, + EvidenceType = new EvidenceType + { + Key = "mas_evidence_types:Safety%20alerts", + Title = "Safety alerts" + }, + Comment = "A related editorial discusses this research and details previous epidemiological studies that suggest improved outcomes with vitamin D supplementation in various clinical scenarios. It states that contrasting the results of this study and its predecessor vitamin D trials with the impressive body of epidemiological research that implicated vitamin D deficiency in various adverse health outcomes offers a stark lesson on the chasm between association and causation. Editorial authors highlight that it now seems safe to conclude that many prior epidemiological associations between vitamin D deficiency and adverse health outcomes were driven by unmeasured residual confounding or reverse causality.", + ResourceLinks = "

Vitamin D and Health Outcomes - Then Came the Randomized Clinical Trials

\r\n

Link 2

" + + }; + + [Fact] + public async Task PutCMSItemSavesItemIntoS3() + { + // Arrange + var fakeS3Service = new Mock(); + + PutObjectRequest putObjectRequest = null; + fakeS3Service.Setup(s => s.PutObjectAsync(It.IsAny(), default(CancellationToken))) + .Callback((pOR, cT) => putObjectRequest = pOR) + .ReturnsAsync(new PutObjectResponse { HttpStatusCode = System.Net.HttpStatusCode.OK }); + + var client = WithImplementation(fakeS3Service.Object).CreateClient(); + var content = new StringContent(JsonConvert.SerializeObject(item), Encoding.UTF8, "application/json"); + + // Act + var response = await client.PutAsync("/api/content/", content); + + // Assert + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + putObjectRequest.Key.ShouldBe("effect-of-vit-d.html"); + putObjectRequest.ContentBody.ShouldMatchApproved(); + } + + } +} diff --git a/lambda/MAS.Tests/IntergrationTests/ContentControllerTests.cs b/lambda/MAS.Tests/IntergrationTests/ContentControllerTests.cs deleted file mode 100644 index 2dc7100b..00000000 --- a/lambda/MAS.Tests/IntergrationTests/ContentControllerTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Threading.Tasks; -using Xunit; -using MAS.Tests.Infrastructure; -using Shouldly; -using Amazon.S3; -using Amazon; -using MAS.Configuration; -using System.IO; -using MAS.Models; -using Amazon.S3.Model; -using Newtonsoft.Json; -using System.Text; -using System.Net.Http; - -namespace MAS.Tests.IntergrationTests.Content -{ - public class ContentControllerTests : TestBase - { - [Fact] - public async Task PutCMSItemSavesItemIntoS3() - { - //Arrange - AmazonS3Config config = new AmazonS3Config() - { - RegionEndpoint = RegionEndpoint.EUWest1, - ServiceURL = AppSettings.AWSConfig.ServiceURL, - ForcePathStyle = true - }; - AmazonS3Client s3Client = new AmazonS3Client(AppSettings.AWSConfig.AccessKey, AppSettings.AWSConfig.SecretKey, config); - - Item item = new Item() - { - Id = "1234", - Slug = "Effect-of-vit-d", - URL = "www.website.com", - Title = "Effect of Vitamin D and Omega-3 Fatty Acid Supplementation on Kidney Function in Patients With Type 2 Diabetes: A Randomized Clinical Trial", - ShortSummary = "RCT (n=1,312) found that among adults with type 2 diabetes, supplementation with vitamin D3 or omega-3 fatty acids, compared with placebo, resulted in no significant difference in change in eGFR at 5 years.", - Source = new Source() - { - Id = "789", - Title = "Journal of the American Medical Association" - }, - EvidenceType = new EvidenceType - { - Key = "mas_evidence_types:Safety%20alerts", - Title = "Safety alerts" - }, - Comment = "A related editorial discusses this research and details previous epidemiological studies that suggest improved outcomes with vitamin D supplementation in various clinical scenarios. It states that contrasting the results of this study and its predecessor vitamin D trials with the impressive body of epidemiological research that implicated vitamin D deficiency in various adverse health outcomes offers a stark lesson on the chasm between association and causation. Editorial authors highlight that it now seems safe to conclude that many prior epidemiological associations between vitamin D deficiency and adverse health outcomes were driven by unmeasured residual confounding or reverse causality.", - ResourceLinks = "

Vitamin D and Health Outcomes - Then Came the Randomized Clinical Trials

\r\n

Link 2

" - - }; - - var content = new StringContent(JsonConvert.SerializeObject(item), Encoding.UTF8, "application/json"); - - //Act - var response = await _client.PutAsync("/api/content/", content); - - // Assert - response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); - - using (var bucketItem = await s3Client.GetObjectAsync(AppSettings.AWSConfig.BucketName, "Effect-of-vit-d.html")) - { - using (StreamReader reader = new StreamReader(bucketItem.ResponseStream)) - { - string contents = reader.ReadToEnd(); - contents.ShouldMatchApproved(); - } - } - } - - } -} diff --git a/lambda/MAS.Tests/IntergrationTests/MailControllerTests.cs b/lambda/MAS.Tests/IntergrationTests/MailControllerTests.cs deleted file mode 100644 index 096ce967..00000000 --- a/lambda/MAS.Tests/IntergrationTests/MailControllerTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using Xunit; -using Shouldly; -using MAS.Tests.Infrastructure; -using System.Text; -using System; -using MAS.Configuration; -using System.Net.Http.Headers; -using System.Net.Http; -using MailChimp.Net.Models; -using Newtonsoft.Json; - -namespace MAS.Tests.IntergrationTests.Mail -{ - public class MailControllerTests : TestBase - { - [Fact] - public async Task PutRequestCreatesAndSendsCampaign() - { - //Arrange - const string mailChimpCampaignsURI = "https://us5.api.mailchimp.com/3.0/campaigns/"; - AppSettings.CMSConfig = TestAppSettings.GetMultipleItemsFeed(); - - var authValue = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"anystring:{AppSettings.MailConfig.ApiKey}"))); - var client = new HttpClient() - { - DefaultRequestHeaders = { Authorization = authValue } - }; - - //Act - var response = await _client.PutAsync("/api/mail/daily", null); - - //Get campaign to check if it saved - var campaignId = await response.Content.ReadAsStringAsync(); - var campaign = await client.GetAsync(mailChimpCampaignsURI + campaignId); - var campaignJson = await campaign.Content.ReadAsStringAsync(); - var campaignResult = JsonConvert.DeserializeObject(campaignJson); - - // Assert - response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); - campaignResult.Status.ShouldNotBeNull(); - campaignResult.Status.ShouldNotBe("Draft"); - } - } -} diff --git a/lambda/MAS.Tests/MAS.Tests.csproj b/lambda/MAS.Tests/MAS.Tests.csproj index c15eb412..d4bafc9b 100644 --- a/lambda/MAS.Tests/MAS.Tests.csproj +++ b/lambda/MAS.Tests/MAS.Tests.csproj @@ -10,25 +10,27 @@ - - - + + + + + - - PreserveNewest + + PreserveNewest - - PreserveNewest + + PreserveNewest + + + PreserveNewest + + PreserveNewest - - - - Always - @@ -36,7 +38,8 @@ - + + @@ -48,4 +51,9 @@ + + + PreserveNewest + + \ No newline at end of file diff --git a/lambda/MAS.Tests/UnitTests/Controllers/MailControllerTests.cs b/lambda/MAS.Tests/UnitTests/Controllers/MailControllerTests.cs new file mode 100644 index 00000000..e339839d --- /dev/null +++ b/lambda/MAS.Tests/UnitTests/Controllers/MailControllerTests.cs @@ -0,0 +1,63 @@ +using MAS.Controllers; +using MAS.Models; +using MAS.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace MAS.Tests.UnitTests.Controllers +{ + public class MailControllerTests + { + [Fact] + public async void GetDailyItemsForToday() + { + + var mockContentService = new Mock(); + var mailService = new MailController(Mock.Of(), mockContentService.Object, Mock.Of(), Mock.Of>()); + + //Act + await mailService.PutMailAsync(); + + //Assert + mockContentService.Verify(mock => mock.GetDailyItemsAsync(null), Times.Once()); + } + + [Fact] + public async void GetDailyItemsForSpecificDate() + { + DateTime date = new DateTime(2020, 1, 15); + var mockContentService = new Mock(); + var mailService = new MailController(Mock.Of(), mockContentService.Object, Mock.Of(), Mock.Of>()); + + //Act + await mailService.PutMailAsync(date); + + //Assert + mockContentService.Verify(mock => mock.GetDailyItemsAsync(date), Times.Once()); + } + + [Fact] + public async void RendersDailyViewWithContentItems() + { + var items = new List() { }.AsEnumerable(); + var mockContentService = new Mock(); + mockContentService.Setup(x => x.GetDailyItemsAsync(null)).ReturnsAsync(items); + + var mockViewRenderer = new Mock(); + + var mailController = new MailController(Mock.Of(), mockContentService.Object, mockViewRenderer.Object, Mock.Of>()); + + //Act + await mailController.PutMailAsync(); + + //Assert + mockViewRenderer.Verify(mock => mock.RenderViewAsync(mailController, "~/Views/DailyEmail.cshtml", items, false), Times.Once()); + } + } +} diff --git a/lambda/MAS.Tests/UnitTests/ContentServiceTests.cs b/lambda/MAS.Tests/UnitTests/Services/ContentServiceTests.cs similarity index 81% rename from lambda/MAS.Tests/UnitTests/ContentServiceTests.cs rename to lambda/MAS.Tests/UnitTests/Services/ContentServiceTests.cs index 4845c4ae..c60ae0aa 100644 --- a/lambda/MAS.Tests/UnitTests/ContentServiceTests.cs +++ b/lambda/MAS.Tests/UnitTests/Services/ContentServiceTests.cs @@ -11,7 +11,7 @@ namespace MAS.Tests.UnitTests { - public class ContentServiceTests : TestBase + public class ContentServiceTests { [Fact] public async Task ReadMultipleItems() @@ -19,11 +19,11 @@ public async Task ReadMultipleItems() //Arrange var mockLogger = new Mock>(); - AppSettings.CMSConfig = TestAppSettings.GetMultipleItemsFeed(); + AppSettings.CMSConfig = TestAppSettings.CMS.Default; var contentService = new ContentService(mockLogger.Object); //Act - var result = await contentService.GetItemsAsync(); + var result = await contentService.GetAllItemsAsync(); //Assert result.Count().ShouldBe(4); @@ -39,11 +39,11 @@ public async Task InvalidURIThrowsError() //Arrange var mockLogger = new Mock>(); - AppSettings.CMSConfig = TestAppSettings.GetInvalidURI(); + AppSettings.CMSConfig = TestAppSettings.CMS.InvalidURI; var contentService = new ContentService(mockLogger.Object); //Act + Assert - await Should.ThrowAsync(() => contentService.GetItemsAsync()); + await Should.ThrowAsync(() => contentService.GetAllItemsAsync()); } } } diff --git a/lambda/MAS.Tests/UnitTests/MailServiceTests.cs b/lambda/MAS.Tests/UnitTests/Services/MailServiceTests.cs similarity index 88% rename from lambda/MAS.Tests/UnitTests/MailServiceTests.cs rename to lambda/MAS.Tests/UnitTests/Services/MailServiceTests.cs index d2674883..98c6bde0 100644 --- a/lambda/MAS.Tests/UnitTests/MailServiceTests.cs +++ b/lambda/MAS.Tests/UnitTests/Services/MailServiceTests.cs @@ -13,7 +13,7 @@ namespace MAS.Tests.UnitTests { - public class MailServiceTests : TestBase + public class MailServiceTests { [Fact] public void CreateCampaignAndSendToMailChimp() @@ -28,8 +28,10 @@ public void CreateCampaignAndSendToMailChimp() var mailService = new MailService(mockMailChimpManager.Object, mockLogger.Object); + AppSettings.MailConfig = Mock.Of(); + //Act - var response = mailService.CreateAndSendCampaignAsync("Test Subject", "Preview Text", "Body Text"); + var response = mailService.CreateAndSendDailyAsync("Test Subject", "Preview Text", "Body Text"); //Assert response.Exception.ShouldBe(null); @@ -51,7 +53,7 @@ public void ErrorInSendingCampaignShouldThrowError() var mailService = new MailService(mockMailChimpManager.Object, mockLogger.Object); //Act + Assert - Should.Throw(() => mailService.CreateAndSendCampaignAsync("Test Subject", "Preview Text", "Body Text")); + Should.Throw(() => mailService.CreateAndSendDailyAsync("Test Subject", "Preview Text", "Body Text")); } } } diff --git a/lambda/MAS.Tests/UnitTests/ViewRendererTests.cs b/lambda/MAS.Tests/UnitTests/Services/ViewRendererTests.cs similarity index 100% rename from lambda/MAS.Tests/UnitTests/ViewRendererTests.cs rename to lambda/MAS.Tests/UnitTests/Services/ViewRendererTests.cs diff --git a/lambda/MAS.Tests/appsettings.Development.json b/lambda/MAS.Tests/appsettings.Development.json deleted file mode 100644 index 38fb825a..00000000 --- a/lambda/MAS.Tests/appsettings.Development.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AWS": { - "Region": "eu-west-1" - } -} \ No newline at end of file diff --git a/lambda/MAS.Tests/appsettings.json b/lambda/MAS.Tests/appsettings.json deleted file mode 100644 index d9f81bc1..00000000 --- a/lambda/MAS.Tests/appsettings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "Logging": { - "RabbitMQHost": "", - "RabbitMQPort": "", - "IncludeScopes": false, - "LogFilePath": "Serilog-{Date}.json", - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - }, - "UseRabbit": true, - "UseFile": true - }, - "AppSettings": { - "Environment": { - "Name": "local" - } - }, - "AWS": { - "AccessKey": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio", - "SecretKey": "", - "ServiceURL": "", - "BucketName": "" - }, - "CMS": { - "URI": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio" - }, - "MailChimp": { - "ApiKey": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio", - "ListId": "", - "DailyTemplateId": "", - "CampaignFolderId": "" - } -} \ No newline at end of file diff --git a/lambda/MAS.Tests/appsettings.test.json b/lambda/MAS.Tests/appsettings.test.json new file mode 100644 index 00000000..8f0b8bf5 --- /dev/null +++ b/lambda/MAS.Tests/appsettings.test.json @@ -0,0 +1,15 @@ +{ + "AppSettings": { + "Environment": { + "Name": "tests" + } + }, + "AWS": { + "StaticUrl": "http://mas-integration-tests/" + }, + "CMS": { + "BaseUrl": "We override this in test setup to point to the Feeds folder on the filesystem", + "AllItemsPath": "/all-items.json", + "DailyItemsPath": "/daily-items.json" + } +} diff --git a/lambda/MAS/Configuration/CMSConfig.cs b/lambda/MAS/Configuration/CMSConfig.cs index a7679e92..d007ae1e 100644 --- a/lambda/MAS/Configuration/CMSConfig.cs +++ b/lambda/MAS/Configuration/CMSConfig.cs @@ -2,6 +2,8 @@ { public class CMSConfig { - public string URI { get; set; } + public string BaseUrl { get; set; } + public string AllItemsPath { get; set; } + public string DailyItemsPath { get; set; } } } diff --git a/lambda/MAS/Controllers/MailController.cs b/lambda/MAS/Controllers/MailController.cs index d9ea13a4..e81a8635 100644 --- a/lambda/MAS/Controllers/MailController.cs +++ b/lambda/MAS/Controllers/MailController.cs @@ -1,9 +1,9 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MAS.Services; +using MAS.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; namespace MAS.Controllers { @@ -12,30 +12,35 @@ public class MailController : ControllerBase { private readonly IMailService _mailService; private readonly IContentService _contentService; + private readonly IViewRenderer _viewRenderer; + private readonly ILogger _logger; - public MailController(IMailService mailService, IContentService contentService) + public MailController(IMailService mailService, IContentService contentService, IViewRenderer viewRenderer, ILogger logger) { _mailService = mailService; _contentService = contentService; + _viewRenderer = viewRenderer; + _logger = logger; } - //PUT api/mail/daily + //PUT api/mail/daily?date=01-01-2020 [HttpPut("daily")] - public async Task PutMailAsync() + public async Task PutMailAsync(DateTime? date = null) { - var items = await _contentService.GetItemsAsync(); + var items = await _contentService.GetDailyItemsAsync(date); - var body = _mailService.CreateEmailBody(items); + var body = await _viewRenderer.RenderViewAsync(this, "~/Views/DailyEmail.cshtml", items.ToList()); var subject = "MAS Email"; var previewText = "This MAS email was created " + DateTime.Now.ToShortDateString(); try { - var campaignId = await _mailService.CreateAndSendCampaignAsync(subject, previewText, body); + var campaignId = await _mailService.CreateAndSendDailyAsync(subject, previewText, body); return Content(campaignId); } catch (Exception e) { + _logger.LogError(e, $"Failed to send daily email - exception: {e.Message}"); return StatusCode(500, new ProblemDetails { Status = 500, Title = e.Message, Detail = e.InnerException?.Message }); } } diff --git a/lambda/MAS/Models/Item.cs b/lambda/MAS/Models/Item.cs index 4dfc11a6..69ecbe69 100644 --- a/lambda/MAS/Models/Item.cs +++ b/lambda/MAS/Models/Item.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace MAS.Models @@ -13,6 +14,8 @@ public class Item public string Title { get; set; } [Required, JsonRequired] public Source Source { get; set; } + [JsonProperty("specialities")] + public List Specialities { get; set; } [Required, JsonRequired] public EvidenceType EvidenceType { get; set; } public string ShortSummary { get; set; } diff --git a/lambda/MAS/Models/Speciality.cs b/lambda/MAS/Models/Speciality.cs new file mode 100644 index 00000000..800ea34e --- /dev/null +++ b/lambda/MAS/Models/Speciality.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MAS.Models +{ + public class Speciality + { + public string Title { get; set; } + + public string Key { get; set; } + } +} \ No newline at end of file diff --git a/lambda/MAS/Services/ContentService.cs b/lambda/MAS/Services/ContentService.cs index 028f802c..809343de 100644 --- a/lambda/MAS/Services/ContentService.cs +++ b/lambda/MAS/Services/ContentService.cs @@ -11,7 +11,8 @@ namespace MAS.Services { public interface IContentService { - Task> GetItemsAsync(); + Task> GetAllItemsAsync(); + Task> GetDailyItemsAsync(DateTime? date = null); } public class ContentService : IContentService @@ -23,15 +24,15 @@ public ContentService(ILogger logger) _logger = logger; } - public async Task> GetItemsAsync() + public async Task> GetAllItemsAsync() { using (WebClient client = new WebClient()) { try { - var jsonStr = await client.DownloadStringTaskAsync(new Uri(AppSettings.CMSConfig.URI)); - var json = JsonConvert.DeserializeObject(jsonStr); - return json; + var jsonStr = await client.DownloadStringTaskAsync(new Uri(AppSettings.CMSConfig.BaseUrl + AppSettings.CMSConfig.AllItemsPath)); + var item = JsonConvert.DeserializeObject(jsonStr); + return item; } catch(Exception e) { @@ -40,5 +41,27 @@ public async Task> GetItemsAsync() } } } + + public async Task> GetDailyItemsAsync(DateTime? date = null) + { + date = date ?? DateTime.Today; + + using (WebClient client = new WebClient()) + { + var path = string.Format(AppSettings.CMSConfig.DailyItemsPath, date.Value.ToString("yyyy-MM-dd")); + try + { + var jsonStr = await client.DownloadStringTaskAsync(new Uri(AppSettings.CMSConfig.BaseUrl + path)); + var item = JsonConvert.DeserializeObject(jsonStr); + return item; + } + catch (Exception e) + { + _logger.LogError($"Failed to get items from CMS - exception: {e.Message}"); + throw new Exception($"Failed to get items from CMS - exception: {e.Message}"); + } + } + } + } } diff --git a/lambda/MAS/Services/MailService.cs b/lambda/MAS/Services/MailService.cs index aa7ab767..cd05b3a0 100644 --- a/lambda/MAS/Services/MailService.cs +++ b/lambda/MAS/Services/MailService.cs @@ -3,18 +3,15 @@ using MailChimp.Net.Models; using MAS.Configuration; using Microsoft.Extensions.Logging; -using MAS.Models; using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace MAS.Services { public interface IMailService { - Task CreateAndSendCampaignAsync(string subject, string previewText, string body); - string CreateEmailBody(IEnumerable item); + Task CreateAndSendDailyAsync(string subject, string previewText, string body); } public class MailService: IMailService @@ -28,7 +25,7 @@ public MailService(IMailChimpManager mailChimpManager, ILogger logg _logger = logger; } - public async Task CreateAndSendCampaignAsync(string subject, string previewText, string body) + public async Task CreateAndSendDailyAsync(string subject, string previewText, string body) { try { @@ -72,22 +69,5 @@ public async Task CreateAndSendCampaignAsync(string subject, string prev } } - - public string CreateEmailBody(IEnumerable items) - { - var body = new StringBuilder(); - - foreach (var item in items) - { - body.Append(item.Source.Title); - body.Append("
"); - body.Append(item.Title); - body.Append("
"); - body.Append(item.ShortSummary); - body.Append("


"); - } - - return body.ToString(); - } } } diff --git a/lambda/MAS/Views/DailyEmail.cshtml b/lambda/MAS/Views/DailyEmail.cshtml new file mode 100644 index 00000000..f9e2fbde --- /dev/null +++ b/lambda/MAS/Views/DailyEmail.cshtml @@ -0,0 +1,156 @@ +@using MAS.Models; +@model List +@{ + ViewData["Title"] = "MAS Daily"; + + var groupedItems = Model.GroupBy(x => x.EvidenceType.Title).ToList(); + var specialitiesAsString = String.Join(',', Model.SelectMany(x => x.Specialities).Select(x => x.Title).ToList()); +} + + + + + + + + +
+ + + + + + + +
+

+ *|INTERESTED:Daily specialities of interest:@specialitiesAsString|* + Articles that match your chosen specialities today + *|ELSE:|* + No articles match your chosen specialities today + *|END:INTERESTED|* + - Edit your subscription +

+
+
+ + +@foreach (var group in groupedItems) +{ + var evidenceType = group.Key; + + @:*|INTERESTED:Daily specialities of interest:@specialitiesAsString|* + + + + + + +
+

@evidenceType

+
+ + @foreach (var item in group) + { + @:*|INTERESTED:Daily specialities of interest:@specialitiesAsString|* + + + + + + +
+ + + + + + + +
+

@item.Title

+

@item.ShortSummary

+

Read SPS commentary

+ +
+
+ @:*|END:INTERESTED|* + } + @:*|END:INTERESTED|* +} + +*|INTERESTED:Send me everything from Medicines Awareness Daily:Send me everything|* + + + + + + +
+ + + + + + + +
+

+ All articles from Medicines awareness services: Daily edition +

+
+
+ +@foreach (var group in groupedItems) +{ + var evidenceType = group.Key; + + + + + + + +
+

@evidenceType

+
+ + @foreach (var item in group) + { + + + + + + +
+ + + + + + + +
+

@item.Title

+

@item.ShortSummary

+

Read SPS commentary

+ +
+
+ } +} +*|END:INTERESTED|* + diff --git a/lambda/MAS/Views/Shared/_EmailLayout.cshtml b/lambda/MAS/Views/Shared/_EmailLayout.cshtml new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/lambda/MAS/Views/Shared/_EmailLayout.cshtml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lambda/MAS/appsettings.json b/lambda/MAS/appsettings.json index 09fba1e0..10618c25 100644 --- a/lambda/MAS/appsettings.json +++ b/lambda/MAS/appsettings.json @@ -26,12 +26,14 @@ "StaticURL": "" }, "CMS": { - "URI": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio" + "BaseUrl": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio", + "AllItemsPath": "items", + "DailyItemsPath": "items/daily/{0}" }, "MailChimp": { "ApiKey": "don't add this or anything else that needs securing. see here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio", "ListId": "", - "DailyTemplateId": "", + "DailyTemplateId": 0, "CampaignFolderId": "" } } \ No newline at end of file diff --git a/mailchimp-templates/daily.html b/mailchimp-templates/daily.html new file mode 100644 index 00000000..38e603e4 --- /dev/null +++ b/mailchimp-templates/daily.html @@ -0,0 +1,2528 @@ + + + + + + Medicines awareness service - daily edition + + + + + + + + + +
+
+ + + + + + +
 
+ + + + + + +
+ + + + + + + + + +
+ + + + +
+
+
+
+
+ + + + +
+
+
+
+
+ + + + + + + +
+ + + + + +
+

*|DATE:jS F|*

+

Medicines awareness service - daily

+

The very latest current awareness and evidence-based medicines information

+

Content produced by the Specialist Pharmacy Service - Medicines Information

+
+
+ +
+
+ + + + + + + +
+ + + + + +
+

Specialist Pharmacy Service and NICE bring you the latest current awareness and evidence-based medicines information, direct to your inbox.
Terms and conditions | Contact us +

+
+
+ + + + + + + +
+
+
+ + \ No newline at end of file