apostrophe 3.15.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.16.0 (2022-03-18)
4
+
5
+ ### Adds
6
+
7
+ * Offers a simple way to set a Cache-Control max-age for Apostrophe page and GET REST API responses for pieces and pages.
8
+ * API keys and bearer tokens "win" over session cookies when both are present. Since API keys and bearer tokens are explicitly added to the request at hand, it never makes sense to ignore them in favor of a cookie, which is implicit. This also simplifies automated testing.
9
+ * `data-apos-test=""` selectors for certain elements frequently selected in QA tests, such as `data-apos-test="adminBar"`.
10
+ * To speed up functional tests, an `insecurePasswords` option has been added to the login module. This option is deliberately named to discourage use for any purpose other than functional tests in which repeated password hashing would unduly limit performance. Normally password hashing is intentionally difficult to slow down brute force attacks, especially if a database is compromised.
11
+
12
+ ### Fixes
13
+
14
+ * `POST`ing a new child page with `_targetId: '_home'` now works properly in combination with `_position: 'lastChild'`.
15
+
3
16
  ## 3.15.0 (2022-03-02)
4
17
 
5
18
  ### Adds
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <div class="apos-admin-bar-wrapper" :class="themeClass">
2
+ <div data-apos-test="adminBar" class="apos-admin-bar-wrapper" :class="themeClass">
3
3
  <div class="apos-admin-bar-spacer" ref="spacer" />
4
4
  <nav class="apos-admin-bar" ref="adminBar">
5
5
  <div class="apos-admin-bar__row">
6
6
  <AposLogoPadless class="apos-admin-bar__logo" />
7
7
  <TheAposAdminBarMenu :items="items" />
8
8
  <TheAposAdminBarLocale v-if="hasLocales()" />
9
- <TheAposAdminBarUser class="apos-admin-bar__user" />
9
+ <TheAposAdminBarUser data-apos-test="authenticatedUserMenuTrigger" class="apos-admin-bar__user" />
10
10
  </div>
11
11
  <TheAposContextBar @mounted="setSpacer" />
12
12
  </nav>
@@ -0,0 +1,26 @@
1
+ /**
2
+ * If the page delivers a logged-out content but we know from session storage that a user is logged-in,
3
+ * we force-refresh the page to bypass the cache, in order to get the logged-in content (with admin UI).
4
+ */
5
+ export default function() {
6
+ const isLoggedOutPageContent = !(apos.login && apos.login.user);
7
+ const isLoggedInCookie = apos.util.getCookie(`${self.apos.shortName}.loggedIn`) === 'true';
8
+
9
+ if (!isLoggedOutPageContent || !isLoggedInCookie) {
10
+ sessionStorage.setItem('aposRefreshedPages', '{}');
11
+
12
+ return;
13
+ }
14
+
15
+ const refreshedPages = JSON.parse(sessionStorage.aposRefreshedPages || '{}');
16
+
17
+ // Avoid potential refresh loops
18
+ if (!refreshedPages[location.href]) {
19
+ refreshedPages[location.href] = true;
20
+ sessionStorage.setItem('aposRefreshedPages', JSON.stringify(refreshedPages));
21
+
22
+ console.info('Received logged-out content from cache while logged-in, refreshing the page');
23
+
24
+ location.reload();
25
+ }
26
+ };
@@ -45,6 +45,7 @@ const cuid = require('cuid');
45
45
  const expressSession = require('express-session');
46
46
 
47
47
  const loginAttemptsNamespace = '@apostrophecms/loginAttempt';
48
+ const loggedInCookieName = 'loggedIn';
48
49
 
49
50
  module.exports = {
50
51
  cascades: [ 'requirements' ],
@@ -151,6 +152,9 @@ module.exports = {
151
152
  expireCookie.expires = new Date(0);
152
153
  const name = self.apos.modules['@apostrophecms/express'].sessionOptions.name;
153
154
  req.res.header('set-cookie', expireCookie.serialize(name, 'deleted'));
155
+
156
+ // TODO: get cookie name from config
157
+ req.res.cookie(`${self.apos.shortName}.${loggedInCookieName}`, 'false');
154
158
  }
155
159
  },
156
160
  // invokes the `props(req, user)` function for the requirement specified by
@@ -789,7 +793,12 @@ module.exports = {
789
793
  },
790
794
  passportSession: {
791
795
  before: '@apostrophecms/i18n',
792
- middleware: self.passport.session()
796
+ middleware: (() => {
797
+ // Wrap the passport middleware so that if the apikey or bearer token
798
+ // middleware already supplied req.user, that wins (explicit wins over implicit)
799
+ const passportSession = self.passport.session();
800
+ return (req, res, next) => req.user ? next() : passportSession(req, res, next);
801
+ })()
793
802
  },
794
803
  honorLoginInvalidBefore: {
795
804
  before: '@apostrophecms/i18n',
@@ -812,6 +821,17 @@ module.exports = {
812
821
  return next();
813
822
  }
814
823
  }
824
+ },
825
+ addLoggedInCookie: {
826
+ before: '@apostrophecms/i18n',
827
+ middleware(req, res, next) {
828
+ // TODO: get cookie name from config
829
+ const cookieName = `${self.apos.shortName}.${loggedInCookieName}`;
830
+ if (req.user && req.cookies[cookieName] !== 'true') {
831
+ res.cookie(cookieName, 'true');
832
+ }
833
+ return next();
834
+ }
815
835
  }
816
836
  };
817
837
  }
@@ -2,6 +2,7 @@
2
2
  <transition name="fade-stage">
3
3
  <div
4
4
  class="apos-login apos-theme-dark"
5
+ data-apos-test="loginForm"
5
6
  v-show="loaded"
6
7
  :class="themeClass"
7
8
  >
@@ -35,6 +36,7 @@
35
36
  <!-- TODO -->
36
37
  <!-- <a href="#" class="apos-login__link">Forgot Password</a> -->
37
38
  <AposButton
39
+ data-apos-test="loginSubmit"
38
40
  :busy="busy"
39
41
  :disabled="disabled"
40
42
  type="primary"
@@ -177,6 +177,11 @@ module.exports = {
177
177
  return async function(req, res) {
178
178
  try {
179
179
  const result = await fn(req);
180
+
181
+ if (req.method === 'GET' && req.user) {
182
+ res.header('Cache-Control', 'no-store');
183
+ }
184
+
180
185
  res.status(200);
181
186
  res.send(result);
182
187
  } catch (err) {
@@ -446,6 +451,27 @@ module.exports = {
446
451
  );
447
452
  },
448
453
 
454
+ setMaxAge(req, maxAge) {
455
+ if (typeof maxAge !== 'number') {
456
+ self.apos.util.warnDev(`"maxAge" property must be defined as a number in the "${self.__meta.name}" module's cache options"`);
457
+ return;
458
+ }
459
+
460
+ // A cookie in session doesn't mean we can't cache, nor an empty flash or passport object.
461
+ // Other session properties must be assumed to be specific to the user, with a possible
462
+ // impact on the response, and thus mean this request must not be cached.
463
+ // Same rule as in [express-cache-on-demand](https://github.com/apostrophecms/express-cache-on-demand/blob/master/index.js#L102)
464
+ const isSessionClearForCaching = Object.entries(req.session).every(([ key, val ]) =>
465
+ key === 'cookie' || (
466
+ (key === 'flash' || key === 'passport') && _.isEmpty(val)
467
+ )
468
+ );
469
+ const isSafeToCache = !req.user && isSessionClearForCaching;
470
+ const cacheControlValue = isSafeToCache ? `max-age=${maxAge}` : 'no-store';
471
+
472
+ req.res.header('Cache-Control', cacheControlValue);
473
+ },
474
+
449
475
  // Call from init once if this module implements the `getBrowserData` method.
450
476
  // The data returned by `getBrowserData(req)` will then be available on
451
477
  // `apos.modules['your-module-name']` in the browser.
@@ -119,6 +119,10 @@ module.exports = {
119
119
  project: self.getAllProjection()
120
120
  }).toObject();
121
121
 
122
+ if (self.options.cache && self.options.cache.api) {
123
+ self.setMaxAge(req, self.options.cache.api.maxAge);
124
+ }
125
+
122
126
  if (!page) {
123
127
  throw self.apos.error('notfound');
124
128
  }
@@ -137,6 +141,11 @@ module.exports = {
137
141
  }
138
142
  } else {
139
143
  const result = await self.getRestQuery(req).and({ level: 0 }).toObject();
144
+
145
+ if (self.options.cache && self.options.cache.api) {
146
+ self.setMaxAge(req, self.options.cache.api.maxAge);
147
+ }
148
+
140
149
  if (!result) {
141
150
  throw self.apos.error('notfound');
142
151
  }
@@ -168,6 +177,11 @@ module.exports = {
168
177
  self.publicApiCheck(req);
169
178
  const criteria = self.getIdCriteria(_id);
170
179
  const result = await self.getRestQuery(req).and(criteria).toObject();
180
+
181
+ if (self.options.cache && self.options.cache.api) {
182
+ self.setMaxAge(req, self.options.cache.api.maxAge);
183
+ }
184
+
171
185
  if (!result) {
172
186
  throw self.apos.error('notfound');
173
187
  }
@@ -220,7 +234,7 @@ module.exports = {
220
234
  }
221
235
 
222
236
  return self.withLock(req, async () => {
223
- const targetPage = await self.findForEditing(req, targetId ? { _id: targetId } : { level: 0 }).ancestors(true).permission('edit').toObject();
237
+ const targetPage = await self.findForEditing(req, targetId ? self.getIdCriteria(targetId) : { level: 0 }).ancestors(true).permission('edit').toObject();
224
238
  if (!targetPage) {
225
239
  throw self.apos.error('notfound');
226
240
  }
@@ -1415,6 +1429,10 @@ database.`);
1415
1429
  await self.emit('serveQuery', query);
1416
1430
  req.data.bestPage = await query.toObject();
1417
1431
  self.evaluatePageMatch(req);
1432
+
1433
+ if (self.options.cache && self.options.cache.page) {
1434
+ self.setMaxAge(req, self.options.cache.page.maxAge);
1435
+ }
1418
1436
  },
1419
1437
  // Normalize req.slug to account for unneeded trailing whitespace,
1420
1438
  // trailing slashes other than the root, and double slash based open
@@ -200,6 +200,11 @@ module.exports = {
200
200
  if (query.get('countsResults')) {
201
201
  result.counts = query.get('countsResults');
202
202
  }
203
+
204
+ if (self.options.cache && self.options.cache.api) {
205
+ self.setMaxAge(req, self.options.cache.api.maxAge);
206
+ }
207
+
203
208
  return result;
204
209
  }
205
210
  ],
@@ -209,6 +214,11 @@ module.exports = {
209
214
  _id = self.inferIdLocaleAndMode(req, _id);
210
215
  self.publicApiCheck(req);
211
216
  const doc = await self.getRestQuery(req).and({ _id }).toObject();
217
+
218
+ if (self.options.cache && self.options.cache.api) {
219
+ self.setMaxAge(req, self.options.cache.api.maxAge);
220
+ }
221
+
212
222
  if (!doc) {
213
223
  throw self.apos.error('notfound');
214
224
  }
@@ -193,6 +193,12 @@ module.exports = {
193
193
  res: {
194
194
  redirect(url) {
195
195
  req.res.redirectedTo = url;
196
+ },
197
+ header(key, value) {
198
+ req.res.headers = {
199
+ ...(req.res.headers || {}),
200
+ [key]: value
201
+ };
196
202
  }
197
203
  },
198
204
  t(key, options = {}) {
@@ -643,6 +643,7 @@ module.exports = {
643
643
  modules: {},
644
644
  prefix: req.prefix,
645
645
  sitePrefix: self.apos.prefix,
646
+ shortName: self.apos.shortName,
646
647
  locale: req.locale,
647
648
  csrfCookieName: self.apos.csrfCookieName,
648
649
  tabId: self.apos.util.generateId(),
@@ -16,6 +16,7 @@
16
16
  <!-- TODO refactor buttons to take a single config obj -->
17
17
  <AposButton
18
18
  class="apos-context-menu__btn"
19
+ data-apos-test="contextMenuTrigger"
19
20
  @click.stop="buttonClicked($event)"
20
21
  v-bind="button"
21
22
  :state="buttonState"
@@ -19,6 +19,7 @@
19
19
  <AposContextMenuItem
20
20
  v-for="item in menu"
21
21
  :key="item.action"
22
+ :data-apos-test-context-menu-item="item.action"
22
23
  :menu-item="item"
23
24
  @clicked="menuItemClicked"
24
25
  :open="isOpen"
@@ -432,7 +432,12 @@ module.exports = {
432
432
 
433
433
  // Initialize the [credential](https://npmjs.org/package/credential) module.
434
434
  initializeCredential() {
435
- self.pw = credential();
435
+ self.pw = credential({
436
+ // For efficient unit tests only. Reducing the work factor
437
+ // for actual credentials increases the speed of brute force attacks
438
+ // if the database is ever compromised
439
+ work: self.options.insecurePasswords ? 0.01 : 1
440
+ });
436
441
  },
437
442
 
438
443
  // Implement the `@apostrophecms/user:add` command line task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/login.js CHANGED
@@ -15,7 +15,18 @@ describe('Login', function() {
15
15
 
16
16
  it('should initialize', async function() {
17
17
  apos = await t.create({
18
- root: module
18
+ root: module,
19
+ modules: {
20
+ '@apostrophecms/express': {
21
+ options: {
22
+ apiKeys: {
23
+ adminApiKey: {
24
+ role: 'admin'
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
19
30
  });
20
31
 
21
32
  assert(apos.modules['@apostrophecms/login']);
@@ -39,6 +50,14 @@ describe('Login', function() {
39
50
  assert(apos.user.insert);
40
51
  const doc = await apos.user.insert(apos.task.getReq(), user);
41
52
  assert(doc._id);
53
+
54
+ const user2 = apos.user.newInstance();
55
+ user2.title = 'Bob Smith';
56
+ user2.username = 'BobSmith';
57
+ user2.password = 'bobsmith';
58
+ user2.email = 'bobsmith@aol.com';
59
+ user2.role = 'guest';
60
+ await apos.user.insert(apos.task.getReq(), user2);
42
61
  });
43
62
 
44
63
  it('should throttle login attempts and show a proper error when the limit is reached', async function () {
@@ -84,6 +103,8 @@ describe('Login', function() {
84
103
  });
85
104
 
86
105
  it('should be able to login a user with their username', async function() {
106
+ const getLoggedInCookieValue =
107
+ jar => jar.toJSON().cookies.find(cookie => cookie.key === `${apos.options.shortName}.loggedIn`).value;
87
108
 
88
109
  const jar = apos.http.jar();
89
110
 
@@ -118,6 +139,8 @@ describe('Login', function() {
118
139
  );
119
140
 
120
141
  assert(page.match(/logged in/));
142
+ assert(page.match(/Harry Putter/));
143
+ assert(getLoggedInCookieValue(jar) === 'true');
121
144
 
122
145
  // otherwise logins are not remembered in a session
123
146
  await apos.http.post(
@@ -141,6 +164,7 @@ describe('Login', function() {
141
164
 
142
165
  // are we back to being able to log in?
143
166
  assert(page.match(/logged out/));
167
+ assert(getLoggedInCookieValue(jar) === 'false');
144
168
  });
145
169
 
146
170
  it('should be able to login a user with their email', async function() {
@@ -386,4 +410,41 @@ describe('Login', function() {
386
410
 
387
411
  });
388
412
 
413
+ it('api key should beat session when both are present', async function() {
414
+ const jar = apos.http.jar();
415
+ await apos.http.post(
416
+ '/api/v1/@apostrophecms/login/login',
417
+ {
418
+ method: 'POST',
419
+ body: {
420
+ username: 'BobSmith',
421
+ password: 'bobsmith',
422
+ session: true
423
+ },
424
+ jar
425
+ }
426
+ );
427
+
428
+ const page = await apos.http.get(
429
+ '/',
430
+ {
431
+ jar
432
+ }
433
+ );
434
+ assert(page.match(/logged in/));
435
+ assert(page.match(/Bob Smith/));
436
+
437
+ const page2 = await apos.http.get(
438
+ '/',
439
+ {
440
+ jar,
441
+ headers: {
442
+ Authorization: 'ApiKey adminApiKey'
443
+ }
444
+ }
445
+ );
446
+ assert(page2.match(/logged in/));
447
+ assert(!page2.match(/Bob Smith/));
448
+ assert(page2.match(/System Task/));
449
+ });
389
450
  });
@@ -2,7 +2,7 @@
2
2
  <h4>Home Page Template</h4>
3
3
  {# This is necessary to the login.js tests. -Tom #}
4
4
  {% if data.user %}
5
- logged in
5
+ logged in as {{ data.user.title }}
6
6
  {% else %}
7
7
  logged out
8
8
  {% endif %}
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ options: {
3
+ // Accelerates unit tests while still testing all the same
4
+ // functionality. Unsafe for other uses
5
+ insecurePasswords: true
6
+ }
7
+ };
@@ -68,4 +68,37 @@ describe('Pages Public API', function() {
68
68
  // But projection did apply
69
69
  assert(!home.searchSummary);
70
70
  });
71
+
72
+ it('should not set a "max-age" cache-control value when retrieving pages, when cache option is not set, with a public API projection', async () => {
73
+ apos.page.options.publicApiProjection = {
74
+ title: 1,
75
+ _url: 1
76
+ };
77
+
78
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
79
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${response1.body._id}`, { fullResponse: true });
80
+
81
+ assert(response1.headers['cache-control'] === undefined);
82
+ assert(response2.headers['cache-control'] === undefined);
83
+ });
84
+
85
+ it('should set a "max-age" cache-control value when retrieving pages, with a public API projection', async () => {
86
+ apos.page.options.publicApiProjection = {
87
+ title: 1,
88
+ _url: 1
89
+ };
90
+ apos.page.options.cache = {
91
+ api: {
92
+ maxAge: 1111
93
+ }
94
+ };
95
+
96
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
97
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${response1.body._id}`, { fullResponse: true });
98
+
99
+ assert(response1.headers['cache-control'] === 'max-age=1111');
100
+ assert(response2.headers['cache-control'] === 'max-age=1111');
101
+
102
+ delete apos.page.options.cache;
103
+ });
71
104
  });
@@ -282,6 +282,15 @@ describe('Pages REST', function() {
282
282
  assert(draftItems.result.ok === 1);
283
283
  assert(draftItems.insertedCount === 7);
284
284
 
285
+ // Change the rank of the archive pages to preserve the constraint that it comes last
286
+ await apos.doc.db.updateMany({
287
+ type: '@apostrophecms/archive-page'
288
+ }, {
289
+ $set: {
290
+ rank: 3
291
+ }
292
+ });
293
+
285
294
  const items = await apos.doc.db.insertMany(testItems);
286
295
 
287
296
  assert(items.result.ok === 1);
@@ -430,7 +439,7 @@ describe('Pages REST', function() {
430
439
  // Is the rank correct?
431
440
  const home = await apos.http.get('/api/v1/@apostrophecms/page', {});
432
441
  assert(home._children);
433
- assert(home._children[3]._id === 'cousin:en:published');
442
+ assert(home._children[1]._id === 'cousin:en:published');
434
443
  });
435
444
 
436
445
  it('is able to move root/cousin before root/parent/child', async function() {
@@ -587,6 +596,33 @@ describe('Pages REST', function() {
587
596
  assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/another-parent/parent/sibling`);
588
597
  });
589
598
 
599
+ it('is able to make a subpage of the homepage at the last index with _position: lastChild', async function() {
600
+ const body = {
601
+ slug: '/third-new',
602
+ visibility: 'public',
603
+ type: 'test-page',
604
+ title: 'Third New',
605
+ _targetId: '_home',
606
+ _position: 'lastChild'
607
+ };
608
+
609
+ const page = await apos.http.post('/api/v1/@apostrophecms/page', {
610
+ body,
611
+ jar
612
+ });
613
+
614
+ assert(page);
615
+ assert(page.title === 'Third New');
616
+ // Is the path generally correct?
617
+ assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/${page._id.replace(':en:published', '')}`);
618
+ const home = await apos.http.get('/api/v1/@apostrophecms/page?children=1', {
619
+ jar
620
+ });
621
+ assert(home);
622
+ assert(home._children);
623
+ assert(home._children[3]._id === page._id);
624
+ });
625
+
590
626
  it('can use PUT to modify a page', async function() {
591
627
  const page = await apos.http.get('/api/v1/@apostrophecms/page/sibling:en:published', { jar });
592
628
  assert(page);
package/test/pages.js CHANGED
@@ -4,6 +4,7 @@ const _ = require('lodash');
4
4
 
5
5
  let apos;
6
6
  let homeId;
7
+ const apiKey = 'this is a test api key';
7
8
 
8
9
  describe('Pages', function() {
9
10
 
@@ -19,6 +20,15 @@ describe('Pages', function() {
19
20
  apos = await t.create({
20
21
  root: module,
21
22
  modules: {
23
+ '@apostrophecms/express': {
24
+ options: {
25
+ apiKeys: {
26
+ [apiKey]: {
27
+ role: 'admin'
28
+ }
29
+ }
30
+ }
31
+ },
22
32
  '@apostrophecms/page': {
23
33
  options: {
24
34
  park: [],
@@ -31,7 +41,11 @@ describe('Pages', function() {
31
41
  name: 'test-page',
32
42
  label: 'Test Page'
33
43
  }
34
- ]
44
+ ],
45
+ publicApiProjection: {
46
+ title: 1,
47
+ _url: 1
48
+ }
35
49
  }
36
50
  },
37
51
  'test-page': {
@@ -481,4 +495,166 @@ describe('Pages', function() {
481
495
  }
482
496
  });
483
497
 
498
+ it('should not set a cache-control value when retrieving pages, when cache option is not set', async () => {
499
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
500
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
501
+
502
+ assert(response1.headers['cache-control'] === undefined);
503
+ assert(response2.headers['cache-control'] === undefined);
504
+ });
505
+
506
+ it('should not set a cache-control value when retrieving pages, when "api" cache option is not set', async () => {
507
+ apos.page.options.cache = {
508
+ page: {
509
+ maxAge: 5555
510
+ }
511
+ };
512
+
513
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
514
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
515
+
516
+ assert(response1.headers['cache-control'] === undefined);
517
+ assert(response2.headers['cache-control'] === undefined);
518
+
519
+ delete apos.page.options.cache;
520
+ });
521
+
522
+ it('should set a "max-age" cache-control value when retrieving pieces, when "api" cache option is set', async () => {
523
+ apos.page.options.cache = {
524
+ api: {
525
+ maxAge: 4444
526
+ }
527
+ };
528
+
529
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
530
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
531
+
532
+ assert(response1.headers['cache-control'] === 'max-age=4444');
533
+ assert(response2.headers['cache-control'] === 'max-age=4444');
534
+
535
+ delete apos.page.options.cache;
536
+ });
537
+
538
+ it('should set a "no-store" cache-control value when retrieving pages, when user is connected', async () => {
539
+ const jar = apos.http.jar();
540
+ const user = apos.user.newInstance();
541
+
542
+ user.title = 'admin';
543
+ user.username = 'admin';
544
+ user.password = 'admin';
545
+ user.email = 'ad@min.com';
546
+ user.role = 'admin';
547
+
548
+ await apos.user.insert(apos.task.getReq(), user);
549
+ await apos.http.post('/api/v1/@apostrophecms/login/login', {
550
+ body: {
551
+ username: 'admin',
552
+ password: 'admin',
553
+ session: true
554
+ },
555
+ jar
556
+ });
557
+
558
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', {
559
+ fullResponse: true,
560
+ jar
561
+ });
562
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, {
563
+ fullResponse: true,
564
+ jar
565
+ });
566
+
567
+ assert(response1.headers['cache-control'] === 'no-store');
568
+ assert(response2.headers['cache-control'] === 'no-store');
569
+ });
570
+
571
+ it('should set a "no-store" cache-control value when retrieving pages, when "api" cache option is set, when user is connected', async () => {
572
+ apos.page.options.cache = {
573
+ api: {
574
+ maxAge: 4444
575
+ }
576
+ };
577
+
578
+ const jar = apos.http.jar();
579
+
580
+ await apos.http.post('/api/v1/@apostrophecms/login/login', {
581
+ body: {
582
+ username: 'admin',
583
+ password: 'admin',
584
+ session: true
585
+ },
586
+ jar
587
+ });
588
+
589
+ const response1 = await apos.http.get('/api/v1/@apostrophecms/page', {
590
+ fullResponse: true,
591
+ jar
592
+ });
593
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, {
594
+ fullResponse: true,
595
+ jar
596
+ });
597
+
598
+ assert(response1.headers['cache-control'] === 'no-store');
599
+ assert(response2.headers['cache-control'] === 'no-store');
600
+
601
+ delete apos.page.options.cache;
602
+ });
603
+
604
+ it('should set a "no-store" cache-control value when retrieving pages, when user is connected using an api key', async () => {
605
+ const response1 = await apos.http.get(`/api/v1/@apostrophecms/page?apiKey=${apiKey}`, { fullResponse: true });
606
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}?apiKey=${apiKey}`, { fullResponse: true });
607
+
608
+ assert(response1.headers['cache-control'] === 'no-store');
609
+ assert(response2.headers['cache-control'] === 'no-store');
610
+ });
611
+
612
+ it('should set a "no-store" cache-control value when retrieving pages, when "api" cache option is set, when user is connected using an api key', async () => {
613
+ apos.page.options.cache = {
614
+ api: {
615
+ maxAge: 4444
616
+ }
617
+ };
618
+
619
+ const response1 = await apos.http.get(`/api/v1/@apostrophecms/page?apiKey=${apiKey}`, { fullResponse: true });
620
+ const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}?apiKey=${apiKey}`, { fullResponse: true });
621
+
622
+ assert(response1.headers['cache-control'] === 'no-store');
623
+ assert(response2.headers['cache-control'] === 'no-store');
624
+
625
+ delete apos.page.options.cache;
626
+ });
627
+
628
+ it('should not set a cache-control value when serving a page, when cache option is not set', async () => {
629
+ const response = await apos.http.get('/', { fullResponse: true });
630
+
631
+ assert(response.headers['cache-control'] === undefined);
632
+ });
633
+
634
+ it('should not set a cache-control value when serving a page, when "page" cache option is not set', async () => {
635
+ apos.page.options.cache = {
636
+ api: {
637
+ maxAge: 4444
638
+ }
639
+ };
640
+ const response = await apos.http.get('/', { fullResponse: true });
641
+
642
+ assert(response.headers['cache-control'] === undefined);
643
+
644
+ delete apos.page.options.cache;
645
+ });
646
+
647
+ it('should set a cache-control value when serving a page, when "page" cache option is set', async () => {
648
+ apos.page.options.cache = {
649
+ page: {
650
+ maxAge: 5555
651
+ }
652
+ };
653
+ const response = await apos.http.get('/', { fullResponse: true });
654
+
655
+ assert(response.headers['cache-control'] === 'max-age=5555');
656
+
657
+ delete apos.page.options.cache;
658
+ });
659
+
484
660
  });
@@ -78,4 +78,37 @@ describe('Pieces Public API', function() {
78
78
  assert(!response.results[0].foo);
79
79
  });
80
80
 
81
+ it('should not set a "max-age" cache-control value when retrieving pieces, when cache option is not set, with a public API projection', async () => {
82
+ apos.thing.options.publicApiProjection = {
83
+ title: 1,
84
+ _url: 1
85
+ };
86
+
87
+ const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
88
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
89
+
90
+ assert(response1.headers['cache-control'] === undefined);
91
+ assert(response2.headers['cache-control'] === undefined);
92
+ });
93
+
94
+ it('should set a "max-age" cache-control value when retrieving pieces, with a public API projection', async () => {
95
+ apos.thing.options.publicApiProjection = {
96
+ title: 1,
97
+ _url: 1
98
+ };
99
+ apos.thing.options.cache = {
100
+ api: {
101
+ maxAge: 2222
102
+ }
103
+ };
104
+
105
+ const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
106
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
107
+
108
+ assert(response1.headers['cache-control'] === 'max-age=2222');
109
+ assert(response2.headers['cache-control'] === 'max-age=2222');
110
+
111
+ delete apos.thing.options.cache;
112
+ });
113
+
81
114
  });
package/test/pieces.js CHANGED
@@ -1280,4 +1280,122 @@ describe('Pieces', function() {
1280
1280
  assert(existingPiece.title === 'new product name');
1281
1281
  assert(existingPiece.color === 'red');
1282
1282
  });
1283
+
1284
+ it('should not set a cache-control value when retrieving pieces, when cache option is not set', async () => {
1285
+ const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
1286
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
1287
+
1288
+ assert(response1.headers['cache-control'] === undefined);
1289
+ assert(response2.headers['cache-control'] === undefined);
1290
+ });
1291
+
1292
+ it('should not set a cache-control value when retrieving pieces, when "api" cache option is not set', async () => {
1293
+ apos.thing.options.cache = {
1294
+ page: {
1295
+ maxAge: 5555
1296
+ }
1297
+ };
1298
+
1299
+ const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
1300
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
1301
+
1302
+ assert(response1.headers['cache-control'] === undefined);
1303
+ assert(response2.headers['cache-control'] === undefined);
1304
+
1305
+ delete apos.thing.options.cache;
1306
+ });
1307
+
1308
+ it('should set a "max-age" cache-control value when retrieving pieces, when "api" cache option is set', async () => {
1309
+ apos.thing.options.cache = {
1310
+ api: {
1311
+ maxAge: 3333
1312
+ }
1313
+ };
1314
+
1315
+ const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
1316
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
1317
+
1318
+ assert(response1.headers['cache-control'] === 'max-age=3333');
1319
+ assert(response2.headers['cache-control'] === 'max-age=3333');
1320
+
1321
+ delete apos.thing.options.cache;
1322
+ });
1323
+
1324
+ it('should set a "no-store" cache-control value when retrieving pieces, when user is connected', async () => {
1325
+ await apos.http.post('/api/v1/@apostrophecms/login/login', {
1326
+ body: {
1327
+ username: 'admin',
1328
+ password: 'admin',
1329
+ session: true
1330
+ },
1331
+ jar
1332
+ });
1333
+
1334
+ const response1 = await apos.http.get('/api/v1/thing', {
1335
+ fullResponse: true,
1336
+ jar
1337
+ });
1338
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', {
1339
+ fullResponse: true,
1340
+ jar
1341
+ });
1342
+
1343
+ assert(response1.headers['cache-control'] === 'no-store');
1344
+ assert(response2.headers['cache-control'] === 'no-store');
1345
+ });
1346
+
1347
+ it('should set a "no-store" cache-control value when retrieving pieces, when "api" cache option is set, when user is connected', async () => {
1348
+ apos.thing.options.cache = {
1349
+ api: {
1350
+ maxAge: 3333
1351
+ }
1352
+ };
1353
+
1354
+ await apos.http.post('/api/v1/@apostrophecms/login/login', {
1355
+ body: {
1356
+ username: 'admin',
1357
+ password: 'admin',
1358
+ session: true
1359
+ },
1360
+ jar
1361
+ });
1362
+
1363
+ const response1 = await apos.http.get('/api/v1/thing', {
1364
+ fullResponse: true,
1365
+ jar
1366
+ });
1367
+ const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', {
1368
+ fullResponse: true,
1369
+ jar
1370
+ });
1371
+
1372
+ assert(response1.headers['cache-control'] === 'no-store');
1373
+ assert(response2.headers['cache-control'] === 'no-store');
1374
+
1375
+ delete apos.thing.options.cache;
1376
+ });
1377
+
1378
+ it('should set a "no-store" cache-control value when retrieving pieces, when user is connected using an api key', async () => {
1379
+ const response1 = await apos.http.get(`/api/v1/thing?apiKey=${apiKey}`, { fullResponse: true });
1380
+ const response2 = await apos.http.get(`/api/v1/thing/testThing:en:published?apiKey=${apiKey}`, { fullResponse: true });
1381
+
1382
+ assert(response1.headers['cache-control'] === 'no-store');
1383
+ assert(response2.headers['cache-control'] === 'no-store');
1384
+ });
1385
+
1386
+ it('should set a "no-store" cache-control value when retrieving pieces, when "api" cache option is set, when user is connected using an api key', async () => {
1387
+ apos.thing.options.cache = {
1388
+ api: {
1389
+ maxAge: 3333
1390
+ }
1391
+ };
1392
+
1393
+ const response1 = await apos.http.get(`/api/v1/thing?apiKey=${apiKey}`, { fullResponse: true });
1394
+ const response2 = await apos.http.get(`/api/v1/thing/testThing:en:published?apiKey=${apiKey}`, { fullResponse: true });
1395
+
1396
+ assert(response1.headers['cache-control'] === 'no-store');
1397
+ assert(response2.headers['cache-control'] === 'no-store');
1398
+
1399
+ delete apos.thing.options.cache;
1400
+ });
1283
1401
  });
package/test/templates.js CHANGED
@@ -115,6 +115,7 @@ describe('Templates', function() {
115
115
  assert($body.length);
116
116
  const aposData = JSON.parse($body.attr('data-apos'));
117
117
  assert(aposData);
118
+ assert(aposData.shortName);
118
119
  assert(aposData.csrfCookieName);
119
120
  assert(!aposData.modules['@apostrophecms/admin-bar']);
120
121
  assert(result.indexOf('<title>I am the title</title>') !== -1);
@@ -129,6 +130,7 @@ describe('Templates', function() {
129
130
  assert($body.length);
130
131
  const aposData = JSON.parse($body.attr('data-apos'));
131
132
  assert(aposData);
133
+ assert(aposData.shortName);
132
134
  assert(aposData.modules['@apostrophecms/admin-bar'].items.length);
133
135
  assert(result.indexOf('<title>I am the title</title>') !== -1);
134
136
  assert(result.indexOf('<h2>I am the main content</h2>') !== -1);