Renamed reason to description in recommendations table (#18527)

fixes https://github.com/TryGhost/Product/issues/4005

We no longer use the 'reason' of a recommendation, but allow a flexible
description instead. Because this is a breaking change in the API, we do
this before making this feature GA.
- Added new database utils for renaming a column
- Added new migration to rename the column
- Updated all references in code
This commit is contained in:
Simon Backx 2023-10-09 16:19:44 +02:00 committed by GitHub
parent a2dd0ea1ca
commit 9abd466397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 515 additions and 224 deletions

View File

@ -4,7 +4,7 @@ import {Meta, apiUrl, createInfiniteQuery, createMutation, useFetchApi} from '..
export type Recommendation = {
id: string
title: string
reason: string|null
description: string|null
excerpt: string|null // Fetched from the site meta data
featured_image: string|null // Fetched from the site meta data
favicon: string|null // Fetched from the site meta data

View File

@ -61,7 +61,7 @@ const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModa
initialState: recommendation ?? {
title: '',
url: initialUrlCleaned,
reason: '',
description: '',
excerpt: null,
featured_image: null,
favicon: null,
@ -119,7 +119,7 @@ const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModa
updatedRecommendation.favicon = oembed?.metadata?.icon ?? formState.favicon ?? null;
updatedRecommendation.one_click_subscribe = false;
}
updatedRecommendation.reason = updatedRecommendation.excerpt || null;
updatedRecommendation.description = updatedRecommendation.excerpt || null;
// Switch modal without changing the route (the second modal is not reachable by URL)
modal.remove();

View File

@ -2,7 +2,7 @@ import AddRecommendationModal from './AddRecommendationModal';
import Modal from '../../../../admin-x-ds/global/modal/Modal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import RecommendationReasonForm, {validateReasonForm} from './RecommendationReasonForm';
import RecommendationDescriptionForm, {validateDescriptionForm} from './RecommendationDescriptionForm';
import trackEvent from '../../../../utils/plausible';
import useForm from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
@ -39,7 +39,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
},
onSaveError: handleError,
onValidate: (state) => {
const newErrors = validateReasonForm(state);
const newErrors = validateDescriptionForm(state);
if (Object.keys(newErrors).length !== 0) {
showToast({
@ -127,7 +127,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
}
}}
>
<RecommendationReasonForm clearError={clearError} errors={errors} formState={formState} setErrors={setErrors} showURL={false} updateForm={updateForm}/>
<RecommendationDescriptionForm clearError={clearError} errors={errors} formState={formState} setErrors={setErrors} showURL={false} updateForm={updateForm}/>
</Modal>;
};

View File

@ -2,7 +2,7 @@ import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationM
import Modal from '../../../../admin-x-ds/global/modal/Modal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import RecommendationReasonForm, {validateReasonForm} from './RecommendationReasonForm';
import RecommendationDescriptionForm, {validateDescriptionForm} from './RecommendationDescriptionForm';
import useForm from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
@ -33,7 +33,7 @@ const EditRecommendationModal: React.FC<RoutingModalProps & EditRecommendationMo
},
onSaveError: handleError,
onValidate: (state) => {
const newErrors = validateReasonForm(state);
const newErrors = validateDescriptionForm(state);
if (Object.keys(newErrors).length !== 0) {
showToast({
@ -119,7 +119,7 @@ const EditRecommendationModal: React.FC<RoutingModalProps & EditRecommendationMo
}
}}
>
<RecommendationReasonForm clearError={clearError} errors={errors} formState={formState} setErrors={setErrors} showURL={true} updateForm={updateForm as any}/>
<RecommendationDescriptionForm clearError={clearError} errors={errors} formState={formState} setErrors={setErrors} showURL={true} updateForm={updateForm as any}/>
</Modal>;
};

View File

@ -18,7 +18,7 @@ interface Props<T extends EditOrAddRecommendation> {
setErrors: (errors: ErrorMessages) => void
}
export const validateReasonFormField = function (errors: ErrorMessages, field: 'title'|'reason', value: string|null) {
export const validateDescriptionFormField = function (errors: ErrorMessages, field: 'title'|'description', value: string|null) {
const cloned = {...errors};
switch (field) {
case 'title':
@ -28,11 +28,11 @@ export const validateReasonFormField = function (errors: ErrorMessages, field: '
delete cloned.title;
}
break;
case 'reason':
case 'description':
if (value && value.length > 200) {
cloned.reason = 'Description cannot be longer than 200 characters';
cloned.description = 'Description cannot be longer than 200 characters';
} else {
delete cloned.reason;
delete cloned.description;
}
break;
default:
@ -43,16 +43,16 @@ export const validateReasonFormField = function (errors: ErrorMessages, field: '
return cloned;
};
export const validateReasonForm = function (formState: EditOrAddRecommendation) {
export const validateDescriptionForm = function (formState: EditOrAddRecommendation) {
let newErrors: ErrorMessages = {};
newErrors = validateReasonFormField(newErrors, 'title', formState.title);
newErrors = validateReasonFormField(newErrors, 'reason', formState.reason);
newErrors = validateDescriptionFormField(newErrors, 'title', formState.title);
newErrors = validateDescriptionFormField(newErrors, 'description', formState.description);
return newErrors;
};
const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recommendation>> = ({showURL, formState, updateForm, errors, clearError, setErrors}) => {
const [reasonLength, setReasonLength] = React.useState(formState?.reason?.length || 0);
const reasonLengthColor = reasonLength > 200 ? 'text-red' : 'text-green';
const RecommendationDescriptionForm: React.FC<Props<EditOrAddRecommendation | Recommendation>> = ({showURL, formState, updateForm, errors, clearError, setErrors}) => {
const [descriptionLength, setDescriptionLength] = React.useState(formState?.description?.length || 0);
const descriptionLengthColor = descriptionLength > 200 ? 'text-red' : 'text-green';
// Do an intial validation on mounting
const didValidate = React.useRef(false);
@ -61,7 +61,7 @@ const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recomme
return;
}
didValidate.current = true;
setErrors(validateReasonForm(formState));
setErrors(validateDescriptionForm(formState));
}, [formState, setErrors]);
return <Form
@ -79,7 +79,7 @@ const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recomme
<RecommendationIcon {...formState} />
<span className='text-[1.6rem] font-semibold text-grey-900'>{formState.title}</span>
</div>
{formState.reason && <span className='pl-[31px] text-[1.35rem] leading-snug text-grey-700'>{formState.reason}</span>}
{formState.description && <span className='pl-[31px] text-[1.35rem] leading-snug text-grey-700'>{formState.description}</span>}
</div>
{formState.one_click_subscribe && <span className='flex whitespace-nowrap pl-6 text-md font-semibold text-green'>Subscribe</span>}
</a>
@ -108,7 +108,7 @@ const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recomme
title="Title"
value={formState.title ?? ''}
onBlur={() => setErrors(
validateReasonFormField(errors, 'title', formState.title)
validateDescriptionFormField(errors, 'title', formState.title)
)}
onChange={(e) => {
clearError?.('title');
@ -117,22 +117,22 @@ const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recomme
/>
<TextArea
clearBg={true}
error={Boolean(errors.reason)}
error={Boolean(errors.description)}
// Note: we don't show the error text here, because errors are related to the character count
hint={<>Max: <strong>200</strong> characters. You&#8217;ve used <strong className={reasonLengthColor}>{reasonLength}</strong></>}
hint={<>Max: <strong>200</strong> characters. You&#8217;ve used <strong className={descriptionLengthColor}>{descriptionLength}</strong></>}
rows={4}
title="Short description"
value={formState.reason ?? ''}
value={formState.description ?? ''}
onBlur={() => setErrors(
validateReasonFormField(errors, 'reason', formState.reason)
validateDescriptionFormField(errors, 'description', formState.description)
)}
onChange={(e) => {
clearError?.('reason');
setReasonLength(e.target.value.length);
updateForm(state => ({...state, reason: e.target.value}));
clearError?.('description');
setDescriptionLength(e.target.value.length);
updateForm(state => ({...state, description: e.target.value}));
}}
/>
</Form>;
};
export default RecommendationReasonForm;
export default RecommendationDescriptionForm;

View File

@ -15,7 +15,7 @@ test.describe('Recommendations', async () => {
favicon: null,
featured_image: null,
one_click_subscribe: false,
reason: 'This is a cool website',
description: 'This is a cool website',
title: 'example.com',
url: 'https://example.com/a-cool-website'}
]}}
@ -38,7 +38,7 @@ test.describe('Recommendations', async () => {
favicon: null,
featured_image: null,
one_click_subscribe: false,
reason: 'This is a cool website',
description: 'This is a cool website',
title: 'example.com',
url: 'https://example.com/a-cool-website'}
]

View File

@ -2,7 +2,7 @@
"recommendations": {
"id": "21452552",
"title": "Some Recommendation",
"reason": "i like this website",
"description": "i like this website",
"excerpt": "this is a great website",
"feature_image": null,
"favicon": null,

View File

@ -104,11 +104,11 @@ export const RecommendationsPageStyles = `
animation: 0.5s ease-in-out fadeIn;
}
.gh-portal-recommendation-subscribed.with-reason {
.gh-portal-recommendation-subscribed.with-description {
position: absolute;
}
.gh-portal-recommendation-subscribed.without-reason {
.gh-portal-recommendation-subscribed.without-description {
margin-top: 5px;
}
@ -184,7 +184,7 @@ const openTab = (url) => {
const RecommendationItem = (recommendation) => {
const {t, onAction, member, site} = useContext(AppContext);
const {title, url, reason, favicon, one_click_subscribe: oneClickSubscribe, featured_image: featuredImage} = recommendation;
const {title, url, description, favicon, one_click_subscribe: oneClickSubscribe, featured_image: featuredImage} = recommendation;
const allowOneClickSubscribe = member && oneClickSubscribe;
const [subscribed, setSubscribed] = useState(false);
const [clicked, setClicked] = useState(false);
@ -263,8 +263,8 @@ const RecommendationItem = (recommendation) => {
<ArrowIcon className="gh-portal-recommendation-arrow-icon" />
</div>
<div className="gh-portal-recommendation-description-container">
{subscribed && <div className={'gh-portal-recommendation-subscribed ' + (reason ? 'with-reason' : 'without-reason')}><span>{t('Verification link sent, check your inbox')}</span><CheckmarkIcon className="gh-portal-recommendation-checkmark-icon" alt=''/></div>}
{reason && <p className={subscribed ? 'gh-portal-recommendation-description-hidden' : ''}>{reason}</p>}
{subscribed && <div className={'gh-portal-recommendation-subscribed ' + (description ? 'with-description' : 'without-description')}><span>{t('Verification link sent, check your inbox')}</span><CheckmarkIcon className="gh-portal-recommendation-checkmark-icon" alt=''/></div>}
{description && <p className={subscribed ? 'gh-portal-recommendation-description-hidden' : ''}>{description}</p>}
</div>
</div>
<div className="gh-portal-recommendation-item-action">

View File

@ -5,7 +5,7 @@
<a href="{{rec.url}}" data-recommendation="{{rec.id}}" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="{{rec.favicon}}" alt="{{rec.title}}">
<h5 class="recommendation-title">{{rec.title}}</h5>
<p class="recommendation-reason">{{rec.reason}}</p>
<p class="recommendation-description">{{rec.description}}</p>
</a>
</li>
{{/each}}

View File

@ -91,7 +91,35 @@ function createSetNullableMigration(table, column, options = {}) {
if (options.disableForeignKeyChecks) {
await knex.raw('SET FOREIGN_KEY_CHECKS=1;').transacting(knex);
}
}
}
}
);
}
/**
* @param {string} table
* @param {string} from
* @param {string} to
*
* @returns {Migration}
*/
function createRenameColumnMigration(table, from, to) {
return createNonTransactionalMigration(
async function up(knex) {
const hasColumn = await knex.schema.hasColumn(table, to);
if (hasColumn) {
logging.warn(`Renaming ${table}.${from} to ${table}.${to} column - skipping as column ${table}.${to} already exists`);
} else {
await commands.renameColumn(table, from, to, knex);
}
},
async function down(knex) {
const hasColumn = await knex.schema.hasColumn(table, from);
if (hasColumn) {
logging.warn(`Renaming ${table}.${to} to ${table}.${from} column - skipping as column ${table}.${from} already exists`);
} else {
await commands.renameColumn(table, to, from, knex);
}
}
);
}
@ -134,7 +162,8 @@ module.exports = {
createAddColumnMigration,
createDropColumnMigration,
createSetNullableMigration,
createDropNullableMigration
createDropNullableMigration,
createRenameColumnMigration
};
/**

View File

@ -0,0 +1,3 @@
const {createRenameColumnMigration} = require('../../utils');
module.exports = createRenameColumnMigration('recommendations', 'reason', 'description');

View File

@ -156,6 +156,25 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec =
}
}
/**
* @param {string} tableName
* @param {string} from
* @param {string} to
* @param {import('knex').Knex.Transaction} [transaction]
*/
async function renameColumn(tableName, from, to, transaction = db.knex) {
logging.info(`Renaming column '${from}' to '${to}' in table '${tableName}'`);
if (DatabaseInfo.isMySQL(transaction)) {
// The knex helper does a lot of interesting things with foreign keys that are slow on bigger MySQL clusters
return await transaction.raw(`ALTER TABLE \`${tableName}\` RENAME COLUMN \`${from}\` TO \`${to}\`;`);
}
return await transaction.schema.table(tableName, function (table) {
table.renameColumn(from, to);
});
}
/**
* Adds an unique index to a table over the given columns.
*
@ -520,6 +539,7 @@ module.exports = {
addForeign,
dropForeign,
addColumn,
renameColumn,
dropColumn,
setNullable,
dropNullable,

View File

@ -1076,7 +1076,7 @@ module.exports = {
excerpt: {type: 'string', maxlength: 2000, nullable: true},
featured_image: {type: 'string', maxlength: 2000, nullable: true},
favicon: {type: 'string', maxlength: 2000, nullable: true},
reason: {type: 'string', maxlength: 2000, nullable: true},
description: {type: 'string', maxlength: 2000, nullable: true},
one_click_subscribe: {type: 'boolean', nullable: false, defaultTo: false},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}

View File

@ -1150,12 +1150,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Because dogs are cute",
"excerpt": "Dogs are cute",
"favicon": "https://dogpictures.com/favicon.ico",
"featured_image": "https://dogpictures.com/dog.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Because dogs are cute",
"title": "Dog Pictures",
"updated_at": null,
"url": "https://dogpictures.com/",
@ -1168,7 +1168,7 @@ exports[`Recommendations Admin API add Can add a full recommendation 2: [headers
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "354",
"content-length": "359",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1184,12 +1184,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": null,
"title": "Dog Pictures",
"updated_at": null,
"url": "https://dogpictures.com/",
@ -1202,7 +1202,7 @@ exports[`Recommendations Admin API add Can add a minimal recommendation 2: [head
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "263",
"content-length": "268",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1218,12 +1218,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": null,
"title": "Recommendation 3 with a different path",
"updated_at": null,
"url": "https://recommendation3.com/path-1",
@ -1236,7 +1236,7 @@ exports[`Recommendations Admin API add Can add a recommendation with the same ho
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "299",
"content-length": "304",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1335,12 +1335,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1353,7 +1353,7 @@ exports[`Recommendations Admin API browse Can browse 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "470",
"content-length": "480",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1410,12 +1410,12 @@ Object {
"subscribers": 3,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
@ -1426,12 +1426,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
@ -1442,12 +1442,12 @@ Object {
"subscribers": 2,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -1458,12 +1458,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -1474,12 +1474,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1492,7 +1492,7 @@ exports[`Recommendations Admin API browse Can include click and subscribe counts
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2103",
"content-length": "2153",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1520,12 +1520,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
@ -1536,12 +1536,12 @@ Object {
"subscribers": 3,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
@ -1552,12 +1552,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1568,12 +1568,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -1584,12 +1584,12 @@ Object {
"subscribers": 2,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -1602,7 +1602,7 @@ exports[`Recommendations Admin API browse Can include click and subscribe counts
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2103",
"content-length": "2153",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1629,12 +1629,12 @@ Object {
"clicks": 2,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
@ -1644,12 +1644,12 @@ Object {
"clicks": 3,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
@ -1659,12 +1659,12 @@ Object {
"clicks": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -1674,12 +1674,12 @@ Object {
"clicks": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -1689,12 +1689,12 @@ Object {
"clicks": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1707,7 +1707,7 @@ exports[`Recommendations Admin API browse Can include only clicks 2: [headers] 1
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2023",
"content-length": "2073",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1734,12 +1734,12 @@ Object {
"subscribers": 3,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
@ -1749,12 +1749,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
@ -1764,12 +1764,12 @@ Object {
"subscribers": 2,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -1779,12 +1779,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -1794,12 +1794,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1812,7 +1812,7 @@ exports[`Recommendations Admin API browse Can include only subscribers 2: [heade
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2048",
"content-length": "2098",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1840,12 +1840,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
@ -1856,12 +1856,12 @@ Object {
"subscribers": 3,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
@ -1872,12 +1872,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -1888,12 +1888,12 @@ Object {
"subscribers": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -1904,12 +1904,12 @@ Object {
"subscribers": 2,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -1922,7 +1922,7 @@ exports[`Recommendations Admin API browse Can order by click and subscribe count
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "2103",
"content-length": "2153",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1946,120 +1946,120 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 14",
"excerpt": "Test excerpt",
"favicon": "https://recommendation14.com/favicon.ico",
"featured_image": "https://recommendation14.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 14",
"title": "Recommendation 14",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation14.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 13",
"excerpt": "Test excerpt",
"favicon": "https://recommendation13.com/favicon.ico",
"featured_image": "https://recommendation13.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 13",
"title": "Recommendation 13",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation13.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 12",
"excerpt": "Test excerpt",
"favicon": "https://recommendation12.com/favicon.ico",
"featured_image": "https://recommendation12.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 12",
"title": "Recommendation 12",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation12.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 11",
"excerpt": "Test excerpt",
"favicon": "https://recommendation11.com/favicon.ico",
"featured_image": "https://recommendation11.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 11",
"title": "Recommendation 11",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation11.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 10",
"excerpt": "Test excerpt",
"favicon": "https://recommendation10.com/favicon.ico",
"featured_image": "https://recommendation10.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 10",
"title": "Recommendation 10",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation10.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 9",
"excerpt": "Test excerpt",
"favicon": "https://recommendation9.com/favicon.ico",
"featured_image": "https://recommendation9.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 9",
"title": "Recommendation 9",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation9.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 8",
"excerpt": "Test excerpt",
"favicon": "https://recommendation8.com/favicon.ico",
"featured_image": "https://recommendation8.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 8",
"title": "Recommendation 8",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation8.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 7",
"excerpt": "Test excerpt",
"favicon": "https://recommendation7.com/favicon.ico",
"featured_image": "https://recommendation7.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 7",
"title": "Recommendation 7",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation7.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 6",
"excerpt": "Test excerpt",
"favicon": "https://recommendation6.com/favicon.ico",
"featured_image": "https://recommendation6.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 6",
"title": "Recommendation 6",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation6.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 5",
"excerpt": "Test excerpt",
"favicon": "https://recommendation5.com/favicon.ico",
"featured_image": "https://recommendation5.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 5",
"title": "Recommendation 5",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation5.com/",
@ -2072,7 +2072,7 @@ exports[`Recommendations Admin API browse Can request pages 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3752",
"content-length": "3852",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2096,60 +2096,60 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": "Test excerpt",
"favicon": "https://recommendation4.com/favicon.ico",
"featured_image": "https://recommendation4.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": "Test excerpt",
"favicon": "https://recommendation3.com/favicon.ico",
"featured_image": "https://recommendation3.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": "Test excerpt",
"favicon": "https://recommendation2.com/favicon.ico",
"featured_image": "https://recommendation2.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -2162,7 +2162,7 @@ exports[`Recommendations Admin API browse Can request pages 4: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1917",
"content-length": "1967",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2175,7 +2175,7 @@ exports[`Recommendations Admin API browse Uses default limit of 5 1: [headers] 1
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1915",
"content-length": "1965",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2203,12 +2203,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Because cats are cute",
"excerpt": "Cats are cute",
"favicon": "https://catpictures.com/favicon.ico",
"featured_image": "https://catpictures.com/cat.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Because cats are cute",
"title": "Cat Pictures",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://dogpictures.com/",
@ -2221,7 +2221,7 @@ exports[`Recommendations Admin API edit Can edit recommendation 2: [headers] 1`]
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "377",
"content-length": "382",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2236,12 +2236,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": null,
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -2254,7 +2254,7 @@ exports[`Recommendations Admin API edit Can edit recommendation and set nullable
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "292",
"content-length": "297",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2269,12 +2269,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": "Test excerpt",
"favicon": "https://recommendation0.com/favicon.ico",
"featured_image": "https://recommendation0.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 0",
"title": "Changed",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -2287,7 +2287,7 @@ exports[`Recommendations Admin API edit Can edit some fields of a recommendation
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "374",
"content-length": "384",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -2333,12 +2333,12 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": "Test excerpt",
"favicon": "https://recommendation1.com/favicon.ico",
"featured_image": "https://recommendation1.com/featured.jpg",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": true,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
@ -2351,7 +2351,7 @@ exports[`Recommendations Admin API read can get a recommendation by ID 2: [heade
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "383",
"content-length": "393",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -7,7 +7,7 @@ const {Recommendation, ClickEvent, SubscribeEvent} = require('@tryghost/recommen
async function addDummyRecommendation(i = 0) {
const recommendation = Recommendation.create({
title: `Recommendation ${i}`,
reason: `Reason ${i}`,
description: `Description ${i}`,
url: new URL(`https://recommendation${i}.com`),
favicon: new URL(`https://recommendation${i}.com/favicon.ico`),
featuredImage: new URL(`https://recommendation${i}.com/featured.jpg`),
@ -357,7 +357,7 @@ describe('Recommendations Admin API', function () {
assert.equal(body.recommendations[0].id, id);
assert.equal(body.recommendations[0].title, 'Recommendation 1');
assert.equal(body.recommendations[0].url, 'https://recommendation1.com/');
assert.equal(body.recommendations[0].reason, 'Reason 1');
assert.equal(body.recommendations[0].description, 'Description 1');
assert.equal(body.recommendations[0].excerpt, 'Test excerpt');
assert.equal(body.recommendations[0].featured_image, 'https://recommendation1.com/featured.jpg');
assert.equal(body.recommendations[0].favicon, 'https://recommendation1.com/favicon.ico');
@ -393,7 +393,7 @@ describe('Recommendations Admin API', function () {
recommendations: [{
title: 'Cat Pictures',
url: 'https://dogpictures.com',
reason: 'Because cats are cute',
description: 'Because cats are cute',
excerpt: 'Cats are cute',
featured_image: 'https://catpictures.com/cat.jpg',
favicon: 'https://catpictures.com/favicon.ico',
@ -419,7 +419,7 @@ describe('Recommendations Admin API', function () {
assert.equal(body.recommendations[0].id, id);
assert.equal(body.recommendations[0].title, 'Cat Pictures');
assert.equal(body.recommendations[0].url, 'https://dogpictures.com/');
assert.equal(body.recommendations[0].reason, 'Because cats are cute');
assert.equal(body.recommendations[0].description, 'Because cats are cute');
assert.equal(body.recommendations[0].excerpt, 'Cats are cute');
assert.equal(body.recommendations[0].featured_image, 'https://catpictures.com/cat.jpg');
assert.equal(body.recommendations[0].favicon, 'https://catpictures.com/favicon.ico');
@ -431,7 +431,7 @@ describe('Recommendations Admin API', function () {
const {body} = await agent.put(`recommendations/${id}/`)
.body({
recommendations: [{
reason: null,
description: null,
excerpt: null,
featured_image: null,
favicon: null
@ -454,7 +454,7 @@ describe('Recommendations Admin API', function () {
// Check everything is set correctly
assert.equal(body.recommendations[0].id, id);
assert.equal(body.recommendations[0].reason, null);
assert.equal(body.recommendations[0].description, null);
assert.equal(body.recommendations[0].excerpt, null);
assert.equal(body.recommendations[0].featured_image, null);
assert.equal(body.recommendations[0].favicon, null);
@ -487,7 +487,7 @@ describe('Recommendations Admin API', function () {
assert.equal(body.recommendations[0].id, id);
assert.equal(body.recommendations[0].title, 'Changed');
assert.equal(body.recommendations[0].url, 'https://recommendation0.com/');
assert.equal(body.recommendations[0].reason, 'Reason 0');
assert.equal(body.recommendations[0].description, 'Description 0');
assert.equal(body.recommendations[0].excerpt, 'Test excerpt');
assert.equal(body.recommendations[0].featured_image, 'https://recommendation0.com/featured.jpg');
assert.equal(body.recommendations[0].favicon, 'https://recommendation0.com/favicon.ico');
@ -502,7 +502,7 @@ describe('Recommendations Admin API', function () {
recommendations: [{
title: 'Cat Pictures',
url: 'https://dogpictures.com',
reason: 'Because cats are cute',
description: 'Because cats are cute',
excerpt: 'Cats are cute',
featured_image: 'ftp://dogpictures.com/dog.jpg',
favicon: 'ftp://dogpictures.com/favicon.ico',
@ -551,7 +551,7 @@ describe('Recommendations Admin API', function () {
// Check everything is set correctly
assert.equal(body.recommendations[0].title, 'Dog Pictures');
assert.equal(body.recommendations[0].url, 'https://dogpictures.com/');
assert.equal(body.recommendations[0].reason, null);
assert.equal(body.recommendations[0].description, null);
assert.equal(body.recommendations[0].excerpt, null);
assert.equal(body.recommendations[0].featured_image, null);
assert.equal(body.recommendations[0].favicon, null);
@ -564,7 +564,7 @@ describe('Recommendations Admin API', function () {
recommendations: [{
title: 'Dog Pictures',
url: 'https://dogpictures.com',
reason: 'Because dogs are cute',
description: 'Because dogs are cute',
excerpt: 'Dogs are cute',
featured_image: 'https://dogpictures.com/dog.jpg',
favicon: 'https://dogpictures.com/favicon.ico',
@ -589,7 +589,7 @@ describe('Recommendations Admin API', function () {
// Check everything is set correctly
assert.equal(body.recommendations[0].title, 'Dog Pictures');
assert.equal(body.recommendations[0].url, 'https://dogpictures.com/');
assert.equal(body.recommendations[0].reason, 'Because dogs are cute');
assert.equal(body.recommendations[0].description, 'Because dogs are cute');
assert.equal(body.recommendations[0].excerpt, 'Dogs are cute');
assert.equal(body.recommendations[0].featured_image, 'https://dogpictures.com/dog.jpg');
assert.equal(body.recommendations[0].favicon, 'https://dogpictures.com/favicon.ico');

View File

@ -15,60 +15,60 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 6",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 6",
"title": "Recommendation 6",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation6.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 5",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 5",
"title": "Recommendation 5",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation5.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -81,7 +81,7 @@ exports[`Recommendations Content API Can paginate recommendations 2: [headers] 1
Object {
"access-control-allow-origin": "*",
"cache-control": "public, max-age=0",
"content-length": "1495",
"content-length": "1545",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -105,24 +105,24 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 1",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 1",
"title": "Recommendation 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation1.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 0",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 0",
"title": "Recommendation 0",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation0.com/",
@ -135,7 +135,7 @@ exports[`Recommendations Content API Can paginate recommendations 4: [headers] 1
Object {
"access-control-allow-origin": "*",
"cache-control": "public, max-age=0",
"content-length": "661",
"content-length": "681",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -159,60 +159,60 @@ Object {
"recommendations": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 6",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 6",
"title": "Recommendation 6",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation6.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 5",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 5",
"title": "Recommendation 5",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation5.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 4",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 4",
"title": "Recommendation 4",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation4.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 3",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 3",
"title": "Recommendation 3",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation3.com/",
},
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": "Description 2",
"excerpt": null,
"favicon": null,
"featured_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"one_click_subscribe": false,
"reason": "Reason 2",
"title": "Recommendation 2",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": "https://recommendation2.com/",
@ -225,7 +225,7 @@ exports[`Recommendations Content API Does not allow includes 2: [headers] 1`] =
Object {
"access-control-allow-origin": "*",
"cache-control": "public, max-age=0",
"content-length": "1495",
"content-length": "1545",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -22,7 +22,7 @@ describe('Recommendations Content API', function () {
for (let i = 0; i < 7; i++) {
const recommendation = Recommendation.create({
title: `Recommendation ${i}`,
reason: `Reason ${i}`,
description: `Description ${i}`,
url: new URL(`https://recommendation${i}.com`),
favicon: null,
featuredImage: null,

View File

@ -74,7 +74,7 @@ describe('Recommendation Event Tracking', function () {
// Add recommendation
const recommendation = Recommendation.create({
title: `Recommendation`,
reason: `Reason`,
description: `Description`,
url: new URL(`https://recommendation.com`),
favicon: null,
featuredImage: null,

View File

@ -1,5 +1,244 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Incoming Recommendation Emails Sends a different email if we receive a recommendation back 1: [html 1] 1`] = `
"<!doctype html>
<html>
<head>
<meta name=\\"viewport\\" content=\\"width=device-width\\">
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
<title>👍 New recommendation</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
table[class=body] p[class=small],
table[class=body] a[class=small] {
font-size: 11px !important;
}
.new-mention-thumbnail {
display: none !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
/* Reset styles for Gmail (it wraps email address in link with custom styles) */
.text-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
hr {
border-width: 0;
height: 0;
margin-top: 34px;
margin-bottom: 34px;
border-bottom-width: 1px;
border-bottom-color: #EEF5F8;
}
a {
color: #15212A;
}
blockquote {
margin-left: 0;
padding-left: 20px;
border-left: 3px solid #DDE1E5;
}
</style>
</head>
<body style=\\"background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;\\">
<table border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" class=\\"body\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\\">
<tr>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;\\">&nbsp;</td>
<td class=\\"container\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; padding: 10px; width: 540px;\\">
<div class=\\"content\\" style=\\"box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;\\">
<!-- START CENTERED CONTAINER -->
<table class=\\"main\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;\\">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class=\\"wrapper\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;\\">
<table border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\\">
<tr>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;\\">
<p style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;\\">Great news!</p>
<p style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 16px;\\">One of the sites you're recommending is now <strong>recommending you back</strong>:</p>
<!--[if !mso !vml]-->
<figure style=\\"margin:0 0 1.5em;padding:0;width:100%;\\">
<a style=\\"display:flex;min-height:110px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;background:#F9F9FA;border-radius:3px;border:1px solid #F9F9FA;color:#15171a;text-decoration:none\\" href=\\"https://www.otherghostsite.com/\\">
<div style=\\"display:inline-block; width:100%; padding:20px\\">
<div style=\\"display:flex;color:#15212a;font-size:13px;font-weight:400\\">
<div style=\\"color:#15212a;font-size:16px;line-height:1.3em;font-weight:600\\">Other Ghost Site</div>
</div>
<div class=\\"kg-bookmark-description\\" style=\\"display:-webkit-box;overflow-y:hidden;margin-top:12px;max-height:40px;color:#738a94;font-size:13px;line-height:1.5em;font-weight:400;-webkit-line-clamp: 2; -webkit-box-orient: vertical;\\"></div>
</div>
</a>
</figure>
<!--[endif]-->
<!--[if vml]>
<table style=\\"border-collapse: collapse; border-spacing: 0; margin: 0; padding: 0; width: 100%; border: 1px solid #f9f9fa; background: #f9f9fa; font-family: -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Oxygen, Ubuntu, Cantarell, \\"Open Sans\\", \\"Helvetica Neue\\", sans-serif;\\">
<tr>
<td width=\\"100%\\" style=\\"padding: 20px;background-color:#F9F9FA;\\">
<table style=\\"margin: 0; padding: 0; border-collapse: collapse; border-spacing: 0;\\">
<tr>
<td style=\\"padding-bottom: 12px;\\">
<table style=\\"margin: 0; padding: 0; border-collapse: collapse; border-spacing: 0;\\">
<tr>
<td><a style=\\"color: #15212A; font-size: 15px; line-height: 1.5em; font-weight: 600; text-decoration: none;\\" href=\\"\\"></a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td><div><a href=\\"\\" style=\\"margin-top: 12px; color: #738a94; font-size: 13px; line-height: 1.5em; font-weight: 400;text-decoration: none;\\"></a></div></td>
</tr>
</table>
</td>
</tr>
</table>
<![endif]-->
<table border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" class=\\"btn btn-primary\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;\\">
<tbody>
<tr>
<td align=\\"left\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;\\">
<table border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;\\">
<tbody>
<tr>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: #15171a; border-radius: 5px; text-align: center;\\"> <a href=\\"http://127.0.0.1:2369/ghost/#/settings-x/recommendations\\" target=\\"_blank\\" style=\\"display: inline-block; color: #ffffff; background-color: #15171a; border: solid 1px #15171a; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #15171a;\\">View recommendations</a></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<hr/>
<p style=\\"word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;\\">You can also copy & paste this URL into your browser:</p>
<p class=\\"text-link\\" style=\\"word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;\\">http://127.0.0.1:2369/ghost/#/settings-x/recommendations</p>
</td>
</tr>
<!-- START FOOTER -->
<tr>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;\\">
<p class=\\"small\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;\\">This message was sent from <a class=\\"small\\" href=\\"http://127.0.0.1:2369/\\" style=\\"text-decoration: underline; color: #738A94; font-size: 11px;\\">127.0.0.1</a> to <a class=\\"small\\" href=\\"mailto:jbloggs@example.com\\" style=\\"text-decoration: underline; color: #738A94; font-size: 11px;\\">jbloggs@example.com</a></p>
</td>
</tr>
<tr>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px\\">
<p class=\\"small\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;\\">Dont want to receive these emails? Manage your preferences <a class=\\"small\\" href=\\"http://127.0.0.1:2369/ghost/#/settings-x/users/show/joe-bloggs\\" style=\\"text-decoration: underline; color: #738A94; font-size: 11px;\\">here</a>.</p>
</td>
</tr>
<!-- END FOOTER -->
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED CONTAINER -->
</div>
</td>
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;\\">&nbsp;</td>
</tr>
</table>
</body>
</html>
"
`;
exports[`Incoming Recommendation Emails Sends a different email if we receive a recommendation back 2: [text 1] 1`] = `
"
You have been recommended by Other Ghost Site.
---
Sent to jbloggs@example.com from 127.0.0.1.
If you would no longer like to receive these notifications you can adjust your settings at http://127.0.0.1:2369/ghost/#/settings-x/users/show/joe-bloggs.
"
`;
exports[`Incoming Recommendation Emails Sends a different email if we receive a recommendation back 3: [metadata 1] 1`] = `
Object {
"subject": "👍 New recommendation: Other Ghost Site",
"to": "jbloggs@example.com",
}
`;
exports[`Incoming Recommendation Emails Sends an email if we receive a recommendation 1: [html 1] 1`] = `
"<!doctype html>
<html>

View File

@ -73,7 +73,7 @@ describe('Incoming Recommendation Emails', function () {
// Create a recommendation to otherghostsite.com
const recommendation = Recommendation.create({
title: `Recommendation`,
reason: `Reason`,
description: `Description`,
url: new URL(`https://www.otherghostsite.com/`),
favicon: null,
featuredImage: null,

View File

@ -41,11 +41,11 @@ describe('{{#recommendations}} helper', function () {
sinon.stub(api, 'recommendationsPublic').get(() => {
return {
browse: sinon.stub().resolves({recommendations: [
{id: '1', title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', reason: 'Reason 1'},
{id: '2', title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', reason: 'Reason 2'},
{id: '3', title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', reason: 'Reason 3'},
{id: '4', title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', reason: 'Reason 4'},
{id: '5', title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', reason: 'Reason 5'}
{id: '1', title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', description: 'Description 1'},
{id: '2', title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', description: 'Description 2'},
{id: '3', title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', description: 'Description 3'},
{id: '4', title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', description: 'Description 4'},
{id: '5', title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', description: 'Description 5'}
], meta: meta})
};
});
@ -74,35 +74,35 @@ describe('{{#recommendations}} helper', function () {
<a href="https://recommendations1.com" data-recommendation="1" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations1.com/favicon.ico" alt="Recommendation 1">
<h5 class="recommendation-title">Recommendation 1</h5>
<p class="recommendation-reason">Reason 1</p>
<p class="recommendation-description">Description 1</p>
</a>
</li>
<li class="recommendation">
<a href="https://recommendations2.com" data-recommendation="2" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations2.com/favicon.ico" alt="Recommendation 2">
<h5 class="recommendation-title">Recommendation 2</h5>
<p class="recommendation-reason">Reason 2</p>
<p class="recommendation-description">Description 2</p>
</a>
</li>
<li class="recommendation">
<a href="https://recommendations3.com" data-recommendation="3" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations3.com/favicon.ico" alt="Recommendation 3">
<h5 class="recommendation-title">Recommendation 3</h5>
<p class="recommendation-reason">Reason 3</p>
<p class="recommendation-description">Description 3</p>
</a>
</li>
<li class="recommendation">
<a href="https://recommendations4.com" data-recommendation="4" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations4.com/favicon.ico" alt="Recommendation 4">
<h5 class="recommendation-title">Recommendation 4</h5>
<p class="recommendation-reason">Reason 4</p>
<p class="recommendation-description">Description 4</p>
</a>
</li>
<li class="recommendation">
<a href="https://recommendations5.com" data-recommendation="5" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations5.com/favicon.ico" alt="Recommendation 5">
<h5 class="recommendation-title">Recommendation 5</h5>
<p class="recommendation-reason">Reason 5</p>
<p class="recommendation-description">Description 5</p>
</a>
</li>
</ul>

View File

@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
*/
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '1b75aae9befefea53b17c9c1991c8a1d';
const currentSchemaHash = '013334f4f51aae785b9afd345861c06a';
const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d';
const currentSettingsHash = '3128d4ec667a50049486b0c21f04be07';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';

View File

@ -45,7 +45,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
return {
id: entity.id,
title: entity.title,
reason: entity.reason,
description: entity.description,
excerpt: entity.excerpt,
featured_image: entity.featuredImage?.toString(),
favicon: entity.favicon?.toString(),
@ -62,7 +62,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
return Recommendation.create({
id: model.id,
title: model.get('title') as string,
reason: model.get('reason') as string | null,
description: model.get('description') as string | null,
excerpt: model.get('excerpt') as string | null,
featuredImage: model.get('featured_image') as string | null,
favicon: model.get('favicon') as string | null,
@ -84,7 +84,7 @@ export class BookshelfRecommendationRepository extends BookshelfRepository<strin
return {
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featuredImage: 'featured_image',
favicon: 'favicon',

View File

@ -8,7 +8,7 @@ import {UnsafeData} from './UnsafeData';
export type RecommendationPlain = {
id: string,
title: string
reason: string|null
description: string|null
excerpt: string|null // Fetched from the site meta data
featuredImage: URL|null // Fetched from the site meta data
favicon: URL|null // Fetched from the site meta data
@ -27,7 +27,7 @@ export type RecommendationPlain = {
export type RecommendationCreateData = {
id?: string
title: string
reason: string|null
description: string|null
excerpt: string|null // Fetched from the site meta data
featuredImage: URL|string|null // Fetched from the site meta data
favicon: URL|string|null // Fetched from the site meta data
@ -49,7 +49,7 @@ export type EditRecommendation = Partial<AddRecommendation>
export class Recommendation {
id: string;
title: string;
reason: string|null;
description: string|null;
excerpt: string|null; // Fetched from the site meta data
featuredImage: URL|null; // Fetched from the site meta data
favicon: URL|null; // Fetched from the site meta data
@ -77,7 +77,7 @@ export class Recommendation {
private constructor(data: RecommendationPlain) {
this.id = data.id;
this.title = data.title;
this.reason = data.reason;
this.description = data.description;
this.excerpt = data.excerpt;
this.featuredImage = data.featuredImage;
this.favicon = data.favicon;
@ -103,9 +103,9 @@ export class Recommendation {
});
}
if (properties.reason && properties.reason.length > 2000) {
if (properties.description && properties.description.length > 2000) {
throw new errors.ValidationError({
message: 'Reason must be less than 2000 characters'
message: 'Description must be less than 2000 characters'
});
}
@ -117,8 +117,8 @@ export class Recommendation {
}
clean() {
if (this.reason !== null && this.reason.length === 0) {
this.reason = null;
if (this.description !== null && this.description.length === 0) {
this.description = null;
}
this.url = this.cleanURL(this.url);
@ -139,7 +139,7 @@ export class Recommendation {
const d = {
id,
title: data.title,
reason: data.reason,
description: data.description,
excerpt: data.excerpt,
featuredImage: new UnsafeData(data.featuredImage, {field: ['featuredImage']}).nullable.url,
favicon: new UnsafeData(data.favicon, {field: ['favicon']}).nullable.url,
@ -162,7 +162,7 @@ export class Recommendation {
return {
id: this.id,
title: this.title,
reason: this.reason,
description: this.description,
excerpt: this.excerpt,
featuredImage: this.featuredImage,
favicon: this.favicon,

View File

@ -18,7 +18,7 @@ const RecommendationIncludesMap = {
const RecommendationOrderMap = {
title: 'title' as const,
reason: 'reason' as const,
description: 'description' as const,
excerpt: 'excerpt' as const,
one_click_subscribe: 'oneClickSubscribe' as const,
created_at: 'createdAt' as const,
@ -54,7 +54,7 @@ export class RecommendationController {
// Optional fields
oneClickSubscribe: recommendation.optionalKey('one_click_subscribe')?.boolean ?? false,
reason: recommendation.optionalKey('reason')?.nullable.string ?? null,
description: recommendation.optionalKey('description')?.nullable.string ?? null,
excerpt: recommendation.optionalKey('excerpt')?.nullable.string ?? null,
featuredImage: recommendation.optionalKey('featured_image')?.nullable.url ?? null,
favicon: recommendation.optionalKey('favicon')?.nullable.url ?? null
@ -75,7 +75,7 @@ export class RecommendationController {
title: recommendation.optionalKey('title')?.string,
url: recommendation.optionalKey('url')?.url,
oneClickSubscribe: recommendation.optionalKey('one_click_subscribe')?.boolean,
reason: recommendation.optionalKey('reason')?.nullable.string,
description: recommendation.optionalKey('description')?.nullable.string,
excerpt: recommendation.optionalKey('excerpt')?.nullable.string,
featuredImage: recommendation.optionalKey('featured_image')?.nullable.url,
favicon: recommendation.optionalKey('favicon')?.nullable.url
@ -207,7 +207,7 @@ export class RecommendationController {
const d = {
id: entity.id,
title: entity.title,
reason: entity.reason,
description: entity.description,
excerpt: entity.excerpt,
featured_image: entity.featuredImage?.toString() ?? null,
favicon: entity.favicon?.toString() ?? null,

View File

@ -15,7 +15,7 @@ describe('BookshelfRecommendationRepository', function () {
repository.toPrimitive(Recommendation.create({
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featuredImage: new URL('https://example.com'),
favicon: new URL('https://example.com'),
@ -27,7 +27,7 @@ describe('BookshelfRecommendationRepository', function () {
{
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featured_image: 'https://example.com/',
favicon: 'https://example.com/',
@ -48,7 +48,7 @@ describe('BookshelfRecommendationRepository', function () {
get: (key: string) => {
return {
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featured_image: 'https://example.com/',
favicon: 'https://example.com/',
@ -65,7 +65,7 @@ describe('BookshelfRecommendationRepository', function () {
Recommendation.create({
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featuredImage: new URL('https://example.com'),
favicon: new URL('https://example.com'),
@ -121,7 +121,7 @@ describe('BookshelfRecommendationRepository', function () {
const recommendation = Recommendation.create({
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featuredImage: new URL('https://example.com'),
favicon: new URL('https://example.com'),
@ -149,7 +149,7 @@ describe('BookshelfRecommendationRepository', function () {
const recommendation = Recommendation.create({
id: 'id',
title: 'title',
reason: 'reason',
description: 'description',
excerpt: 'excerpt',
featuredImage: new URL('https://example.com'),
favicon: new URL('https://example.com'),

View File

@ -7,7 +7,7 @@ describe('Recommendation', function () {
assert.throws(() => {
Recommendation.validate({
title: '',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -24,7 +24,7 @@ describe('Recommendation', function () {
assert.throws(() => {
Recommendation.validate({
title: 'a'.repeat(2001),
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -37,11 +37,11 @@ describe('Recommendation', function () {
});
});
it('Throws for a long reason', function () {
it('Throws for a long description', function () {
assert.throws(() => {
Recommendation.validate({
title: 'Test',
reason: 'a'.repeat(2001),
description: 'a'.repeat(2001),
excerpt: null,
featuredImage: null,
favicon: null,
@ -50,7 +50,7 @@ describe('Recommendation', function () {
});
}, {
name: 'ValidationError',
message: 'Reason must be less than 2000 characters'
message: 'Description must be less than 2000 characters'
});
});
@ -58,7 +58,7 @@ describe('Recommendation', function () {
assert.throws(() => {
Recommendation.validate({
title: 'Test',
reason: null,
description: null,
excerpt: 'a'.repeat(2001),
featuredImage: null,
favicon: null,
@ -76,7 +76,7 @@ describe('Recommendation', function () {
it('sets createdAt ms to 0', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -91,7 +91,7 @@ describe('Recommendation', function () {
it('sets updatedAt ms to 0', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -103,10 +103,10 @@ describe('Recommendation', function () {
assert.equal(recommendation.updatedAt!.getMilliseconds(), 0);
});
it('sets empty reason to null', function () {
it('sets empty description to null', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: '',
description: '',
excerpt: null,
featuredImage: null,
favicon: null,
@ -115,13 +115,13 @@ describe('Recommendation', function () {
updatedAt: new Date('2021-01-01T00:00:05Z')
});
assert.equal(recommendation.reason, null);
assert.equal(recommendation.description, null);
});
it('removes search and hash params', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: '',
description: '',
excerpt: null,
featuredImage: null,
favicon: null,
@ -138,7 +138,7 @@ describe('Recommendation', function () {
it('does not return instance of self', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -155,7 +155,7 @@ describe('Recommendation', function () {
it('can edit known properties', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -176,7 +176,7 @@ describe('Recommendation', function () {
it('can not edit unknown properties', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -199,7 +199,7 @@ describe('Recommendation', function () {
it('can delete', function () {
const recommendation = Recommendation.create({
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,

View File

@ -17,7 +17,7 @@ describe('RecommendationController', function () {
return {
id,
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: new URL('https://example.com/image.png'),
favicon: new URL('https://example.com/favicon.ico'),
@ -40,7 +40,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -61,7 +61,7 @@ describe('RecommendationController', function () {
return {
id: '1',
title: plain.title,
reason: plain.reason,
description: plain.description,
excerpt: plain.excerpt,
featuredImage: plain.featuredImage ? new URL(plain.featuredImage.toString()) : null,
favicon: plain.favicon ? new URL(plain.favicon.toString()) : null,
@ -77,7 +77,7 @@ describe('RecommendationController', function () {
recommendations: [
{
title: 'Test',
reason: 'My reason',
description: 'My description',
excerpt: 'My excerpt',
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -94,7 +94,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: 'My reason',
description: 'My description',
excerpt: 'My excerpt',
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -113,7 +113,7 @@ describe('RecommendationController', function () {
return {
id: '1',
title: plain.title,
reason: plain.reason,
description: plain.description,
excerpt: plain.excerpt,
featuredImage: plain.featuredImage ? new URL(plain.featuredImage.toString()) : null,
favicon: plain.favicon ? new URL(plain.favicon.toString()) : null,
@ -141,7 +141,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featured_image: null,
favicon: null,
@ -162,7 +162,7 @@ describe('RecommendationController', function () {
return {
id: '1',
title: edit.title || 'Test',
reason: edit.reason || null,
description: edit.description || null,
excerpt: edit.excerpt || null,
featuredImage: edit.featuredImage ? new URL(edit.featuredImage.toString()) : null,
favicon: edit.favicon ? new URL(edit.favicon.toString()) : null,
@ -191,7 +191,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featured_image: null,
favicon: null,
@ -210,7 +210,7 @@ describe('RecommendationController', function () {
return {
id: '1',
title: edit.title || 'Test',
reason: edit.reason || null,
description: edit.description || null,
excerpt: edit.excerpt || null,
featuredImage: edit.featuredImage ? new URL(edit.featuredImage.toString()) : null,
favicon: edit.favicon ? new URL(edit.favicon.toString()) : null,
@ -226,7 +226,7 @@ describe('RecommendationController', function () {
recommendations: [
{
// All execpt title
reason: 'My reason',
description: 'My description',
excerpt: 'My excerpt',
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -245,7 +245,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: 'My reason',
description: 'My description',
excerpt: 'My excerpt',
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -285,7 +285,7 @@ describe('RecommendationController', function () {
{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: new URL('https://example.com/image.png'),
favicon: new URL('https://example.com/favicon.ico'),
@ -312,7 +312,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -341,7 +341,7 @@ describe('RecommendationController', function () {
{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: new URL('https://example.com/image.png'),
favicon: new URL('https://example.com/favicon.ico'),
@ -369,7 +369,7 @@ describe('RecommendationController', function () {
data: [{
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featured_image: 'https://example.com/image.png',
favicon: 'https://example.com/favicon.ico',
@ -489,7 +489,7 @@ describe('RecommendationController', function () {
user: {}
}),
{
message: 'order.0.field must be one of title, reason, excerpt, one_click_subscribe, created_at, updated_at, count.clicks, count.subscribers'
message: 'order.0.field must be one of title, description, excerpt, one_click_subscribe, created_at, updated_at, count.clicks, count.subscribers'
}
);
});
@ -515,7 +515,7 @@ describe('RecommendationController', function () {
let rec = {
id: '1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: new URL('https://example.com/image.png'),
favicon: new URL('https://example.com/favicon.ico'),

View File

@ -62,7 +62,7 @@ describe('RecommendationService', function () {
Recommendation.create({
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -78,7 +78,7 @@ describe('RecommendationService', function () {
Recommendation.create({
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -114,7 +114,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -136,7 +136,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -147,7 +147,7 @@ describe('RecommendationService', function () {
await assert.rejects(() => service.addRecommendation({
url: 'http://localhost/1',
title: 'Test 2',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -162,7 +162,7 @@ describe('RecommendationService', function () {
const response = await service.addRecommendation({
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -170,7 +170,7 @@ describe('RecommendationService', function () {
});
assert.deepEqual(response, {
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -198,7 +198,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -211,7 +211,7 @@ describe('RecommendationService', function () {
await assert.doesNotReject(() => service.addRecommendation({
url: 'http://localhost/2',
title: 'Test 2',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -237,7 +237,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -250,7 +250,7 @@ describe('RecommendationService', function () {
});
assert.deepEqual(response, {
title: 'Test 2',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -289,7 +289,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -309,7 +309,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -327,7 +327,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -339,7 +339,7 @@ describe('RecommendationService', function () {
id: '3',
url: 'http://localhost/2',
title: 'Test 2',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -366,7 +366,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -378,7 +378,7 @@ describe('RecommendationService', function () {
id: '3',
url: 'http://localhost/2',
title: 'Test 2',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -399,7 +399,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -431,7 +431,7 @@ describe('RecommendationService', function () {
id: '2',
url: 'http://localhost/1',
title: 'Test',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,

View File

@ -30,7 +30,7 @@ describe('WellknownService', function () {
const recommendations = [
Recommendation.create({
title: 'My Blog',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,
@ -41,7 +41,7 @@ describe('WellknownService', function () {
}),
Recommendation.create({
title: 'My Other Blog',
reason: null,
description: null,
excerpt: null,
featuredImage: null,
favicon: null,