apostrophe 3.13.0 → 3.14.2-alpha.20220401
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 +29 -0
- package/index.js +9 -7
- package/modules/@apostrophecms/asset/index.js +24 -9
- package/modules/@apostrophecms/doc/index.js +11 -3
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +0 -7
- package/modules/@apostrophecms/express/index.js +43 -37
- package/modules/@apostrophecms/http/index.js +0 -20
- package/modules/@apostrophecms/i18n/i18n/en.json +2 -0
- package/modules/@apostrophecms/login/index.js +160 -33
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +23 -24
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +1 -1
- package/modules/@apostrophecms/migration/index.js +27 -32
- package/modules/@apostrophecms/page/index.js +134 -80
- package/modules/@apostrophecms/piece-page-type/index.js +1 -1
- package/modules/@apostrophecms/piece-type/index.js +85 -72
- package/modules/@apostrophecms/util/ui/src/http.js +1 -7
- package/package.json +3 -2
- package/test/express.js +2 -26
- package/test/http.js +0 -24
- package/test/login-requirements.js +76 -6
- package/test/login.js +42 -0
- package/test/parked-pages.js +284 -13
- package/test/pieces-page-type.js +63 -0
- package/test/package.json +0 -11
|
@@ -65,7 +65,6 @@ export default () => {
|
|
|
65
65
|
// parsed as JSON only if the content-type is application/json)
|
|
66
66
|
// `headers` (an object containing header names and values)
|
|
67
67
|
// `draft` (if true, always add aposMode=draft to the query string, creating one if needed)
|
|
68
|
-
// `csrf` (unless explicitly set to `false`, send the X-XSRF-TOKEN header when talking to the same site)
|
|
69
68
|
// `fullResponse` (if true, return an object with `status`, `headers` and `body`
|
|
70
69
|
// properties, rather than returning the body directly; the individual `headers` are canonicalized
|
|
71
70
|
// to lowercase names. If there are duplicate headers after canonicalization only the
|
|
@@ -140,10 +139,10 @@ export default () => {
|
|
|
140
139
|
|
|
141
140
|
const busyName = options.busy === true ? 'busy' : options.busy;
|
|
142
141
|
const xmlhttp = new XMLHttpRequest();
|
|
143
|
-
const csrfToken = apos.csrfCookieName ? apos.util.getCookie(apos.csrfCookieName) : 'csrf-fallback';
|
|
144
142
|
let data = options.body;
|
|
145
143
|
let keys;
|
|
146
144
|
let i;
|
|
145
|
+
|
|
147
146
|
if (options.qs) {
|
|
148
147
|
url = apos.http.addQueryToUrl(url, options.qs);
|
|
149
148
|
}
|
|
@@ -166,11 +165,6 @@ export default () => {
|
|
|
166
165
|
if (sendJson) {
|
|
167
166
|
xmlhttp.setRequestHeader('Content-Type', 'application/json');
|
|
168
167
|
}
|
|
169
|
-
if (csrfToken && (options.csrf !== false)) {
|
|
170
|
-
if (apos.util.sameSite(url)) {
|
|
171
|
-
xmlhttp.setRequestHeader('X-XSRF-TOKEN', csrfToken);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
168
|
if (options.headers) {
|
|
175
169
|
keys = Object.keys(options.headers);
|
|
176
170
|
for (i = 0; (i < keys.length); i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.2-alpha.20220401",
|
|
4
4
|
"description": "The Apostrophe Content Management System.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"eslint-plugin-promise": "^5.1.0",
|
|
62
62
|
"express": "^4.16.4",
|
|
63
63
|
"express-bearer-token": "^2.4.0",
|
|
64
|
+
"express-cache-on-demand": "^1.0.3",
|
|
64
65
|
"express-session": "^1.17.1",
|
|
65
66
|
"form-data": "^4.0.0",
|
|
66
67
|
"fs-extra": "^7.0.1",
|
|
@@ -136,4 +137,4 @@
|
|
|
136
137
|
"browserslist": [
|
|
137
138
|
"ie >= 10"
|
|
138
139
|
]
|
|
139
|
-
}
|
|
140
|
+
}
|
package/test/express.js
CHANGED
|
@@ -39,37 +39,13 @@ describe('Express', function() {
|
|
|
39
39
|
assert(body.toString() === 'ok');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('should flunk a POST request
|
|
42
|
+
it('should flunk a POST request without the CSRF cookie', async function() {
|
|
43
43
|
try {
|
|
44
44
|
await apos.http.post('/tests/body', {
|
|
45
45
|
body: {
|
|
46
46
|
person: {
|
|
47
47
|
age: '30'
|
|
48
48
|
}
|
|
49
|
-
},
|
|
50
|
-
jar,
|
|
51
|
-
csrf: false
|
|
52
|
-
});
|
|
53
|
-
assert(false);
|
|
54
|
-
} catch (e) {
|
|
55
|
-
assert(e);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should flunk a POST request with the wrong CSRF token', async function() {
|
|
60
|
-
const csrfToken = 'BOGOSITY';
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
await apos.http.post('/tests/body', {
|
|
64
|
-
body: {
|
|
65
|
-
person: {
|
|
66
|
-
age: '30'
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
jar,
|
|
70
|
-
csrf: false,
|
|
71
|
-
headers: {
|
|
72
|
-
'X-XSRF-TOKEN': csrfToken
|
|
73
49
|
}
|
|
74
50
|
});
|
|
75
51
|
assert(false);
|
|
@@ -78,7 +54,7 @@ describe('Express', function() {
|
|
|
78
54
|
}
|
|
79
55
|
});
|
|
80
56
|
|
|
81
|
-
it('should use the extended bodyParser for submitted forms', async function() {
|
|
57
|
+
it('should use the extended bodyParser for submitted forms, and pass CSRF with the cookie', async function() {
|
|
82
58
|
|
|
83
59
|
const response = await apos.http.post('/tests/body', {
|
|
84
60
|
send: 'form',
|
package/test/http.js
CHANGED
|
@@ -40,30 +40,6 @@ describe('Http', function() {
|
|
|
40
40
|
assert(result.match(/logged out/));
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('should not be able to make an http POST request without csrf header', async () => {
|
|
44
|
-
try {
|
|
45
|
-
await apos.http.post('/csrf-test', {
|
|
46
|
-
jar,
|
|
47
|
-
csrf: false
|
|
48
|
-
});
|
|
49
|
-
assert(false);
|
|
50
|
-
} catch (e) {
|
|
51
|
-
assert(e.status === 403);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should be able to make an http POST request with manually built csrf header', async () => {
|
|
56
|
-
const response = await apos.http.post('/csrf-test?manual=1', {
|
|
57
|
-
jar,
|
|
58
|
-
headers: {
|
|
59
|
-
'X-XSRF-TOKEN': apos.http.getCookie(jar, '/', `${apos.options.shortName}.csrf`)
|
|
60
|
-
},
|
|
61
|
-
body: {},
|
|
62
|
-
csrf: false
|
|
63
|
-
});
|
|
64
|
-
assert(response.ok === true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
43
|
it('should be able to make an http POST request with csrf header via default csrf convenience of http.post', async () => {
|
|
68
44
|
const response = await apos.http.post('/csrf-test', {
|
|
69
45
|
jar,
|
|
@@ -4,6 +4,8 @@ const assert = require('assert');
|
|
|
4
4
|
let apos;
|
|
5
5
|
|
|
6
6
|
describe('Login', function() {
|
|
7
|
+
const extraSecretErr = 'extra secret incorrect';
|
|
8
|
+
const captchaErr = 'captcha code incorrect';
|
|
7
9
|
|
|
8
10
|
this.timeout(20000);
|
|
9
11
|
|
|
@@ -30,7 +32,7 @@ describe('Login', function() {
|
|
|
30
32
|
},
|
|
31
33
|
async verify(req, data) {
|
|
32
34
|
if (data !== 'xyz') {
|
|
33
|
-
throw self.apos.error('invalid',
|
|
35
|
+
throw self.apos.error('invalid', captchaErr);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
},
|
|
@@ -45,7 +47,7 @@ describe('Login', function() {
|
|
|
45
47
|
},
|
|
46
48
|
async verify(req, data, user) {
|
|
47
49
|
if (data !== user.extraSecret) {
|
|
48
|
-
throw self.apos.error('invalid',
|
|
50
|
+
throw self.apos.error('invalid', extraSecretErr);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -119,7 +121,7 @@ describe('Login', function() {
|
|
|
119
121
|
assert(false);
|
|
120
122
|
} catch (e) {
|
|
121
123
|
assert(e.status === 400);
|
|
122
|
-
assert.strictEqual(e.body.message,
|
|
124
|
+
assert.strictEqual(e.body.message, captchaErr);
|
|
123
125
|
assert.strictEqual(e.body.data.requirement, 'WeakCaptcha');
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -167,7 +169,7 @@ describe('Login', function() {
|
|
|
167
169
|
assert(false);
|
|
168
170
|
} catch (e) {
|
|
169
171
|
assert(e.status === 400);
|
|
170
|
-
assert.strictEqual(e.body.message,
|
|
172
|
+
assert.strictEqual(e.body.message, captchaErr);
|
|
171
173
|
assert.strictEqual(e.body.data.requirement, 'WeakCaptcha');
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -182,7 +184,10 @@ describe('Login', function() {
|
|
|
182
184
|
assert(page.match(/logged out/));
|
|
183
185
|
});
|
|
184
186
|
|
|
185
|
-
it('
|
|
187
|
+
it('should throttle requirements verify attemps and show a proper error when the limit is reached', async function () {
|
|
188
|
+
const loginModule = apos.modules['@apostrophecms/login'];
|
|
189
|
+
const { allowedAttempts } = loginModule.options.throttle;
|
|
190
|
+
const namespace = '@apostrophecms/loginAttempt/ExtraSecret';
|
|
186
191
|
|
|
187
192
|
const jar = apos.http.jar();
|
|
188
193
|
|
|
@@ -224,6 +229,71 @@ describe('Login', function() {
|
|
|
224
229
|
|
|
225
230
|
assert(page.match(/logged out/));
|
|
226
231
|
|
|
232
|
+
const token = result.incompleteToken;
|
|
233
|
+
|
|
234
|
+
for (let index = 0; index <= allowedAttempts; index++) {
|
|
235
|
+
try {
|
|
236
|
+
await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', {
|
|
237
|
+
body: {
|
|
238
|
+
incompleteToken: token,
|
|
239
|
+
session: true,
|
|
240
|
+
name: 'ExtraSecret',
|
|
241
|
+
value: 'roll-off'
|
|
242
|
+
},
|
|
243
|
+
jar
|
|
244
|
+
});
|
|
245
|
+
} catch ({ status, body }) {
|
|
246
|
+
if (index < allowedAttempts) {
|
|
247
|
+
assert(body.message === extraSecretErr);
|
|
248
|
+
} else {
|
|
249
|
+
assert(body.message === 'Too many attempts. You may try again in a minute.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await loginModule.clearLoginAttempts('HarryPutter', namespace);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('initial login should produce an incompleteToken, convertible with the afterPasswordVerified requirements', async function() {
|
|
258
|
+
|
|
259
|
+
const jar = apos.http.jar();
|
|
260
|
+
|
|
261
|
+
// establish session
|
|
262
|
+
let page = await apos.http.get(
|
|
263
|
+
'/',
|
|
264
|
+
{
|
|
265
|
+
jar
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const result = await apos.http.post(
|
|
270
|
+
'/api/v1/@apostrophecms/login/login',
|
|
271
|
+
{
|
|
272
|
+
method: 'POST',
|
|
273
|
+
body: {
|
|
274
|
+
username: 'HarryPutter',
|
|
275
|
+
password: 'crookshanks',
|
|
276
|
+
session: true,
|
|
277
|
+
requirements: {
|
|
278
|
+
WeakCaptcha: 'xyz'
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
jar
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert(result.incompleteToken);
|
|
286
|
+
|
|
287
|
+
// Make sure it did not create a login session prematurely
|
|
288
|
+
page = await apos.http.get(
|
|
289
|
+
'/',
|
|
290
|
+
{
|
|
291
|
+
jar
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
assert(page.match(/logged out/));
|
|
296
|
+
|
|
227
297
|
// Make sure it won't convert with an incorrect ExtraSecret
|
|
228
298
|
|
|
229
299
|
const token = result.incompleteToken;
|
|
@@ -240,7 +310,7 @@ describe('Login', function() {
|
|
|
240
310
|
});
|
|
241
311
|
} catch ({ status, body }) {
|
|
242
312
|
assert(status === 400);
|
|
243
|
-
assert.strictEqual(body.message,
|
|
313
|
+
assert.strictEqual(body.message, extraSecretErr);
|
|
244
314
|
assert.strictEqual(body.data.requirement, 'ExtraSecret');
|
|
245
315
|
}
|
|
246
316
|
|
package/test/login.js
CHANGED
|
@@ -41,6 +41,48 @@ describe('Login', function() {
|
|
|
41
41
|
assert(doc._id);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('should throttle login attempts and show a proper error when the limit is reached', async function () {
|
|
45
|
+
const loginModule = apos.modules['@apostrophecms/login'];
|
|
46
|
+
const { allowedAttempts } = loginModule.options.throttle;
|
|
47
|
+
const jar = apos.http.jar();
|
|
48
|
+
const username = 'HarryPutter';
|
|
49
|
+
// establish session
|
|
50
|
+
const page = await apos.http.get(
|
|
51
|
+
'/',
|
|
52
|
+
{
|
|
53
|
+
jar
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
assert(page.match(/logged out/));
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index <= allowedAttempts; index++) {
|
|
60
|
+
try {
|
|
61
|
+
await apos.http.post(
|
|
62
|
+
'/api/v1/@apostrophecms/login/login',
|
|
63
|
+
{
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: {
|
|
66
|
+
username,
|
|
67
|
+
password: 'badpassword',
|
|
68
|
+
session: true
|
|
69
|
+
},
|
|
70
|
+
jar
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
} catch ({ body }) {
|
|
75
|
+
if (index < allowedAttempts) {
|
|
76
|
+
assert(body.message === 'Your credentials are incorrect, or there is no such user');
|
|
77
|
+
} else {
|
|
78
|
+
assert(body.message === 'Too many attempts. You may try again in a minute.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await loginModule.clearLoginAttempts(username);
|
|
84
|
+
});
|
|
85
|
+
|
|
44
86
|
it('should be able to login a user with their username', async function() {
|
|
45
87
|
|
|
46
88
|
const jar = apos.http.jar();
|
package/test/parked-pages.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
const t = require('../test-lib/test.js');
|
|
2
2
|
const assert = require('assert');
|
|
3
3
|
|
|
4
|
-
let apos, apos2;
|
|
4
|
+
let apos, apos2, apos3, apos4, apos5, apos6;
|
|
5
|
+
|
|
6
|
+
const park2 = [
|
|
7
|
+
{
|
|
8
|
+
slug: '/',
|
|
9
|
+
parkedId: 'home',
|
|
10
|
+
_defaults: {
|
|
11
|
+
title: 'Home',
|
|
12
|
+
type: 'default-page'
|
|
13
|
+
},
|
|
14
|
+
_children: [
|
|
15
|
+
{
|
|
16
|
+
slug: '/default1',
|
|
17
|
+
parkedId: 'default1',
|
|
18
|
+
_defaults: {
|
|
19
|
+
type: 'default-page',
|
|
20
|
+
title: 'Default 1'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
slug: '/default2',
|
|
25
|
+
parkedId: 'default2',
|
|
26
|
+
_defaults: {
|
|
27
|
+
type: 'default-page',
|
|
28
|
+
title: 'Default 2'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
];
|
|
5
34
|
|
|
6
35
|
describe('Parked Pages', function() {
|
|
7
36
|
|
|
@@ -10,12 +39,21 @@ describe('Parked Pages', function() {
|
|
|
10
39
|
after(async function() {
|
|
11
40
|
await t.destroy(apos);
|
|
12
41
|
await t.destroy(apos2);
|
|
42
|
+
await t.destroy(apos3);
|
|
43
|
+
await t.destroy(apos4);
|
|
44
|
+
await t.destroy(apos5);
|
|
45
|
+
await t.destroy(apos6);
|
|
13
46
|
});
|
|
14
47
|
|
|
15
|
-
it('standard parked pages should be as expected', async function() {
|
|
48
|
+
it('standard and custom parked pages should be as expected', async function() {
|
|
49
|
+
this.timeout(20000);
|
|
16
50
|
apos = await t.create({
|
|
17
51
|
root: module,
|
|
18
|
-
modules: {
|
|
52
|
+
modules: {
|
|
53
|
+
'default-page': {
|
|
54
|
+
extend: '@apostrophecms/page-type'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
19
57
|
});
|
|
20
58
|
const req = apos.task.getReq();
|
|
21
59
|
const home = await apos.page.find(req, { slug: '/' }).toObject();
|
|
@@ -29,21 +67,13 @@ describe('Parked Pages', function() {
|
|
|
29
67
|
});
|
|
30
68
|
|
|
31
69
|
it('overridden home page should work without disturbing archive', async function() {
|
|
70
|
+
this.timeout(20000);
|
|
32
71
|
apos2 = await t.create({
|
|
33
72
|
root: module,
|
|
34
73
|
modules: {
|
|
35
74
|
'@apostrophecms/page': {
|
|
36
75
|
options: {
|
|
37
|
-
park:
|
|
38
|
-
{
|
|
39
|
-
slug: '/',
|
|
40
|
-
parkedId: 'home',
|
|
41
|
-
_defaults: {
|
|
42
|
-
title: 'Home',
|
|
43
|
-
type: 'default-page'
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
]
|
|
76
|
+
park: park2
|
|
47
77
|
}
|
|
48
78
|
},
|
|
49
79
|
'default-page': {}
|
|
@@ -58,5 +88,246 @@ describe('Parked Pages', function() {
|
|
|
58
88
|
assert(archive);
|
|
59
89
|
assert(archive.parkedId === 'archive');
|
|
60
90
|
assert(archive.type === '@apostrophecms/archive-page');
|
|
91
|
+
await apos2.db.collection('verify').insertOne({
|
|
92
|
+
checkSameDb: true
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('all pages should have consistent aposDocId across draft and published', async function() {
|
|
97
|
+
this.timeout(20000);
|
|
98
|
+
await validate(apos2, [ '/', '/archive', '/default1', '/default2' ]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('third apos object should park a third child correctly when spinning up later on existing db', async function() {
|
|
102
|
+
this.timeout(20000);
|
|
103
|
+
apos3 = await t.create({
|
|
104
|
+
root: module,
|
|
105
|
+
modules: {
|
|
106
|
+
'@apostrophecms/page': {
|
|
107
|
+
options: {
|
|
108
|
+
park: [
|
|
109
|
+
...park2,
|
|
110
|
+
{
|
|
111
|
+
slug: '/default3',
|
|
112
|
+
parkedId: 'default3',
|
|
113
|
+
_defaults: {
|
|
114
|
+
type: 'default-page',
|
|
115
|
+
title: 'Default 3'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
'default-page': {}
|
|
122
|
+
},
|
|
123
|
+
shortName: apos2.options.shortName
|
|
124
|
+
});
|
|
125
|
+
// prove apos2 and apos3 are talking to the same db and it hasn't been erased
|
|
126
|
+
assert(await apos3.db.collection('verify').findOne({
|
|
127
|
+
checkSameDb: true
|
|
128
|
+
}));
|
|
61
129
|
});
|
|
130
|
+
|
|
131
|
+
it('all pages should have consistent aposDocId across draft and published', async function() {
|
|
132
|
+
await validate(apos3, [ '/', '/archive', '/default1', '/default2', '/default3' ]);
|
|
133
|
+
// Should be same db, make sure of that
|
|
134
|
+
await validate(apos2, [ '/', '/archive', '/default1', '/default2', '/default3' ]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('nested parked children work', async function() {
|
|
138
|
+
this.timeout(20000);
|
|
139
|
+
apos4 = await t.create({
|
|
140
|
+
root: module,
|
|
141
|
+
modules: {
|
|
142
|
+
'@apostrophecms/page': {
|
|
143
|
+
options: {
|
|
144
|
+
park: [
|
|
145
|
+
...park2,
|
|
146
|
+
{
|
|
147
|
+
slug: '/default3',
|
|
148
|
+
parkedId: 'default3',
|
|
149
|
+
_defaults: {
|
|
150
|
+
type: 'default-page',
|
|
151
|
+
title: 'Default 3'
|
|
152
|
+
},
|
|
153
|
+
_children: [
|
|
154
|
+
{
|
|
155
|
+
slug: '/default3/child1',
|
|
156
|
+
parkedId: 'default3child1',
|
|
157
|
+
_defaults: {
|
|
158
|
+
type: 'default-page',
|
|
159
|
+
title: 'Default 3 Child 1'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
'default-page': {}
|
|
168
|
+
},
|
|
169
|
+
shortName: apos2.options.shortName
|
|
170
|
+
});
|
|
171
|
+
await validate(apos4, [ '/', '/archive', '/default1', '/default2', '/default3', '/default3/child1' ]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('nested parked children work across locales if locales are added later', async function() {
|
|
175
|
+
this.timeout(20000);
|
|
176
|
+
apos5 = await t.create({
|
|
177
|
+
root: module,
|
|
178
|
+
modules: {
|
|
179
|
+
'@apostrophecms/i18n': {
|
|
180
|
+
options: {
|
|
181
|
+
locales: {
|
|
182
|
+
en: {
|
|
183
|
+
label: 'English',
|
|
184
|
+
hostname: 'en'
|
|
185
|
+
},
|
|
186
|
+
fr: {
|
|
187
|
+
label: 'French',
|
|
188
|
+
hostname: 'fr'
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
'@apostrophecms/page': {
|
|
194
|
+
options: {
|
|
195
|
+
park: [
|
|
196
|
+
...park2,
|
|
197
|
+
{
|
|
198
|
+
slug: '/default3',
|
|
199
|
+
parkedId: 'default3',
|
|
200
|
+
_defaults: {
|
|
201
|
+
type: 'default-page',
|
|
202
|
+
title: 'Default 3'
|
|
203
|
+
},
|
|
204
|
+
_children: [
|
|
205
|
+
{
|
|
206
|
+
slug: '/default3/child1',
|
|
207
|
+
parkedId: 'default3child1',
|
|
208
|
+
_defaults: {
|
|
209
|
+
type: 'default-page',
|
|
210
|
+
title: 'Default 3 Child 1'
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
'default-page': {}
|
|
219
|
+
},
|
|
220
|
+
shortName: apos4.options.shortName
|
|
221
|
+
});
|
|
222
|
+
await validate(apos5, [ '/', '/archive', '/default1', '/default2', '/default3', '/default3/child1' ]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('nested parked children work across locales if locales are present from the start', async function() {
|
|
227
|
+
this.timeout(20000);
|
|
228
|
+
apos6 = await t.create({
|
|
229
|
+
root: module,
|
|
230
|
+
modules: {
|
|
231
|
+
'@apostrophecms/i18n': {
|
|
232
|
+
options: {
|
|
233
|
+
locales: {
|
|
234
|
+
en: {
|
|
235
|
+
label: 'English',
|
|
236
|
+
hostname: 'en'
|
|
237
|
+
},
|
|
238
|
+
fr: {
|
|
239
|
+
label: 'French',
|
|
240
|
+
hostname: 'fr'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
'@apostrophecms/page': {
|
|
246
|
+
options: {
|
|
247
|
+
park: [
|
|
248
|
+
...park2,
|
|
249
|
+
{
|
|
250
|
+
slug: '/default3',
|
|
251
|
+
parkedId: 'default3',
|
|
252
|
+
_defaults: {
|
|
253
|
+
type: 'default-page',
|
|
254
|
+
title: 'Default 3'
|
|
255
|
+
},
|
|
256
|
+
_children: [
|
|
257
|
+
{
|
|
258
|
+
slug: '/default3/child1',
|
|
259
|
+
parkedId: 'default3child1',
|
|
260
|
+
_defaults: {
|
|
261
|
+
type: 'default-page',
|
|
262
|
+
title: 'Default 3 Child 1'
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
'default-page': {}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
await validate(apos6, [ '/', '/archive', '/default1', '/default2', '/default3', '/default3/child1' ]);
|
|
62
274
|
});
|
|
275
|
+
|
|
276
|
+
async function validate(apos, expected) {
|
|
277
|
+
const locales = Object.keys(apos.i18n.locales);
|
|
278
|
+
const slugs = await apos.doc.db.distinct('slug', {
|
|
279
|
+
slug: /^\//
|
|
280
|
+
});
|
|
281
|
+
slugs.sort();
|
|
282
|
+
assert.deepStrictEqual(slugs, expected);
|
|
283
|
+
const pages = await apos.doc.db.find({
|
|
284
|
+
slug: /^\//
|
|
285
|
+
}).toArray();
|
|
286
|
+
assert(pages.length === slugs.length * 2 * locales.length);
|
|
287
|
+
for (const slug of slugs) {
|
|
288
|
+
const matches = pages.filter(page => page.slug === slug);
|
|
289
|
+
matches.sort((p1, p2) => {
|
|
290
|
+
if (p1.aposLocale < p2.aposLocale) {
|
|
291
|
+
return -1;
|
|
292
|
+
} else if (p1.aposLocale > p2.aposLocale) {
|
|
293
|
+
return 1;
|
|
294
|
+
} else {
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
assert.strictEqual(matches.length, 2 * locales.length);
|
|
299
|
+
let i = 0;
|
|
300
|
+
let aposDocId;
|
|
301
|
+
for (const locale of locales) {
|
|
302
|
+
const draft = i;
|
|
303
|
+
const published = i + 1;
|
|
304
|
+
assert.strictEqual(matches[draft].aposLocale, `${locale}:draft`);
|
|
305
|
+
assert.strictEqual(matches[published].aposLocale, `${locale}:published`);
|
|
306
|
+
assert(matches[draft].aposDocId);
|
|
307
|
+
assert.strictEqual(matches[draft].aposDocId, matches[published].aposDocId);
|
|
308
|
+
if (!aposDocId) {
|
|
309
|
+
aposDocId = matches[draft].aposDocId;
|
|
310
|
+
} else {
|
|
311
|
+
assert.strictEqual(matches[draft].aposDocId, aposDocId);
|
|
312
|
+
}
|
|
313
|
+
assert.strictEqual(matches[draft]._id, `${aposDocId}:${locale}:draft`);
|
|
314
|
+
assert.strictEqual(matches[published]._id, `${aposDocId}:${locale}:published`);
|
|
315
|
+
i += 2;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const home = await apos.page.find(apos.task.getReq(), {
|
|
319
|
+
slug: '/'
|
|
320
|
+
}).children({
|
|
321
|
+
depth: 2
|
|
322
|
+
}).toObject();
|
|
323
|
+
const children = expected.filter(slug => slug.startsWith('/default') && !slug.match(/\/.*\//));
|
|
324
|
+
assert.deepStrictEqual(home._children.map(child => child.slug), children);
|
|
325
|
+
const grandkids = expected.filter(slug => slug.match(/\/.*\//));
|
|
326
|
+
for (const grandkid of grandkids) {
|
|
327
|
+
const parentSlug = grandkid.replace(/\/[^/]+$/, '');
|
|
328
|
+
const parent = home._children.find(child => child.slug === parentSlug);
|
|
329
|
+
assert(parent);
|
|
330
|
+
assert(parent._children);
|
|
331
|
+
assert(parent._children.find(child => child.slug === grandkid));
|
|
332
|
+
}
|
|
333
|
+
}
|