refactor: Cleanup (#21)

* refactor(routes): remove unused sitemap route

* style(comments): enforce consistent comment style

* style(exports): enforce consistent export style

* chore: Add jsDoc for contentful service

* chore: Add jsDoc for contentful service \n \n closes #23

* refactor(app): move query parameter comment to right position and mention comment route middleware

* test(npm): add temporary test script

* refactor(middlewares): split up bootstrap middleware - fixes #23759

* refactor(cookies): use constances to give context the maxAge cookie setting

* refactor(variables): use more descriptive names for variables

* space became spaceId when it was just the id not the full space instance
* all (access) token variable name variants became [api-type]Token
* all clients are now called deliveryClient or previewClient
* cpa and cda only remain when they are used as actual API id
* env variables names adjusted

* perf(helpers): only run marked when content is not empty

* refactor(comments): fix typos

* refactor(comments): add hint why error is logged to console in settings

* chore: Add comments to routes and services

* refactor(requires): order and group requires

* fix(settings): add validation for wrong preview token

* chore: Add comments to routes

* chore: fix typo

* chore: console.error -> throw

* chore: move cookie name to a constant

* chore: Fix app.js comment

* typo: removing t

* chore: typos

* chore: removed unnecessary comment line sign

* chore: newline for readabillity

* chore: remove dangling `t`

* chore: remove `t`, add `l`

* chore: typos

* Fleshed out title

* build(npm): remove unused dependencies

* build(npm): upgrade to latest stable contentful sdk

* chore: Addressing David feedbak

* fix(credentials): update to match the new space

* chore: Addressing code review comments

* chore: Addressing code review comments

* chore: res -> response, req-> request

* chore: include exactly what we need

* chore: Address JPs comments

* chore: Address JPs comments

* chore: Address Fredericks comments

* chore: Address Fredericks comments

* fixup! chore: Address Fredericks comments

* fixup! fixup! chore: Address Fredericks comments

* fixup! fixup! fixup! chore: Address Fredericks comments

* fixup! fixup! fixup! fixup! chore: Address Fredericks comments

* fixup! fixup! fixup! fixup! fixup! chore: Address Fredericks comments

* fixup! fixup! fixup! fixup! fixup! fixup! chore: Address Fredericks comments
This commit is contained in:
Benedikt Rötsch
2017-11-03 10:58:42 +01:00
parent 730ce24cc9
commit 7ad2c1fa4f
25 changed files with 507 additions and 340 deletions

View File

@@ -4,5 +4,15 @@ module.exports = {
'plugins': [ 'plugins': [
'standard', 'standard',
'promise' 'promise'
] ],
'env': {
'node': true
},
'rules': {
"capitalized-comments": [
"error",
"always"
],
"spaced-comment": ["error", "always"]
}
} }

View File

@@ -1,5 +1,5 @@
# the-example-app.js # The Node.js example app
The Contentful example app, written in JS The Contentful example app, written in node.js.
## Requirements ## Requirements
@@ -30,6 +30,6 @@ Open http://localhost:3000/?enable_editorial_features in your browser.
The following deep links are supported: The following deep links are supported:
* `?enable_editorial_features` - Shows `Edit in web app` button on every content type plus `Draft` and `Pending Changes` status pills * `?enable_editorial_features` - Shows `Edit in web app` button on every content type plus `Draft` and `Pending Changes` status pills
* `?space_id=xxx&delivery_access_token=xxx&preview_access_token=xxx` - Configure the connected space * `?space_id=xxx&delivery_token=xxx&preview_token=xxx` - Configure the connected space

123
app.js
View File

@@ -1,19 +1,25 @@
require('dotenv').config({ path: 'variables.env' })
const express = require('express')
const querystring = require('querystring')
const path = require('path') const path = require('path')
const helpers = require('./helpers')
const logger = require('morgan')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const express = require('express')
const logger = require('morgan')
const querystring = require('querystring')
const routes = require('./routes/index') // Load environment variables using dotenv
require('dotenv').config({ path: 'variables.env' })
const { initClient, getSpace } = require('./services/contentful') const helpers = require('./helpers')
const breadcrumb = require('./lib/breadcrumb') const breadcrumb = require('./lib/breadcrumb')
const routes = require('./routes/index')
const { initClients, getSpace } = require('./services/contentful')
const { updateCookie } = require('./lib/cookies')
const app = express() const app = express()
// view engine setup const SETTINGS_NAME = 'theExampleAppSettings'
// View engine setup
app.set('views', path.join(__dirname, 'views')) app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug') app.set('view engine', 'pug')
@@ -24,43 +30,46 @@ app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, 'public')))
app.use(breadcrumb()) app.use(breadcrumb())
// Pass our application state and custom helpers to all our templates // Set our application state based on environment variables or query parameters
app.use(async function (req, res, next) { app.use(async function (request, response, next) {
// Allow setting of API credentials via query parameters // Set default settings based on environment variables
let settings = { let settings = {
space: process.env.CF_SPACE, spaceId: process.env.CONTENTFUL_SPACE_ID,
cda: process.env.CF_ACCESS_TOKEN, deliveryToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
cpa: process.env.CF_PREVIEW_ACCESS_TOKEN, previewToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
editorialFeatures: false, editorialFeatures: false,
...req.cookies.theExampleAppSettings // Overwrite default settings using those stored in a cookie
...request.cookies.theExampleAppSettings
} }
const { space_id, preview_access_token, delivery_access_token } = req.query // Allow setting of API credentials via query parameters
if (space_id && preview_access_token && delivery_access_token) { // eslint-disable-line camelcase const { space_id, preview_token, delivery_token } = request.query
if (space_id && preview_token && delivery_token) { // eslint-disable-line camelcase
settings = { settings = {
...settings, ...settings,
space: space_id, spaceId: space_id,
cda: delivery_access_token, deliveryToken: delivery_token,
cpa: preview_access_token previewToken: preview_token
} }
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true }) updateCookie(response, SETTINGS_NAME, settings)
} }
// Allow enabling of editorial features via query parameters // Allow enabling of editorial features via query parameters
const { enable_editorial_features } = req.query const { enable_editorial_features } = request.query
if (enable_editorial_features !== undefined) { // eslint-disable-line camelcase if (enable_editorial_features !== undefined) { // eslint-disable-line camelcase
delete req.query.enable_editorial_features delete request.query.enable_editorial_features
settings = { settings.editorialFeatures = true
...settings, updateCookie(response, SETTINGS_NAME, settings)
editorialFeatures: true
}
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true })
} }
initClient(settings) initClients(settings)
res.locals.settings = settings response.locals.settings = settings
next()
})
// Manage language and API type state and make it globally available // Make data available for our views to consume
app.use(async function (request, response, next) {
// Set active api based on query parameter
const apis = [ const apis = [
{ {
id: 'cda', id: 'cda',
@@ -72,54 +81,56 @@ app.use(async function (req, res, next) {
} }
] ]
res.locals.currentApi = apis response.locals.currentApi = apis
.find((api) => api.id === (req.query.api || 'cda')) .find((api) => api.id === (request.query.api || 'cda'))
// Get enabled locales from Contentful // Get enabled locales from Contentful
const space = await getSpace() const space = await getSpace()
res.locals.locales = space.locales response.locals.locales = space.locales
const defaultLocale = res.locals.locales const defaultLocale = response.locals.locales
.find((locale) => locale.default) .find((locale) => locale.default)
if (req.query.locale) { if (request.query.locale) {
res.locals.currentLocale = space.locales response.locals.currentLocale = space.locales
.find((locale) => locale.code === req.query.locale) .find((locale) => locale.code === request.query.locale)
} }
if (!res.locals.currentLocale) { if (!response.locals.currentLocale) {
res.locals.currentLocale = defaultLocale response.locals.currentLocale = defaultLocale
} }
// Inject custom helpers // Inject custom helpers
res.locals.helpers = helpers response.locals.helpers = helpers
// Make query string available in templates // Make query string available in templates to render links properly
const qs = querystring.stringify(req.query) const qs = querystring.stringify(request.query)
res.locals.queryString = qs ? `?${qs}` : '' response.locals.queryString = qs ? `?${qs}` : ''
res.locals.query = req.query response.locals.query = request.query
res.locals.currentPath = req.path response.locals.currentPath = request.path
next() next()
}) })
// Initialize the route handling
// Check ./routes/index.js to get a list of all implemented routes
app.use('/', routes) app.use('/', routes)
// catch 404 and forward to error handler // Catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (request, response, next) {
var err = new Error('Not Found') var err = new Error('Not Found')
err.status = 404 err.status = 404
next(err) next(err)
}) })
// error handler // Error handler
app.use(function (err, req, res, next) { app.use(function (err, request, response, next) {
// set locals, only providing error in development // Set locals, only providing error in development
res.locals.error = req.app.get('env') === 'development' ? err : {} response.locals.error = request.app.get('env') === 'development' ? err : {}
// render the error page // Render the error page
res.status(err.status || 500) response.status(err.status || 500)
res.render('error') response.render('error')
}) })
module.exports = app module.exports = app

30
bin/www
View File

@@ -1,46 +1,42 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Module dependencies. * Module dependencies
*/ */
const app = require('../app') const app = require('../app')
const http = require('http') const http = require('http')
/**
* Get port from environment and store in Express.
*/
/**
* Get port from environment and store in Express
*/
const port = normalizePort(process.env.PORT || '3000') const port = normalizePort(process.env.PORT || '3000')
app.set('port', port) app.set('port', port)
/** /**
* Create HTTP server. * Create HTTP server
*/ */
const server = http.createServer(app) const server = http.createServer(app)
/** /**
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces
*/ */
server.listen(port) server.listen(port)
server.on('error', onError) server.on('error', onError)
server.on('listening', onListening) server.on('listening', onListening)
/** /**
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false
*/ */
function normalizePort (val) { function normalizePort (val) {
const port = parseInt(val, 10) const port = parseInt(val, 10)
if (isNaN(port)) { if (isNaN(port)) {
// named pipe // Named pipe
return val return val
} }
if (port >= 0) { if (port >= 0) {
// port number // Port number
return port return port
} }
@@ -48,9 +44,8 @@ function normalizePort (val) {
} }
/** /**
* Event listener for HTTP server "error" event. * Event listener for HTTP server "error" event
*/ */
function onError (error) { function onError (error) {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
throw error throw error
@@ -60,7 +55,7 @@ function onError (error) {
? 'Pipe ' + port ? 'Pipe ' + port
: 'Port ' + port : 'Port ' + port
// handle specific listen errors with friendly messages // Handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
console.error(bind + ' requires elevated privileges') console.error(bind + ' requires elevated privileges')
@@ -76,9 +71,8 @@ function onError (error) {
} }
/** /**
* Event listener for HTTP server "listening" event. * Event listener for HTTP server "listening" event
*/ */
function onListening () { function onListening () {
const addr = server.address() const addr = server.address()
const bind = typeof addr === 'string' const bind = typeof addr === 'string'

View File

@@ -1,13 +1,12 @@
/* /**
Catch Errors Handler * Catch Errors Handler
With async/await, you need some way to catch errors. * Instead of using try{} catch(e) {} in each controller, we wrap the function in
Instead of using try{} catch(e) {} in each controller, we wrap the function in * catchErrors(), catch any errors they throw, and pass it along to our express middleware with next().
catchErrors(), catch any errors they throw, and pass it along to our express middleware with next(). */
*/
exports.catchErrors = (fn) => { module.exports.catchErrors = (fn) => {
return function (req, res, next) { return function (request, response, next) {
return fn(req, res, next).catch((e) => { return fn(request, response, next).catch((e) => {
next(e) next(e)
}) })
} }

View File

@@ -1,23 +1,28 @@
const marked = require('marked') const marked = require('marked')
// Parse markdown text // Parse markdown text
exports.markdown = (content) => { module.exports.markdown = (content = '') => {
content = content || '' if (!content.trim()) {
return marked(removeIvalidDataURL(content), {sanitize: true}) return ''
}
return marked(removeInvalidDataURL(content), {sanitize: true})
} }
// Dump is a handy debugging function we can use to sort of "console.log" our data // A handy debugging function we can use to sort of "console.log" our data
exports.dump = (obj) => JSON.stringify(obj, null, 2) module.exports.dump = (obj) => JSON.stringify(obj, null, 2)
// Evil users might try to add base64 url data to execute js module.exports.formatMetaTitle = (title) => {
// so we should take care of that
function removeIvalidDataURL (content) {
let regex = /data:\S+;base64\S*/gm
return content.replace(regex, '#')
}
exports.formatMetaTitle = (title) => {
if (!title) { if (!title) {
return 'The Example App' return 'The Example App'
} }
return `${title.charAt(0).toUpperCase()}${title.slice(1)} — The Example App` return `${title.charAt(0).toUpperCase()}${title.slice(1)} — The Example App`
} }
/**
* Evil users might try to add base64 url data to execute js code
* so we should purge any potentially harmful data to mitigate risk
*/
function removeInvalidDataURL (content) {
let regex = /data:\S+;base64\S*/gm
return content.replace(regex, '#')
}

View File

@@ -1,9 +1,9 @@
const url = require('url') const url = require('url')
module.exports = (modifier) => { module.exports = (modifier) => {
return (req, res, next) => { return (request, response, next) => {
const baseUrl = url.format({ protocol: req.protocol, host: req.get('host') }) const baseUrl = url.format({ protocol: request.protocol, host: request.get('host') })
const parts = url.parse(req.url).pathname.split('/').filter(Boolean) const parts = url.parse(request.url).pathname.split('/').filter(Boolean)
let items = [] let items = []
items.push({ label: 'Home', url: baseUrl }) items.push({ label: 'Home', url: baseUrl })
@@ -19,9 +19,8 @@ module.exports = (modifier) => {
if (modifier) { if (modifier) {
items = items.map(modifier) items = items.map(modifier)
} }
// make it global // Make it global
req.app.locals.breadcrumb = items request.app.locals.breadcrumb = items
// next operation
next() next()
} }
} }

4
lib/cookies.js Normal file
View File

@@ -0,0 +1,4 @@
const ONE_YEAR_IN_SECONDS = 31536000
module.exports.updateCookie = (response, cookieName, value) => {
response.cookie(cookieName, value, { maxAge: ONE_YEAR_IN_SECONDS, httpOnly: true })
}

View File

@@ -7,6 +7,7 @@
"start": "node ./bin/www", "start": "node ./bin/www",
"lint": "eslint ./app.js routes", "lint": "eslint ./app.js routes",
"format": "eslint --fix . bin --ignore public node_modules", "format": "eslint --fix . bin --ignore public node_modules",
"test": "echo 'test'",
"test:integration": "jest test/integration", "test:integration": "jest test/integration",
"test:integration:watch": "jest test/integration --watch", "test:integration:watch": "jest test/integration --watch",
"test:unit": "jest test/unit", "test:unit": "jest test/unit",
@@ -17,16 +18,13 @@
}, },
"dependencies": { "dependencies": {
"body-parser": "~1.15.2", "body-parser": "~1.15.2",
"contentful": "^5.0.0-rc3", "contentful": "^5.0.2",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"debug": "~2.2.0",
"dotenv": "^4.0.0", "dotenv": "^4.0.0",
"express": "~4.14.0", "express": "~4.14.0",
"jstransformer-markdown-it": "^2.0.0",
"marked": "^0.3.6", "marked": "^0.3.6",
"morgan": "~1.7.0", "morgan": "~1.7.0",
"pug": "~2.0.0-beta6", "pug": "~2.0.0-beta6"
"safe-json-stringify": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
@@ -37,8 +35,6 @@
"eslint-plugin-standard": "^2.0.1", "eslint-plugin-standard": "^2.0.1",
"jest": "^21.2.1", "jest": "^21.2.1",
"nodemon": "^1.12.1", "nodemon": "^1.12.1",
"pug-lint": "^2.5.0",
"superagent": "^3.6.3",
"supertest": "^3.0.0" "supertest": "^3.0.0"
} }
} }

View File

@@ -1,5 +1,9 @@
/* GET category listing. */ /*
exports.getCategories = async (req, res, next) => { * The purpose of this module is to render the category page when the route is requested
res.render('categories', { title: 'Categories' }) */
// Renders categories page when `/categories` route is requested
module.exports.getCategories = async (request, response, next) => {
response.render('categories', { title: 'Categories' })
} }

View File

@@ -1,80 +1,124 @@
const {getCourses, getCourse, getCategories, getCoursesByCategory} = require('./../services/contentful') /*
const attachEntryState = require('./../lib/entry-state') * The purpose of this module is to render the category page when the route is requested
*/
exports.getCourses = async (req, res, next) => { const {
// We get all the entries with the content type `course` getCourses,
getCourse,
getCategories,
getCoursesByCategory
} = require('./../services/contentful')
const attachEntryState = require('../lib/entry-state')
const { updateCookie } = require('../lib/cookies')
/**
* Renders courses list when `/courses` route is requested
*
* @param request - Object - Express request object
* @param response - Object - Express response object
* @param next - Function - express callback
*
* @returns {undefined}
*/
module.exports.getCourses = async (request, response, next) => {
// Get all the entries of content type course
let courses = [] let courses = []
let categories = [] let categories = []
courses = await getCourses(res.locals.currentLocale.code, res.locals.currentApi.id) courses = await getCourses(response.locals.currentLocale.code, response.locals.currentApi.id)
// Attach entry state flags when using preview API // Attach entry state flags when using preview API
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') { if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
courses = await Promise.all(courses.map(attachEntryState)) courses = await Promise.all(courses.map(attachEntryState))
} }
categories = await getCategories(res.locals.currentLocale.code, res.locals.currentApi.id) categories = await getCategories(response.locals.currentLocale.code, response.locals.currentApi.id)
res.render('courses', { title: `All Courses (${courses.length})`, categories, courses }) response.render('courses', { title: `All Courses (${courses.length})`, categories, courses })
} }
exports.getCourse = async (req, res, next) => { /**
let course = await getCourse(req.params.slug, res.locals.currentLocale.code, res.locals.currentApi.id) * Renders a course when `/courses/:slug` route is requested
*
* @param request - Object - Express request object
* @param response - Object - Express response object
* @param next - Function - express callback
*
* @returns {undefined}
*/
module.exports.getCourse = async (request, response, next) => {
let course = await getCourse(request.params.slug, response.locals.currentLocale.code, response.locals.currentApi.id)
// Get lessons // Get lessons
const lessons = course.fields.lessons const lessons = course.fields.lessons
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug) let {lesson, lessonIndex} = getNextLesson(lessons, request.params.lslug)
const lesson = lessons[lessonIndex]
// Save visited lessons // Manage state of viewed lessons
const cookie = req.cookies.visitedLessons const cookie = request.cookies.visitedLessons
let visitedLessons = cookie || [] let visitedLessons = cookie || []
visitedLessons.push(course.sys.id) visitedLessons.push(course.sys.id)
visitedLessons = [...new Set(visitedLessons)] visitedLessons = [...new Set(visitedLessons)]
res.cookie('visitedLessons', visitedLessons, { maxAge: 900000, httpOnly: true }) updateCookie(response, 'visitedLessons', visitedLessons)
// Attach entry state flags when using preview API // Attach entry state flags when using preview API
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') { if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
course = await attachEntryState(course) course = await attachEntryState(course)
} }
res.render('course', {title: course.fields.title, course, lesson, lessons, lessonIndex, visitedLessons}) response.render('course', {title: course.fields.title, course, lesson, lessons, lessonIndex, visitedLessons})
} }
exports.getCoursesByCategory = async (req, res, next) => { /**
* Renders a courses list by a category when `/courses/category/:category` route is requested
*
* @param request - Object - Express request object
* @param response - Object - Express response object
* @param next - Function - Express callback
*
* @returns {undefined}
*/
module.exports.getCoursesByCategory = async (request, response, next) => {
// We get all the entries with the content type `course` filtered by a category // We get all the entries with the content type `course` filtered by a category
let courses = [] let courses = []
let categories = [] let categories = []
let activeCategory = '' let activeCategory = ''
try { try {
categories = await getCategories() categories = await getCategories()
activeCategory = categories.find((category) => category.fields.slug === req.params.category) activeCategory = categories.find((category) => category.fields.slug === request.params.category)
courses = await getCoursesByCategory(activeCategory.sys.id, res.locals.currentLocale.code, res.locals.currentApi.id) courses = await getCoursesByCategory(activeCategory.sys.id, response.locals.currentLocale.code, response.locals.currentApi.id)
} catch (e) { } catch (e) {
console.log('Error ', e) console.log('Error ', e)
} }
res.render('courses', { title: `${activeCategory.fields.title} (${courses.length})`, categories, courses }) response.render('courses', { title: `${activeCategory.fields.title} (${courses.length})`, categories, courses })
} }
/* GET course lesson detail. */ /**
exports.getLesson = async (req, res, next) => { * Renders a lesson details when `/courses/:courseSlug/lessons/:lessonSlug` route is requested
let course = await getCourse(req.params.cslug, res.locals.currentLocale.code, res.locals.currentApi.id) *
* @param request - Object - Express request object
* @param response - Object - Express response object
* @param next - Function - express callback
*
* @returns {undefined}
*/
module.exports.getLesson = async (request, response, next) => {
let course = await getCourse(request.params.cslug, response.locals.currentLocale.code, response.locals.currentApi.id)
const lessons = course.fields.lessons const lessons = course.fields.lessons
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug) let {lesson, nextLesson} = getNextLesson(lessons, request.params.lslug)
let lesson = lessons[lessonIndex]
const nextLesson = lessons[lessonIndex + 1] || null
// Save visited lessons // Save visited lessons
const cookie = req.cookies.visitedLessons const cookie = request.cookies.visitedLessons
let visitedLessons = cookie || [] let visitedLessons = cookie || []
visitedLessons.push(lesson.sys.id) visitedLessons.push(lesson.sys.id)
visitedLessons = [...new Set(visitedLessons)] visitedLessons = [...new Set(visitedLessons)]
res.cookie('visitedLessons', visitedLessons, { maxAge: 900000, httpOnly: true }) updateCookie(response, 'visitedLessons', visitedLessons)
// Attach entry state flags when using preview API // Attach entry state flags when using preview API
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') { if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
lesson = await attachEntryState(lesson) lesson = await attachEntryState(lesson)
} }
res.render('course', { response.render('course', {
title: `${course.fields.title} | ${lesson.fields.title}`, title: `${course.fields.title} | ${lesson.fields.title}`,
course, course,
lesson, lesson,
@@ -84,3 +128,14 @@ exports.getLesson = async (req, res, next) => {
}) })
} }
function getNextLesson (lessons, lslug) {
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === lslug)
let lesson = lessons[lessonIndex]
const nextLesson = lessons[lessonIndex + 1] || null
return {
lessonIndex,
lesson,
nextLesson
}
}

View File

@@ -1,4 +1,11 @@
exports.getImprint = (req, res, next) => { /**
res.render('imprint', { title: 'Imprint' }) * Renders imprint page when `/imprint` route is requested
* @param request - Object - Express request
* @param response - Object - Express response
* @param next - Function - Express callback
* @returns {undefined}
*/
module.exports.getImprint = (request, response, next) => {
response.render('imprint', { title: 'Imprint' })
} }

View File

@@ -1,30 +1,32 @@
/**
* This module connects rendering modules to routes
*/
const express = require('express') const express = require('express')
const { catchErrors } = require('../handlers/errorHandlers')
const { getCourses, getCourse, getLesson, getCoursesByCategory } = require('./courses')
const { getSettings, postSettings } = require('./settings')
const { getSitemap } = require('./sitemap')
const { getLandingPage } = require('./landingPage')
const { getImprint } = require('./imprint')
const router = express.Router() const router = express.Router()
/* GET the home landing page. */ const { catchErrors } = require('../handlers/errorHandlers')
const { getCourses, getCourse, getLesson, getCoursesByCategory } = require('./courses')
const { getSettings, postSettings } = require('./settings')
const { getLandingPage } = require('./landingPage')
const { getImprint } = require('./imprint')
// GET the home landing page
router.get('/', catchErrors(getLandingPage)) router.get('/', catchErrors(getLandingPage))
/* Courses Routes */ // Courses routes
router.get('/courses', catchErrors(getCourses)) router.get('/courses', catchErrors(getCourses))
router.get('/courses/categories/:category', catchErrors(getCoursesByCategory)) router.get('/courses/categories/:category', catchErrors(getCoursesByCategory))
router.get('/courses/:slug', catchErrors(getCourse)) router.get('/courses/:slug', catchErrors(getCourse))
router.get('/courses/:slug/lessons', catchErrors(getCourse)) router.get('/courses/:slug/lessons', catchErrors(getCourse))
router.get('/courses/:cslug/lessons/:lslug', catchErrors(getLesson)) router.get('/courses/:cslug/lessons/:lslug', catchErrors(getLesson))
/* Settings Routes */ // Settings routes
router.get('/settings', catchErrors(getSettings)) router.get('/settings', catchErrors(getSettings))
router.post('/settings', catchErrors(postSettings)) router.post('/settings', catchErrors(postSettings))
/* Sitemap Route */ // Imprint route
router.get('/sitemap', catchErrors(getSitemap))
/* Imprint Route */
router.get('/imprint', catchErrors(getImprint)) router.get('/imprint', catchErrors(getImprint))
module.exports = router module.exports = router

View File

@@ -1,21 +1,36 @@
const { getLandingPage } = require('../services/contentful') /**
const attachEntryState = require('./../lib/entry-state') * This module renders a layout when its route is requested
* It is used for pages like home page
*/
const url = require('url') const url = require('url')
exports.getLandingPage = async (req, res, next) => { const { getLandingPage } = require('../services/contentful')
let pathname = url.parse(req.url).pathname.split('/').filter(Boolean)[0] const attachEntryState = require('./../lib/entry-state')
/**
* Renders a landing page when `/` route is requested
* based on the pathname an entry is queried from contentful
* and a view is rendered from the pulled data
*
* @param request - Object - Express request
* @param response - Object - Express response
* @param next - Function - Express callback
* @returns {undefined}
*/
module.exports.getLandingPage = async (request, response, next) => {
let pathname = url.parse(request.url).pathname.split('/').filter(Boolean)[0]
pathname = pathname || 'home' pathname = pathname || 'home'
let landingPage = await getLandingPage( let landingPage = await getLandingPage(
pathname, pathname,
res.locals.currentLocale.code, response.locals.currentLocale.code,
res.locals.currentApi.id response.locals.currentApi.id
) )
// Attach entry state flags when using preview APIgs // Attach entry state flags when using preview API
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') { if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
landingPage = await attachEntryState(landingPage) landingPage = await attachEntryState(landingPage)
} }
res.render('landingPage', { title: pathname, landingPage }) response.render('landingPage', { title: pathname, landingPage })
} }

View File

@@ -1,16 +1,25 @@
/**
* This module renders the settings page when `settings` route is requested
* it also saves the settings to a cookie
*/
const { createClient } = require('contentful') const { createClient } = require('contentful')
const { initClient, getSpace } = require('./../services/contentful') const { initClients, getSpace } = require('./../services/contentful')
const { updateCookie } = require('../lib/cookies')
async function renderSettings (res, opts) { const SETTINGS_NAME = 'theExampleAppSettings'
// Get connectred space to display the space name on top of the settings
async function renderSettings (response, opts) {
// Get connected space to display the space name on top of the settings
let space = false let space = false
try { try {
space = await getSpace() space = await getSpace()
} catch (error) { } catch (error) {
console.error(error) // We throw the error here, it will be handled by the error middleware
// We keep space false to ensure the "Connected to" box is not shown.
throw (error)
} }
res.render('settings', { response.render('settings', {
title: 'Settings', title: 'Settings',
errors: {}, errors: {},
hasErrors: false, hasErrors: false,
@@ -20,68 +29,85 @@ async function renderSettings (res, opts) {
}) })
} }
/* GET settings page. */ /**
exports.getSettings = async (req, res, next) => { * Renders the settings page when `/settings` route is requested
const { settings } = res.locals *
await renderSettings(res, { * @param request - Object - Express request
* @param response - Object - Express response
* @param next - Function - Express callback
*
* @returns {undefined}
*/
module.exports.getSettings = async (request, response, next) => {
const { settings } = response.locals
await renderSettings(response, {
settings settings
}) })
} }
/* POST settings page. */ /**
exports.postSettings = async (req, res, next) => { * Save settings when POST request is triggered to the `/settings` route
* and render the settings page
*
* @param request - Object - Express request
* @param response - Object - Express response
* @param next - Function - Express callback
*
* @returns {undefined}
*/
module.exports.postSettings = async (request, response, next) => {
const errorList = [] const errorList = []
const { space, cda, cpa, editorialFeatures } = req.body const { spaceId, deliveryToken, previewToken, editorialFeatures } = request.body
const settings = { const settings = {
space, spaceId,
cda, deliveryToken,
cpa, previewToken,
editorialFeatures: !!editorialFeatures editorialFeatures: !!editorialFeatures
} }
// Validate required fields. // Validate required fields.
if (!space) { if (!spaceId) {
errorList.push({ errorList.push({
field: 'space', field: 'spaceId',
message: 'This field is required' message: 'This field is required'
}) })
} }
if (!cda) { if (!deliveryToken) {
errorList.push({ errorList.push({
field: 'cda', field: 'deliveryToken',
message: 'This field is required' message: 'This field is required'
}) })
} }
if (!cpa) { if (!previewToken) {
errorList.push({ errorList.push({
field: 'cpa', field: 'previewToken',
message: 'This field is required' message: 'This field is required'
}) })
} }
// Validate space and CDA access token. // Validate space and delivery access token.
if (space && cda) { if (spaceId && deliveryToken) {
try { try {
await createClient({ await createClient({
space, space: spaceId,
accessToken: cda accessToken: deliveryToken
}).getSpace() }).getSpace()
} catch (err) { } catch (err) {
if (err.response.status === 401) { if (err.response.status === 401) {
errorList.push({ errorList.push({
field: 'cda', field: 'deliveryToken',
message: 'Your Delivery API key is invalid.' message: 'Your Delivery API key is invalid.'
}) })
} else if (err.response.status === 404) { } else if (err.response.status === 404) {
errorList.push({ errorList.push({
field: 'space', field: 'spaceId',
message: 'This space does not exist or your access token is not associated with your space.' message: 'This space does not exist or your access token is not associated with your space.'
}) })
} else { } else {
errorList.push({ errorList.push({
field: 'cda', field: 'deliveryToken',
message: `Something went wrong: ${err.response.data.message}` message: `Something went wrong: ${err.response.data.message}`
}) })
} }
@@ -89,24 +115,27 @@ exports.postSettings = async (req, res, next) => {
} }
// Validate space and CPA access token. // Validate space and CPA access token.
if (space && cpa) { if (spaceId && previewToken) {
try { try {
await createClient({ await createClient({
space, space: spaceId,
accessToken: cpa, accessToken: previewToken,
host: 'preview.contentful.com' host: 'preview.contentful.com'
}).getSpace() }).getSpace()
} catch (err) { } catch (err) {
if (err.response.status === 401) { if (err.response.status === 401) {
errorList.push({ errorList.push({
field: 'cpa', field: 'previewToken',
message: 'Your Preview API key is invalid.' message: 'Your Preview API key is invalid.'
}) })
} else if (err.response.status === 404) { } else if (err.response.status === 404) {
// Already validated via CDA errorList.push({
field: 'spaceId',
message: 'This space does not exist or your delivery token is not associated with your space.'
})
} else { } else {
errorList.push({ errorList.push({
field: 'cpa', field: 'previewToken',
message: `Something went wrong: ${err.response.data.message}` message: `Something went wrong: ${err.response.data.message}`
}) })
} }
@@ -116,11 +145,11 @@ exports.postSettings = async (req, res, next) => {
// When no errors occurred // When no errors occurred
if (!errorList.length) { if (!errorList.length) {
// Store new settings // Store new settings
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true }) updateCookie(response, SETTINGS_NAME, settings)
res.locals.settings = settings response.locals.settings = settings
// Reinit clients // Reinit clients
initClient(settings) initClients(settings)
} }
// Generate error dictionary // Generate error dictionary
@@ -135,7 +164,7 @@ exports.postSettings = async (req, res, next) => {
} }
}, {}) }, {})
await renderSettings(res, { await renderSettings(response, {
settings, settings,
errors, errors,
hasErrors: errorList.length > 0, hasErrors: errorList.length > 0,

View File

@@ -1,5 +0,0 @@
/* GET sitemap page. */
exports.getSitemap = async (req, res, next) => {
res.render('sitemap', { title: 'Sitemap' })
}

View File

@@ -1,107 +1,148 @@
/**
* The purpose of this module is to get data from contentful
*/
const { createClient } = require('contentful') const { createClient } = require('contentful')
let cdaClient = null let deliveryClient = null
let cpaClient = null let previewClient = null
// Initialize our client /**
exports.initClient = (options) => { * Initialize the contentful Client
// Getting the version the app version * @param options {space: string, cda: string, cpa: string}
*
* @returns {undefined}
*/
module.exports.initClients = (options) => {
// Getting the app version
const { version } = require('../package.json') const { version } = require('../package.json')
const config = options || { const config = options || {
space: process.env.CF_SPACE, spaceId: process.env.CF_SPACE_ID,
cda: process.env.CF_ACCESS_TOKEN, deliveryToken: process.env.CF_DELIVERY_TOKEN,
cpa: process.env.CF_PREVIEW_ACCESS_TOKEN previewToken: process.env.CF_PREVIEW_TOKEN
} }
cdaClient = createClient({ deliveryClient = createClient({
application: `contentful.the-example-app.node/${version}`, application: `the-example-app.node/${version}`,
space: config.space, space: config.spaceId,
accessToken: config.cda accessToken: config.deliveryToken
}) })
cpaClient = createClient({ previewClient = createClient({
application: `contentful.the-example-app.node/${version}`, application: `the-example-app.node/${version}`,
space: config.space, space: config.spaceId,
accessToken: config.cpa, accessToken: config.previewToken,
host: 'preview.contentful.com' host: 'preview.contentful.com'
}) })
} }
// Get the Space the app is connected to. Used for the settings form and to get all available locales. /**
exports.getSpace = assert((api = `cda`) => { * Get the Space the app is connected to. Used for the settings form and to get all available locales
const client = api === 'cda' ? cdaClient : cpaClient * @param api - string - the api to use, cda or cap. Default: 'cda'
return client.getSpace() * @returns {undefined}
*/
module.exports.getSpace = assert((api = `cda`) => {
return getClient(api).getSpace()
}, 'Space') }, 'Space')
// Get a single entry. Used to detect the `Draft` or `Pending Changes` state. /**
exports.getEntry = assert((entryId, api = `cda`) => { * Gets an entry. Used to detect the `Draft` or `Pending Changes` state
const client = api === 'cda' ? cdaClient : cpaClient * @param entryId - string - the entry id
return client.getEntry(entryId) * @param api - string - the api to use fetching the entry
*
* @returns {Object}
*/
module.exports.getEntry = assert((entryId, api = `cda`) => {
return getClient(api).getEntry(entryId)
}, 'Entry') }, 'Entry')
// to get all the courses we request all the entries /**
// with the content_type `course` from Contentful * Get all entries with content_type `course`
exports.getCourses = assert((locale = 'en-US', api = `cda`) => { * @param locale - string - the locale of the entry [default: 'en-US']
const client = api === 'cda' ? cdaClient : cpaClient * @param api - string the api enpoint to use when fetching the data
return client.getEntries({ * @returns {Array<Object>}
*/
module.exports.getCourses = assert((locale = 'en-US', api = `cda`) => {
return getClient(api).getEntries({
content_type: 'course', content_type: 'course',
locale, locale,
order: 'sys.createdAt', order: 'sys.createdAt', // Ordering the entries by creation date
include: 10 include: 6 // We use include param to increase the link level, the include value goes from 1 to 6
}) })
.then((response) => response.items) .then((response) => response.items)
}, 'Course') }, 'Course')
// Landing pages like the home or about page are fully controlable via Contentful. /**
exports.getLandingPage = (slug, locale = 'en-US', api = `cda`) => { * Get entries of content_type `layout` e.g. Landing page
const client = api === 'cda' ? cdaClient : cpaClient * @param slug - string - the slug of the entry to use in the query
return client.getEntries({ * @param locale - string - locale of the entry to request [default: 'en-US']
* @param api - string - the api enpoint to use when fetching the data
* @returns {Object}
*/
module.exports.getLandingPage = (slug, locale = 'en-US', api = `cda`) => {
// Even though we need a single entry, we request it using the collection endpoint
// To get all the linked refs in one go, the SDK will use the data and resolve the links automatically
return getClient(api).getEntries({
content_type: 'layout', content_type: 'layout',
locale, locale,
'fields.slug': slug, 'fields.slug': slug,
include: 10 include: 6
}) })
.then((response) => response.items[0]) .then((response) => response.items[0])
} }
// the SDK supports link resolution only when you call the collection endpoints /**
// That's why we are using getEntries with a query instead of getEntry(entryId) * Get an entry with content_type `course`
// make sure to specify the content_type whenever you want to perform a query * @param slug - string - the slug of the entry to use in the query
exports.getCourse = assert((slug, locale = 'en-US', api = `cda`) => { * @param locale - string - locale of the entry to request [default: 'en-US']
const client = api === 'cda' ? cdaClient : cpaClient * @param api - string - the api enpoint to use when fetching the data
return client.getEntries({ * @returns {Object}
*/
module.exports.getCourse = assert((slug, locale = 'en-US', api = `cda`) => {
// Even though we need a single entry, we request it using the collection endpoint
// To get all the linked refs in one go, the SDK will use the data and resolve the links automatically
return getClient(api).getEntries({
content_type: 'course', content_type: 'course',
'fields.slug': slug, 'fields.slug': slug,
locale, locale,
include: 10 include: 6
}) })
.then((response) => response.items[0]) .then((response) => response.items[0])
}, 'Course') }, 'Course')
exports.getCategories = assert((locale = 'en-US', api = `cda`) => { module.exports.getCategories = assert((locale = 'en-US', api = `cda`) => {
const client = api === 'cda' ? cdaClient : cpaClient return getClient(api).getEntries({content_type: 'category', locale})
return client.getEntries({content_type: 'category', locale})
.then((response) => response.items) .then((response) => response.items)
}, 'Course') }, 'Course')
// Getting a course by Category is simply querying all entries /**
// with a query params `fields.categories.sys.id` equal to the desired category id * Get Courses by Categories
// Note that you need to send the `content_type` param to be able to query the entry * To get a course by category, simply query all entries
exports.getCoursesByCategory = assert((category, locale = 'en-US', api = `cda`) => { * with a query params `fields.categories.sys.id` equal to the desired category id
const client = api === 'cda' ? cdaClient : cpaClient * Note that you need to send the `content_type` param to be able to query the entry
return client.getEntries({ * @param category - string - the id of the category
* @param locale - string - locale of the entry to request [default: 'en-US']
* @param api - string - the api enpoint to use when fetching the data
* @returns {Object}
*/
module.exports.getCoursesByCategory = assert((category, locale = 'en-US', api = `cda`) => {
return getClient(api).getEntries({
content_type: 'course', content_type: 'course',
'fields.categories.sys.id': category, 'fields.categories.sys.id': category,
locale, locale,
order: '-sys.createdAt', order: '-sys.createdAt',
include: 10 include: 6
}) })
.then((response) => response.items) .then((response) => response.items)
}, 'Category') }, 'Category')
// Utitlities functions // Utility function
function getClient (api = 'cda') {
return api === 'cda' ? deliveryClient : previewClient
}
function assert (fn, context) { function assert (fn, context) {
return function (req, res, next) { return function (request, response, next) {
return fn(req, res, next) return fn(request, response, next)
.then((data) => { .then((data) => {
if (!data) { if (!data) {
var err = new Error(`${context} Not Found`) var err = new Error(`${context} Not Found`)

6
test/.eslintrc.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
'env': {
'node': true,
'jest': true
}
}

View File

@@ -1,7 +1,7 @@
/* global describe, test, expect */
const app = require('../../app')
const request = require('supertest') const request = require('supertest')
const app = require('../../app')
describe('courses', () => { describe('courses', () => {
test('it should render a list of courses', () => { test('it should render a list of courses', () => {
return request(app).get('/courses') return request(app).get('/courses')

View File

@@ -1,7 +1,7 @@
/* global describe, test */
const app = require('../../app')
const request = require('supertest') const request = require('supertest')
const app = require('../../app')
describe('Home page', () => { describe('Home page', () => {
test('it should render the landing page', () => { test('it should render the landing page', () => {
return request(app).get('/').expect(200) return request(app).get('/').expect(200)

View File

@@ -1,12 +1,13 @@
const app = require('../../app')
const request = require('supertest')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const cookie = require('cookie') const cookie = require('cookie')
const cookieParser = require('cookie-parser') const cookieParser = require('cookie-parser')
const request = require('supertest')
function getSettingsCookie (res) { const app = require('../../app')
function getSettingsCookie (response) {
try { try {
const cookies = res.headers['set-cookie'] const cookies = response.headers['set-cookie']
const settingsCookie = cookies.find((cookie) => cookie.startsWith('theExampleAppSettings=')) const settingsCookie = cookies.find((cookie) => cookie.startsWith('theExampleAppSettings='))
const parsedCookie = cookie.parse(settingsCookie) const parsedCookie = cookie.parse(settingsCookie)
return cookieParser.JSONCookie(parsedCookie.theExampleAppSettings) return cookieParser.JSONCookie(parsedCookie.theExampleAppSettings)
@@ -38,15 +39,15 @@ describe('settings', () => {
expect(inputCpa.val()).toBe(process.env.CF_PREVIEW_ACCESS_TOKEN) expect(inputCpa.val()).toBe(process.env.CF_PREVIEW_ACCESS_TOKEN)
const inputEditorialFeatures = $('#input-editorial-features') const inputEditorialFeatures = $('#input-editorial-features')
expect(inputEditorialFeatures.prop('checked')).toBeFalsy() expect(inputEditorialFeaturesponse.prop('checked')).toBeFalsy()
}) })
}) })
test('should have the editorial features enabled when query parameter is set and set cookie for it', () => { test('should have the editorial features enabled when query parameter is set and set cookie for it', () => {
return request(app).get('/settings?enable_editorial_features') return request(app).get('/settings?enable_editorial_features')
.expect(200) .expect(200)
.expect((res) => { .expect((response) => {
const cookie = getSettingsCookie(res) const cookie = getSettingsCookie(response)
if (!cookie.editorialFeatures) { if (!cookie.editorialFeatures) {
throw new Error('Did not set cookie value for editorial features') throw new Error('Did not set cookie value for editorial features')
} }
@@ -67,7 +68,7 @@ describe('settings', () => {
const $ = cheerio.load(response.text) const $ = cheerio.load(response.text)
const inputEditorialFeatures = $('#input-editorial-features') const inputEditorialFeatures = $('#input-editorial-features')
expect(inputEditorialFeatures.prop('checked')).toBeTruthy() expect(inputEditorialFeaturesponse.prop('checked')).toBeTruthy()
}) })
}) })
}) })

View File

@@ -6,8 +6,8 @@ const { mockCourse, mockCategory } = require('./mocks/index')
jest.mock('../../services/contentful') jest.mock('../../services/contentful')
const contentful = require('../../services/contentful') const contentful = require('../../services/contentful')
const req = {} const request = {}
const res = { const response = {
locals: { locals: {
currentLocale: { currentLocale: {
code: 'en-US' code: 'en-US'
@@ -25,58 +25,58 @@ beforeAll(() => {
contentful.getCategories.mockImplementation(() => [mockCategory]) contentful.getCategories.mockImplementation(() => [mockCategory])
contentful.getCoursesByCategory.mockImplementation(() => []) contentful.getCoursesByCategory.mockImplementation(() => [])
res.render = jest.fn() response.render = jest.fn()
res.cookie = jest.fn() response.cookie = jest.fn()
req.cookies = { visitedLessons: [] } request.cookies = { visitedLessons: [] }
}) })
afterEach(() => { afterEach(() => {
res.render.mockClear() response.render.mockClear()
res.render.mockReset() response.render.mockReset()
}) })
describe('Courses', () => { describe('Courses', () => {
test('it should courses list once', async () => { test('it should courses list once', async () => {
await getCourses(req, res) await getCourses(request, response)
expect(res.render.mock.calls[0][0]).toBe('courses') expect(response.render.mock.calls[0][0]).toBe('courses')
expect(res.render.mock.calls[0][1].title).toBe('All Courses (1)') expect(response.render.mock.calls[0][1].title).toBe('All Courses (1)')
expect(res.render.mock.calls[0][1].courses.length).toBe(1) expect(response.render.mock.calls[0][1].courses.length).toBe(1)
expect(res.render.mock.calls.length).toBe(1) expect(response.render.mock.calls.length).toBe(1)
}) })
test('it should render single course once', async () => { test('it should render single course once', async () => {
req.params = {slug: 'slug', lslug: 'lessonSlug'} request.params = {slug: 'slug', lslug: 'lessonSlug'}
await getCourse(req, res) await getCourse(request, response)
expect(res.render.mock.calls[0][0]).toBe('course') expect(response.render.mock.calls[0][0]).toBe('course')
expect(res.render.mock.calls[0][1].title).toBe(mockCourse.fields.title) expect(response.render.mock.calls[0][1].title).toBe(mockCourse.fields.title)
expect(res.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id) expect(response.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
expect(res.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id) expect(response.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
expect(res.render.mock.calls.length).toBe(1) expect(response.render.mock.calls.length).toBe(1)
}) })
test('it should render list of courses by categories', async () => { test('it should render list of courses by categories', async () => {
req.params = {slug: 'slug', lslug: 'lslug', category: 'categorySlug'} request.params = {slug: 'slug', lslug: 'lslug', category: 'categorySlug'}
await getCoursesByCategory(req, res) await getCoursesByCategory(request, response)
expect(res.render.mock.calls[0][0]).toBe('courses') expect(response.render.mock.calls[0][0]).toBe('courses')
expect(res.render.mock.calls[0][1].title).toBe(`${mockCategory.fields.title} (0)`) expect(response.render.mock.calls[0][1].title).toBe(`${mockCategory.fields.title} (0)`)
expect(res.render.mock.calls.length).toBe(1) expect(response.render.mock.calls.length).toBe(1)
}) })
}) })
describe('Lessons', () => { describe('Lessons', () => {
test('it should render a lesson', async () => { test('it should render a lesson', async () => {
req.params = { cslug: 'courseSlug', lslug: 'lessonSlug' } request.params = { cslug: 'courseSlug', lslug: 'lessonSlug' }
await getLesson(req, res) await getLesson(request, response)
expect(res.render.mock.calls[0][0]).toBe('course') expect(response.render.mock.calls[0][0]).toBe('course')
expect(res.render.mock.calls[0][1].title).toBe('Course title | Lesson title') expect(response.render.mock.calls[0][1].title).toBe('Course title | Lesson title')
expect(res.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id) expect(response.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
expect(res.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id) expect(response.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
expect(res.render.mock.calls.length).toBe(1) expect(response.render.mock.calls.length).toBe(1)
}) })
}) })
describe('Settings', () => { describe('Settings', () => {
test('It should render settings', async () => { test('It should render settings', async () => {
res.locals = { response.locals = {
settings: { settings: {
space: 'spaceId', space: 'spaceId',
cda: 'cda', cda: 'cda',
@@ -84,9 +84,9 @@ describe('Settings', () => {
editorialFeatures: false editorialFeatures: false
} }
} }
await getSettings(req, res) await getSettings(request, response)
expect(res.render.mock.calls[0][0]).toBe('settings') expect(response.render.mock.calls[0][0]).toBe('settings')
expect(res.render.mock.calls[0][1].title).toBe('Settings') expect(response.render.mock.calls[0][1].title).toBe('Settings')
expect(res.render.mock.calls[0][1].settings).toBe(res.locals.settings) expect(response.render.mock.calls[0][1].settings).toBe(response.locals.settings)
}) })
}) })

View File

@@ -1,5 +1,5 @@
NODE_ENV=development NODE_ENV=development
CF_SPACE=8im83r7wdkz2 CONTENTFUL_SPACE_ID=ft4tkuv7nwl0
CF_ACCESS_TOKEN=febda697c8d0031e6da7df9c56c97c1b19a8a8f5c400c0deacfdf7c25397d5ae CONTENTFUL_DELIVERY_TOKEN=57459fe48bd2b1bef4855294455af52562dbc0c7f0eb84f8b2cd68692c186417
CF_PREVIEW_ACCESS_TOKEN=7480498b164ce9d580c811ce3ccd1c73518f967ba7afd83384654aff866bd5d2 CONTENTFUL_PREVIEW_TOKEN=a9972e3cd83528def2fc9d3428c67cd622eb26d0a24239718c6ac61fe0288f2f
PORT=3000 PORT=3000

View File

@@ -39,26 +39,26 @@ block content
form(action=`/settings` method="POST" class="form") form(action=`/settings` method="POST" class="form")
.form-item .form-item
label(for="input-space") Space ID label(for="input-space-id") Space ID
input(type="text" name="space" id="input-space" value=settings.space) input(type="text" name="spaceId" id="input-space-id" value=settings.spaceId)
if 'space' in errors if 'spaceId' in errors
+renderErrors(errors.space) +renderErrors(errors.spaceId)
.form-item__help-text The Space ID is a unique identifier for your space. .form-item__help-text The Space ID is a unique identifier for your space.
.form-item .form-item
label(for="input-cda") Content Delivery API - access token label(for="input-delivery-token") Content Delivery API - access token
input(type="text" name="cda" id="input-cda" value=settings.cda) input(type="text" name="deliveryToken" id="input-delivery-token" value=settings.deliveryToken)
if 'cda' in errors if 'deliveryToken' in errors
+renderErrors(errors.cda) +renderErrors(errors.deliveryToken)
.form-item__help-text .form-item__help-text
| View published content using this API.&nbsp; | View published content using this API.&nbsp;
a(href='https://www.contentful.com/developers/docs/references/content-delivery-api/' target='_blank' rel='noopener') Content Delivery API. a(href='https://www.contentful.com/developers/docs/references/content-delivery-api/' target='_blank' rel='noopener') Content Delivery API.
.form-item .form-item
label(for="input-cpa") Content Preview API - access token label(for="input-preview-token") Content Preview API - access token
input(type="text" name="cpa" id="input-cpa" value=settings.cpa) input(type="text" name="previewToken" id="input-preview-token" value=settings.previewToken)
if 'cpa' in errors if 'previewToken' in errors
+renderErrors(errors.cpa) +renderErrors(errors.previewToken)
.form-item__help-text .form-item__help-text
| Preview unpublished content using this API (i.e. content with “Draft” status).&nbsp; | Preview unpublished content using this API (i.e. content with “Draft” status).&nbsp;
a(href='https://www.contentful.com/developers/docs/references/content-preview-api/' target='_blank' rel='noopener') Content Preview API. a(href='https://www.contentful.com/developers/docs/references/content-preview-api/' target='_blank' rel='noopener') Content Preview API.

View File

@@ -1,6 +0,0 @@
extends layout
block content
.layout-centered
h1= title
p Welcome to #{title}