apostrophe 4.27.1 → 4.28.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 +35 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +71 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +6 -6
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +135 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- package/test/universal-graph.js +1135 -0
|
@@ -0,0 +1,2701 @@
|
|
|
1
|
+
const t = require('../test-lib/test.js');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
|
|
4
|
+
describe('Static Build Support', function () {
|
|
5
|
+
this.timeout(t.timeout);
|
|
6
|
+
|
|
7
|
+
describe('URL helper methods', function () {
|
|
8
|
+
let apos;
|
|
9
|
+
|
|
10
|
+
before(async function () {
|
|
11
|
+
apos = await t.create({
|
|
12
|
+
root: module,
|
|
13
|
+
modules: {
|
|
14
|
+
'@apostrophecms/url': {}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
after(async function () {
|
|
20
|
+
await t.destroy(apos);
|
|
21
|
+
apos = null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should initialize with static: false (default)', async function () {
|
|
25
|
+
assert(apos.url);
|
|
26
|
+
assert.strictEqual(apos.url.options.static, false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('getChoiceFilter returns query string format when static is false', function () {
|
|
30
|
+
assert.strictEqual(
|
|
31
|
+
apos.url.getChoiceFilter('category', 'tech', 1),
|
|
32
|
+
'?category=tech'
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('getChoiceFilter returns query string with page when static is false', function () {
|
|
37
|
+
assert.strictEqual(
|
|
38
|
+
apos.url.getChoiceFilter('category', 'tech', 2),
|
|
39
|
+
'?category=tech&page=2'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('getChoiceFilter returns empty string for null value', function () {
|
|
44
|
+
assert.strictEqual(apos.url.getChoiceFilter('category', null, 1), '');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('getChoiceFilter encodes special characters', function () {
|
|
48
|
+
assert.strictEqual(
|
|
49
|
+
apos.url.getChoiceFilter('my filter', 'hello world', 1),
|
|
50
|
+
'?my%20filter=hello%20world'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('getPageFilter returns empty string for page 1', function () {
|
|
55
|
+
assert.strictEqual(apos.url.getPageFilter(1), '');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('getPageFilter returns query string for page > 1 when static is false', function () {
|
|
59
|
+
assert.strictEqual(apos.url.getPageFilter(2), '?page=2');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Static mode URL helpers', function () {
|
|
64
|
+
let apos;
|
|
65
|
+
|
|
66
|
+
before(async function () {
|
|
67
|
+
apos = await t.create({
|
|
68
|
+
root: module,
|
|
69
|
+
modules: {
|
|
70
|
+
'@apostrophecms/url': {
|
|
71
|
+
options: { static: true }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
after(async function () {
|
|
78
|
+
await t.destroy(apos);
|
|
79
|
+
apos = null;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should initialize with static: true', async function () {
|
|
83
|
+
assert.strictEqual(apos.url.options.static, true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('getChoiceFilter returns path format when static is true', function () {
|
|
87
|
+
assert.strictEqual(
|
|
88
|
+
apos.url.getChoiceFilter('category', 'tech', 1),
|
|
89
|
+
'/category/tech'
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('getChoiceFilter returns path with page when static is true', function () {
|
|
94
|
+
assert.strictEqual(
|
|
95
|
+
apos.url.getChoiceFilter('category', 'tech', 2),
|
|
96
|
+
'/category/tech/page/2'
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('getPageFilter returns path format for page > 1 when static is true', function () {
|
|
101
|
+
assert.strictEqual(apos.url.getPageFilter(2), '/page/2');
|
|
102
|
+
assert.strictEqual(apos.url.getPageFilter(3), '/page/3');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('getPageFilter still returns empty string for page 1 in static mode', function () {
|
|
106
|
+
assert.strictEqual(apos.url.getPageFilter(1), '');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getAllUrlMetadata', function () {
|
|
111
|
+
let apos;
|
|
112
|
+
|
|
113
|
+
before(async function () {
|
|
114
|
+
apos = await t.create({
|
|
115
|
+
root: module,
|
|
116
|
+
modules: {
|
|
117
|
+
'@apostrophecms/url': {
|
|
118
|
+
options: { static: true }
|
|
119
|
+
},
|
|
120
|
+
article: {
|
|
121
|
+
extend: '@apostrophecms/piece-type',
|
|
122
|
+
options: {
|
|
123
|
+
name: 'article',
|
|
124
|
+
label: 'Article',
|
|
125
|
+
alias: 'article',
|
|
126
|
+
sort: { title: 1 }
|
|
127
|
+
},
|
|
128
|
+
fields: {
|
|
129
|
+
add: {
|
|
130
|
+
category: {
|
|
131
|
+
type: 'select',
|
|
132
|
+
label: 'Category',
|
|
133
|
+
choices: [
|
|
134
|
+
{
|
|
135
|
+
label: 'Tech',
|
|
136
|
+
value: 'tech'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: 'Science',
|
|
140
|
+
value: 'science'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'Art',
|
|
144
|
+
value: 'art'
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
'article-page': {
|
|
152
|
+
extend: '@apostrophecms/piece-page-type',
|
|
153
|
+
options: {
|
|
154
|
+
name: 'articlePage',
|
|
155
|
+
label: 'Articles',
|
|
156
|
+
alias: 'articlePage',
|
|
157
|
+
perPage: 5,
|
|
158
|
+
piecesFilters: [
|
|
159
|
+
{ name: 'category' }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
'@apostrophecms/page': {
|
|
164
|
+
options: {
|
|
165
|
+
park: [
|
|
166
|
+
{
|
|
167
|
+
title: 'Articles',
|
|
168
|
+
type: 'articlePage',
|
|
169
|
+
slug: '/articles',
|
|
170
|
+
parkedId: 'articles'
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Insert 12 articles across 3 categories
|
|
179
|
+
const req = apos.task.getReq();
|
|
180
|
+
for (let i = 1; i <= 12; i++) {
|
|
181
|
+
const padded = String(i).padStart(3, '0');
|
|
182
|
+
const categories = [ 'tech', 'science', 'art' ];
|
|
183
|
+
const category = categories[(i - 1) % 3];
|
|
184
|
+
await apos.article.insert(req, {
|
|
185
|
+
title: `Article ${padded}`,
|
|
186
|
+
slug: `article-${padded}`,
|
|
187
|
+
visibility: 'public',
|
|
188
|
+
category
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
after(async function () {
|
|
194
|
+
await t.destroy(apos);
|
|
195
|
+
apos = null;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return URL metadata for all documents', async function () {
|
|
199
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
200
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
201
|
+
assert(Array.isArray(results));
|
|
202
|
+
assert(results.length > 0);
|
|
203
|
+
|
|
204
|
+
const articlesPage = results.find(r => r.url === '/articles');
|
|
205
|
+
assert(articlesPage, 'Should include the articles index page');
|
|
206
|
+
assert.strictEqual(articlesPage.type, 'articlePage');
|
|
207
|
+
assert(articlesPage.aposDocId);
|
|
208
|
+
assert(articlesPage.i18nId);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should include individual article URLs', async function () {
|
|
212
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
213
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
214
|
+
const articleUrls = results.filter(r => r.type === 'article');
|
|
215
|
+
|
|
216
|
+
assert.strictEqual(articleUrls.length, 12);
|
|
217
|
+
assert(articleUrls.every(a => a.url.startsWith('/articles/article-')));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('document entries should not have contentType', async function () {
|
|
221
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
222
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
223
|
+
const docEntries = results.filter(r => r.aposDocId);
|
|
224
|
+
|
|
225
|
+
assert(docEntries.length > 0, 'Should have document entries');
|
|
226
|
+
for (const entry of docEntries) {
|
|
227
|
+
assert.strictEqual(
|
|
228
|
+
entry.contentType,
|
|
229
|
+
undefined,
|
|
230
|
+
`Document entry ${entry.url} should not have contentType`
|
|
231
|
+
);
|
|
232
|
+
assert.notStrictEqual(
|
|
233
|
+
entry.sitemap,
|
|
234
|
+
false,
|
|
235
|
+
`Document entry ${entry.url} should not set sitemap: false`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should include filter URLs in static mode', async function () {
|
|
241
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
242
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
243
|
+
|
|
244
|
+
// Should include filter URLs like /articles/category/tech
|
|
245
|
+
const filterUrls = results.filter(
|
|
246
|
+
r => r.url && r.url.match(/\/articles\/category\//)
|
|
247
|
+
);
|
|
248
|
+
assert(filterUrls.length > 0, 'Should include filter URLs');
|
|
249
|
+
|
|
250
|
+
// Should have entries for each category with pieces
|
|
251
|
+
const categories = [ 'tech', 'science', 'art' ];
|
|
252
|
+
for (const cat of categories) {
|
|
253
|
+
const catUrl = filterUrls.find(
|
|
254
|
+
r => r.url === `/articles/category/${cat}`
|
|
255
|
+
);
|
|
256
|
+
assert(catUrl, `Should include URL for category: ${cat}`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should include pagination URLs in static mode', async function () {
|
|
261
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
262
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
263
|
+
|
|
264
|
+
// 12 articles with perPage=5 means 3 pages.
|
|
265
|
+
// Page 1 is the base URL (/articles), so only /page/2 and /page/3
|
|
266
|
+
// should appear as separate entries.
|
|
267
|
+
const paginationUrls = results.filter(
|
|
268
|
+
r => r.url && r.url.match(/\/articles\/page\/\d+$/)
|
|
269
|
+
);
|
|
270
|
+
assert.strictEqual(
|
|
271
|
+
paginationUrls.length,
|
|
272
|
+
2,
|
|
273
|
+
'Should have exactly 2 pagination URLs'
|
|
274
|
+
);
|
|
275
|
+
assert(
|
|
276
|
+
paginationUrls.some(r => r.url === '/articles/page/2'),
|
|
277
|
+
'Should include page 2'
|
|
278
|
+
);
|
|
279
|
+
assert(
|
|
280
|
+
paginationUrls.some(r => r.url === '/articles/page/3'),
|
|
281
|
+
'Should include page 3'
|
|
282
|
+
);
|
|
283
|
+
assert(
|
|
284
|
+
!paginationUrls.some(r => r.url === '/articles/page/1'),
|
|
285
|
+
'Should not include page 1 (that is the base URL)'
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('filter URLs should use path format in static mode', async function () {
|
|
290
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
291
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
292
|
+
const filterUrls = results.filter(
|
|
293
|
+
r => r.url && r.url.match(/\/articles\/category\//)
|
|
294
|
+
);
|
|
295
|
+
// 3 categories with 4 articles each, perPage=5: 1 page per category,
|
|
296
|
+
// so exactly 3 filter URLs (no paginated filter URLs)
|
|
297
|
+
assert.strictEqual(filterUrls.length, 3, 'Should have exactly 3 filter URLs');
|
|
298
|
+
for (const entry of filterUrls) {
|
|
299
|
+
assert(!entry.url.includes('?'), `URL should not contain query string: ${entry.url}`);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should have consistent i18nId values', async function () {
|
|
304
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
305
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
306
|
+
for (const entry of results) {
|
|
307
|
+
assert(entry.i18nId, `Entry with url ${entry.url} should have i18nId`);
|
|
308
|
+
}
|
|
309
|
+
const ids = results.map(r => r.i18nId);
|
|
310
|
+
const unique = new Set(ids);
|
|
311
|
+
assert.strictEqual(
|
|
312
|
+
unique.size,
|
|
313
|
+
ids.length,
|
|
314
|
+
'All i18nId values should be unique'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should include home page URL', async function () {
|
|
319
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
320
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
321
|
+
const home = results.find(r => r.url === '/');
|
|
322
|
+
assert(home, 'Should include the home page');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should respect excludeTypes option', async function () {
|
|
326
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
327
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req, {
|
|
328
|
+
excludeTypes: [ 'article' ]
|
|
329
|
+
});
|
|
330
|
+
const articles = results.filter(r => r.type === 'article');
|
|
331
|
+
assert.strictEqual(articles.length, 0, 'Should not include excluded types');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('getAllUrlMetadata with literal content entries', function () {
|
|
336
|
+
let apos;
|
|
337
|
+
|
|
338
|
+
before(async function () {
|
|
339
|
+
apos = await t.create({
|
|
340
|
+
root: module,
|
|
341
|
+
modules: {
|
|
342
|
+
'@apostrophecms/url': {
|
|
343
|
+
options: { static: true }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
// Simulate a styles stylesheet being present in the global doc
|
|
348
|
+
// by setting it directly in the database
|
|
349
|
+
await apos.doc.db.updateOne(
|
|
350
|
+
{
|
|
351
|
+
type: '@apostrophecms/global',
|
|
352
|
+
aposLocale: 'en:published'
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
$set: {
|
|
356
|
+
stylesStylesheet: 'body { color: red; }',
|
|
357
|
+
stylesStylesheetVersion: 'test-version'
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
after(async function () {
|
|
364
|
+
await t.destroy(apos);
|
|
365
|
+
apos = null;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should include styles stylesheet as a literal content entry', async function () {
|
|
369
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
370
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
371
|
+
const stylesheet = results.find(
|
|
372
|
+
r => r.i18nId === '@apostrophecms/styles:stylesheet'
|
|
373
|
+
);
|
|
374
|
+
assert(stylesheet, 'Should include styles stylesheet entry');
|
|
375
|
+
assert.strictEqual(stylesheet.contentType, 'text/css');
|
|
376
|
+
assert.strictEqual(stylesheet.sitemap, false, 'Literal content entries should have sitemap: false');
|
|
377
|
+
assert(
|
|
378
|
+
stylesheet.url.includes('/api/v1/@apostrophecms/styles/stylesheet'),
|
|
379
|
+
'URL should point to the styles API route'
|
|
380
|
+
);
|
|
381
|
+
assert(
|
|
382
|
+
stylesheet.url.includes('version=test-version'),
|
|
383
|
+
'URL should include the stylesheet version for cache busting'
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('literal content entries have contentType property', async function () {
|
|
388
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
389
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
390
|
+
const literals = results.filter(r => r.contentType);
|
|
391
|
+
|
|
392
|
+
for (const entry of literals) {
|
|
393
|
+
assert(typeof entry.contentType === 'string');
|
|
394
|
+
assert(entry.url);
|
|
395
|
+
assert(entry.i18nId);
|
|
396
|
+
assert.strictEqual(entry.sitemap, false, 'Literal content entries should opt out of sitemaps');
|
|
397
|
+
assert(!entry.changefreq, 'Literal content entries should not have changefreq');
|
|
398
|
+
assert(!entry.priority, 'Literal content entries should not have priority');
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('getAllUrlMetadata with attachments', function () {
|
|
404
|
+
let apos;
|
|
405
|
+
|
|
406
|
+
before(async function () {
|
|
407
|
+
apos = await t.create({
|
|
408
|
+
root: module,
|
|
409
|
+
modules: {
|
|
410
|
+
'@apostrophecms/url': {
|
|
411
|
+
options: { static: true }
|
|
412
|
+
},
|
|
413
|
+
article: {
|
|
414
|
+
extend: '@apostrophecms/piece-type',
|
|
415
|
+
options: {
|
|
416
|
+
name: 'article',
|
|
417
|
+
label: 'Article',
|
|
418
|
+
alias: 'article'
|
|
419
|
+
},
|
|
420
|
+
fields: {
|
|
421
|
+
add: {
|
|
422
|
+
_image: {
|
|
423
|
+
type: 'relationship',
|
|
424
|
+
withType: '@apostrophecms/image',
|
|
425
|
+
label: 'Image',
|
|
426
|
+
max: 1
|
|
427
|
+
},
|
|
428
|
+
_file: {
|
|
429
|
+
type: 'relationship',
|
|
430
|
+
withType: '@apostrophecms/file',
|
|
431
|
+
label: 'File',
|
|
432
|
+
max: 1
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
'article-page': {
|
|
438
|
+
extend: '@apostrophecms/piece-page-type',
|
|
439
|
+
options: {
|
|
440
|
+
name: 'articlePage',
|
|
441
|
+
label: 'Articles',
|
|
442
|
+
alias: 'articlePage',
|
|
443
|
+
perPage: 10
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
'@apostrophecms/page': {
|
|
447
|
+
options: {
|
|
448
|
+
park: [
|
|
449
|
+
{
|
|
450
|
+
title: 'Articles',
|
|
451
|
+
type: 'articlePage',
|
|
452
|
+
slug: '/articles',
|
|
453
|
+
parkedId: 'articles'
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const req = apos.task.getReq();
|
|
462
|
+
|
|
463
|
+
// Insert an article so we have a document with a known _id
|
|
464
|
+
const article = await apos.article.insert(req, {
|
|
465
|
+
title: 'Attachment Test Article',
|
|
466
|
+
visibility: 'public'
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Update the article raw record to reference image and file
|
|
470
|
+
// docs via idsStorage fields, as if a user had chosen media
|
|
471
|
+
// through the CMS UI.
|
|
472
|
+
await apos.doc.db.updateMany(
|
|
473
|
+
{ aposDocId: article.aposDocId },
|
|
474
|
+
{
|
|
475
|
+
$set: {
|
|
476
|
+
imageIds: [ 'img-1' ],
|
|
477
|
+
fileIds: [ 'file-1' ]
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Seed attachment records directly into the DB to avoid
|
|
483
|
+
// needing real uploaded files. Attachment `docIds` reference
|
|
484
|
+
// the image/file doc IDs (not the article), matching how the
|
|
485
|
+
// core attachment module stores references.
|
|
486
|
+
const imgDocId = 'img-1:en:published';
|
|
487
|
+
const fileDocId = 'file-1:en:published';
|
|
488
|
+
|
|
489
|
+
await apos.attachment.db.insertMany([
|
|
490
|
+
{
|
|
491
|
+
_id: 'att-jpg-1',
|
|
492
|
+
name: 'photo',
|
|
493
|
+
extension: 'jpg',
|
|
494
|
+
group: 'images',
|
|
495
|
+
width: 800,
|
|
496
|
+
height: 600,
|
|
497
|
+
archived: false,
|
|
498
|
+
docIds: [ imgDocId ],
|
|
499
|
+
crops: [],
|
|
500
|
+
used: true,
|
|
501
|
+
utilized: true
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
_id: 'att-pdf-1',
|
|
505
|
+
name: 'document',
|
|
506
|
+
extension: 'pdf',
|
|
507
|
+
group: 'office',
|
|
508
|
+
archived: false,
|
|
509
|
+
docIds: [ fileDocId ],
|
|
510
|
+
crops: [],
|
|
511
|
+
used: true,
|
|
512
|
+
utilized: true
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
_id: 'att-orphan-1',
|
|
516
|
+
name: 'orphan',
|
|
517
|
+
extension: 'png',
|
|
518
|
+
group: 'images',
|
|
519
|
+
width: 100,
|
|
520
|
+
height: 100,
|
|
521
|
+
archived: false,
|
|
522
|
+
docIds: [ 'img-orphan:en:published' ],
|
|
523
|
+
crops: [],
|
|
524
|
+
used: false,
|
|
525
|
+
utilized: false
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
_id: 'att-archived-1',
|
|
529
|
+
name: 'archived-photo',
|
|
530
|
+
extension: 'jpg',
|
|
531
|
+
group: 'images',
|
|
532
|
+
width: 200,
|
|
533
|
+
height: 200,
|
|
534
|
+
archived: true,
|
|
535
|
+
docIds: [ imgDocId ],
|
|
536
|
+
crops: [],
|
|
537
|
+
used: true,
|
|
538
|
+
utilized: true
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
_id: 'att-cropped-1',
|
|
542
|
+
name: 'cropped-photo',
|
|
543
|
+
extension: 'jpg',
|
|
544
|
+
group: 'images',
|
|
545
|
+
width: 1000,
|
|
546
|
+
height: 800,
|
|
547
|
+
archived: false,
|
|
548
|
+
docIds: [ imgDocId ],
|
|
549
|
+
crops: [
|
|
550
|
+
{
|
|
551
|
+
top: 10,
|
|
552
|
+
left: 20,
|
|
553
|
+
width: 300,
|
|
554
|
+
height: 400
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
used: true,
|
|
558
|
+
utilized: true
|
|
559
|
+
}
|
|
560
|
+
]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
after(async function () {
|
|
564
|
+
await t.destroy(apos);
|
|
565
|
+
apos = null;
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should return attachments as null when not requested', async function () {
|
|
569
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
570
|
+
const result = await apos.url.getAllUrlMetadata(req);
|
|
571
|
+
assert.strictEqual(result.attachments, null);
|
|
572
|
+
assert(Array.isArray(result.pages));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should return attachment metadata when requested', async function () {
|
|
576
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
577
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
578
|
+
attachments: { scope: 'used' }
|
|
579
|
+
});
|
|
580
|
+
assert(result.attachments);
|
|
581
|
+
assert(typeof result.attachments.uploadsUrl === 'string');
|
|
582
|
+
assert(Array.isArray(result.attachments.results));
|
|
583
|
+
assert(result.attachments.results.length > 0);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('used scope should only include attachments referenced by content docs', async function () {
|
|
587
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
588
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
589
|
+
attachments: { scope: 'used' }
|
|
590
|
+
});
|
|
591
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
592
|
+
assert(ids.includes('att-jpg-1'), 'Should include attachment referenced via image relationship');
|
|
593
|
+
assert(ids.includes('att-pdf-1'), 'Should include attachment referenced via file relationship');
|
|
594
|
+
assert(!ids.includes('att-orphan-1'), 'Should not include attachment whose image doc is unreferenced by content');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('all scope should include all attachments', async function () {
|
|
598
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
599
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
600
|
+
attachments: { scope: 'all' }
|
|
601
|
+
});
|
|
602
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
603
|
+
assert(ids.includes('att-jpg-1'));
|
|
604
|
+
assert(ids.includes('att-pdf-1'));
|
|
605
|
+
assert(ids.includes('att-orphan-1'), 'all scope should include attachments not referenced by content docs');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('sized attachment should have multiple size variants', async function () {
|
|
609
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
610
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
611
|
+
attachments: { scope: 'used' }
|
|
612
|
+
});
|
|
613
|
+
const jpgAtt = result.attachments.results.find(a => a._id === 'att-jpg-1');
|
|
614
|
+
assert(jpgAtt, 'Should find the jpg attachment');
|
|
615
|
+
assert(jpgAtt.urls.length > 1, 'Sized attachment should have multiple URL entries');
|
|
616
|
+
const sizeNames = jpgAtt.urls.map(u => u.size);
|
|
617
|
+
assert(sizeNames.includes('full'), 'Should include full size');
|
|
618
|
+
assert(sizeNames.includes('one-half'), 'Should include one-half size');
|
|
619
|
+
assert(sizeNames.includes('original'), 'Should include original size');
|
|
620
|
+
for (const entry of jpgAtt.urls) {
|
|
621
|
+
assert(typeof entry.path === 'string');
|
|
622
|
+
assert(entry.path.startsWith('/attachments/'));
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('non-sized attachment should have only a path', async function () {
|
|
627
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
628
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
629
|
+
attachments: { scope: 'used' }
|
|
630
|
+
});
|
|
631
|
+
const pdfAtt = result.attachments.results.find(a => a._id === 'att-pdf-1');
|
|
632
|
+
assert(pdfAtt, 'Should find the pdf attachment');
|
|
633
|
+
assert.strictEqual(pdfAtt.urls.length, 1, 'Non-sized attachment should have one entry');
|
|
634
|
+
assert.strictEqual(
|
|
635
|
+
pdfAtt.urls[0].size,
|
|
636
|
+
undefined,
|
|
637
|
+
'Non-sized attachment should not have a size property'
|
|
638
|
+
);
|
|
639
|
+
assert(pdfAtt.urls[0].path.includes('.pdf'));
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('skipSizes should exclude specified sizes', async function () {
|
|
643
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
644
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
645
|
+
attachments: {
|
|
646
|
+
scope: 'used',
|
|
647
|
+
skipSizes: [ 'original', 'max' ]
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
const jpgAtt = result.attachments.results.find(a => a._id === 'att-jpg-1');
|
|
651
|
+
const sizeNames = jpgAtt.urls.map(u => u.size);
|
|
652
|
+
assert(!sizeNames.includes('original'), 'original should be skipped');
|
|
653
|
+
assert(!sizeNames.includes('max'), 'max should be skipped');
|
|
654
|
+
assert(sizeNames.includes('full'), 'full should still be present');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('sizes should include only specified sizes', async function () {
|
|
658
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
659
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
660
|
+
attachments: {
|
|
661
|
+
scope: 'used',
|
|
662
|
+
sizes: [ 'full', 'one-half' ]
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
const jpgAtt = result.attachments.results.find(a => a._id === 'att-jpg-1');
|
|
666
|
+
const sizeNames = jpgAtt.urls.map(u => u.size);
|
|
667
|
+
assert(sizeNames.includes('full'));
|
|
668
|
+
assert(sizeNames.includes('one-half'));
|
|
669
|
+
assert(!sizeNames.includes('original'), 'original should not be included when sizes is explicit');
|
|
670
|
+
assert(!sizeNames.includes('max'), 'max should not be included when sizes is explicit');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('uploadsUrl should match the uploadfs base URL', async function () {
|
|
674
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
675
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
676
|
+
attachments: { scope: 'used' }
|
|
677
|
+
});
|
|
678
|
+
assert.strictEqual(
|
|
679
|
+
result.attachments.uploadsUrl,
|
|
680
|
+
apos.attachment.uploadfs.getUrl()
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should exclude archived attachments even if they have docIds', async function () {
|
|
685
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
686
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
687
|
+
attachments: { scope: 'used' }
|
|
688
|
+
});
|
|
689
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
690
|
+
assert(!ids.includes('att-archived-1'), 'Archived attachments should be excluded');
|
|
691
|
+
assert(ids.includes('att-jpg-1'), 'Non-archived attachments should be included');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should exclude archived attachments in all scope too', async function () {
|
|
695
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
696
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
697
|
+
attachments: { scope: 'all' }
|
|
698
|
+
});
|
|
699
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
700
|
+
assert(!ids.includes('att-archived-1'), 'Archived attachments should be excluded in all scope');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('crop variants should include all sizes by default', async function () {
|
|
704
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
705
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
706
|
+
attachments: { scope: 'used' }
|
|
707
|
+
});
|
|
708
|
+
const att = result.attachments.results.find(a => a._id === 'att-cropped-1');
|
|
709
|
+
assert(att, 'Should find the cropped attachment');
|
|
710
|
+
// Should have all regular sizes + all crop sizes
|
|
711
|
+
const cropUrls = att.urls.filter(u => u.path.includes('.20.'));
|
|
712
|
+
assert(cropUrls.length > 0, 'Should have crop variant URLs');
|
|
713
|
+
const cropSizes = cropUrls.map(u => u.size);
|
|
714
|
+
assert(cropSizes.includes('full'), 'Crop should include full size');
|
|
715
|
+
assert(cropSizes.includes('original'), 'Crop should include original size');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('crop variants should respect skipSizes', async function () {
|
|
719
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
720
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
721
|
+
attachments: {
|
|
722
|
+
scope: 'used',
|
|
723
|
+
skipSizes: [ 'original', 'max' ]
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
const att = result.attachments.results.find(a => a._id === 'att-cropped-1');
|
|
727
|
+
const cropUrls = att.urls.filter(u => u.path.includes('.20.'));
|
|
728
|
+
const cropSizes = cropUrls.map(u => u.size);
|
|
729
|
+
assert(!cropSizes.includes('original'), 'Crop should skip original when told to');
|
|
730
|
+
assert(!cropSizes.includes('max'), 'Crop should skip max when told to');
|
|
731
|
+
assert(cropSizes.includes('full'), 'Crop should still include full');
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('crop variants should respect sizes filter', async function () {
|
|
735
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
736
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
737
|
+
attachments: {
|
|
738
|
+
scope: 'used',
|
|
739
|
+
sizes: [ 'full', 'one-half' ]
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
const att = result.attachments.results.find(a => a._id === 'att-cropped-1');
|
|
743
|
+
const cropUrls = att.urls.filter(u => u.path.includes('.20.'));
|
|
744
|
+
const cropSizes = cropUrls.map(u => u.size);
|
|
745
|
+
assert.strictEqual(cropSizes.length, 2, 'Crop should only have the 2 requested sizes');
|
|
746
|
+
assert(cropSizes.includes('full'));
|
|
747
|
+
assert(cropSizes.includes('one-half'));
|
|
748
|
+
assert(!cropSizes.includes('original'));
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('used scope with direct attachment fields', function () {
|
|
753
|
+
let apos;
|
|
754
|
+
|
|
755
|
+
before(async function () {
|
|
756
|
+
apos = await t.create({
|
|
757
|
+
root: module,
|
|
758
|
+
modules: {
|
|
759
|
+
'@apostrophecms/url': {
|
|
760
|
+
options: { static: true }
|
|
761
|
+
},
|
|
762
|
+
// Piece type with a direct attachment field in its own schema
|
|
763
|
+
'direct-attachment-piece': {
|
|
764
|
+
extend: '@apostrophecms/piece-type',
|
|
765
|
+
options: {
|
|
766
|
+
name: 'direct-attachment-piece',
|
|
767
|
+
label: 'Direct Attachment Piece',
|
|
768
|
+
alias: 'directAttachmentPiece'
|
|
769
|
+
},
|
|
770
|
+
fields: {
|
|
771
|
+
add: {
|
|
772
|
+
file: {
|
|
773
|
+
type: 'attachment',
|
|
774
|
+
label: 'File',
|
|
775
|
+
group: 'office'
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
// Widget with a direct attachment field (not a relationship)
|
|
781
|
+
'attachment-widget': {
|
|
782
|
+
extend: '@apostrophecms/widget-type',
|
|
783
|
+
options: {
|
|
784
|
+
label: 'Attachment Widget'
|
|
785
|
+
},
|
|
786
|
+
fields: {
|
|
787
|
+
add: {
|
|
788
|
+
photo: {
|
|
789
|
+
type: 'attachment',
|
|
790
|
+
label: 'Photo',
|
|
791
|
+
fileGroup: 'images'
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
// Page type with an area that allows the attachment widget
|
|
797
|
+
'test-page': {
|
|
798
|
+
extend: '@apostrophecms/page-type',
|
|
799
|
+
options: {
|
|
800
|
+
label: 'Test Page'
|
|
801
|
+
},
|
|
802
|
+
fields: {
|
|
803
|
+
add: {
|
|
804
|
+
body: {
|
|
805
|
+
type: 'area',
|
|
806
|
+
label: 'Body',
|
|
807
|
+
options: {
|
|
808
|
+
widgets: {
|
|
809
|
+
attachment: {}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
'@apostrophecms/page': {
|
|
817
|
+
options: {
|
|
818
|
+
types: [
|
|
819
|
+
{
|
|
820
|
+
name: 'test-page',
|
|
821
|
+
label: 'Test Page'
|
|
822
|
+
}
|
|
823
|
+
],
|
|
824
|
+
park: [
|
|
825
|
+
{
|
|
826
|
+
title: 'Widget Attachment Page',
|
|
827
|
+
type: 'test-page',
|
|
828
|
+
slug: '/widget-att',
|
|
829
|
+
parkedId: 'widget-att'
|
|
830
|
+
}
|
|
831
|
+
]
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const req = apos.task.getReq();
|
|
838
|
+
|
|
839
|
+
// --- Piece with a direct attachment field ---
|
|
840
|
+
const piece = await apos.directAttachmentPiece.insert(req, {
|
|
841
|
+
title: 'Piece With Direct Attachment',
|
|
842
|
+
visibility: 'public'
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// Seed an attachment referencing the piece doc (as
|
|
846
|
+
// updateDocReferences would do at save time)
|
|
847
|
+
const pieceDocId = `${piece.aposDocId}:en:published`;
|
|
848
|
+
await apos.attachment.db.insertOne({
|
|
849
|
+
_id: 'att-direct-piece',
|
|
850
|
+
name: 'piece-doc',
|
|
851
|
+
extension: 'pdf',
|
|
852
|
+
group: 'office',
|
|
853
|
+
archived: false,
|
|
854
|
+
docIds: [ pieceDocId ],
|
|
855
|
+
crops: [],
|
|
856
|
+
utilized: true
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// --- Page with a widget that has a direct attachment field ---
|
|
860
|
+
const page = await apos.doc.db.findOne({
|
|
861
|
+
slug: '/widget-att',
|
|
862
|
+
aposLocale: 'en:published'
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Simulate an area with an attachment-widget containing an
|
|
866
|
+
// attachment object, as if uploaded through the CMS UI.
|
|
867
|
+
// updateDocReferences stores the parent page's _id in
|
|
868
|
+
// attachment.docIds.
|
|
869
|
+
const pageDocId = page._id;
|
|
870
|
+
await apos.doc.db.updateOne(
|
|
871
|
+
{ _id: pageDocId },
|
|
872
|
+
{
|
|
873
|
+
$set: {
|
|
874
|
+
body: {
|
|
875
|
+
metaType: 'area',
|
|
876
|
+
items: [
|
|
877
|
+
{
|
|
878
|
+
_id: 'widget-1',
|
|
879
|
+
metaType: 'widget',
|
|
880
|
+
type: 'attachment-widget',
|
|
881
|
+
photo: {
|
|
882
|
+
_id: 'att-widget-photo',
|
|
883
|
+
type: 'attachment',
|
|
884
|
+
group: 'images',
|
|
885
|
+
name: 'widget-photo',
|
|
886
|
+
extension: 'jpg'
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
await apos.attachment.db.insertOne({
|
|
896
|
+
_id: 'att-widget-photo',
|
|
897
|
+
name: 'widget-photo',
|
|
898
|
+
extension: 'jpg',
|
|
899
|
+
group: 'images',
|
|
900
|
+
width: 400,
|
|
901
|
+
height: 300,
|
|
902
|
+
archived: false,
|
|
903
|
+
docIds: [ pageDocId ],
|
|
904
|
+
crops: [],
|
|
905
|
+
utilized: true
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// --- Unrelated attachment (should not appear in used scope) ---
|
|
909
|
+
await apos.attachment.db.insertOne({
|
|
910
|
+
_id: 'att-unrelated',
|
|
911
|
+
name: 'unrelated',
|
|
912
|
+
extension: 'png',
|
|
913
|
+
group: 'images',
|
|
914
|
+
width: 50,
|
|
915
|
+
height: 50,
|
|
916
|
+
archived: false,
|
|
917
|
+
docIds: [ 'some-other-doc:en:published' ],
|
|
918
|
+
crops: [],
|
|
919
|
+
utilized: true
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
after(async function () {
|
|
924
|
+
await t.destroy(apos);
|
|
925
|
+
apos = null;
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('used scope includes attachment from piece with direct attachment field', async function () {
|
|
929
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
930
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
931
|
+
attachments: { scope: 'used' }
|
|
932
|
+
});
|
|
933
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
934
|
+
assert(
|
|
935
|
+
ids.includes('att-direct-piece'),
|
|
936
|
+
'Should include attachment owned by a piece with a direct attachment field'
|
|
937
|
+
);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('used scope includes attachment from widget with direct attachment field', async function () {
|
|
941
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
942
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
943
|
+
attachments: { scope: 'used' }
|
|
944
|
+
});
|
|
945
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
946
|
+
assert(
|
|
947
|
+
ids.includes('att-widget-photo'),
|
|
948
|
+
'Should include attachment from a widget with a direct attachment field inside a page area'
|
|
949
|
+
);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('used scope excludes unrelated attachments', async function () {
|
|
953
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
954
|
+
const result = await apos.url.getAllUrlMetadata(req, {
|
|
955
|
+
attachments: { scope: 'used' }
|
|
956
|
+
});
|
|
957
|
+
const ids = result.attachments.results.map(a => a._id);
|
|
958
|
+
assert(
|
|
959
|
+
!ids.includes('att-unrelated'),
|
|
960
|
+
'Should not include attachments not referenced by any content doc'
|
|
961
|
+
);
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
describe('REST API endpoint', function () {
|
|
966
|
+
let apos;
|
|
967
|
+
const externalFrontKey = 'test-static-build-key';
|
|
968
|
+
|
|
969
|
+
before(async function () {
|
|
970
|
+
apos = await t.create({
|
|
971
|
+
root: module,
|
|
972
|
+
modules: {
|
|
973
|
+
'@apostrophecms/url': {
|
|
974
|
+
options: { static: true }
|
|
975
|
+
},
|
|
976
|
+
'@apostrophecms/express': {
|
|
977
|
+
options: {
|
|
978
|
+
externalFrontKey
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
article: {
|
|
982
|
+
extend: '@apostrophecms/piece-type',
|
|
983
|
+
options: {
|
|
984
|
+
name: 'article',
|
|
985
|
+
label: 'Article',
|
|
986
|
+
alias: 'article'
|
|
987
|
+
},
|
|
988
|
+
fields: {
|
|
989
|
+
add: {
|
|
990
|
+
_image: {
|
|
991
|
+
type: 'relationship',
|
|
992
|
+
withType: '@apostrophecms/image',
|
|
993
|
+
label: 'Image',
|
|
994
|
+
max: 1
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
'article-page': {
|
|
1000
|
+
extend: '@apostrophecms/piece-page-type',
|
|
1001
|
+
options: {
|
|
1002
|
+
name: 'articlePage',
|
|
1003
|
+
label: 'Articles',
|
|
1004
|
+
alias: 'articlePage',
|
|
1005
|
+
perPage: 10
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
'@apostrophecms/page': {
|
|
1009
|
+
options: {
|
|
1010
|
+
park: [
|
|
1011
|
+
{
|
|
1012
|
+
title: 'Articles',
|
|
1013
|
+
type: 'articlePage',
|
|
1014
|
+
slug: '/articles',
|
|
1015
|
+
parkedId: 'articles'
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const req = apos.task.getReq();
|
|
1024
|
+
const article = await apos.article.insert(req, {
|
|
1025
|
+
title: 'Test Article',
|
|
1026
|
+
visibility: 'public'
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Set up idsStorage so the article references an image doc
|
|
1030
|
+
await apos.doc.db.updateMany(
|
|
1031
|
+
{ aposDocId: article.aposDocId },
|
|
1032
|
+
{ $set: { imageIds: [ 'api-img-1' ] } }
|
|
1033
|
+
);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
after(async function () {
|
|
1037
|
+
await t.destroy(apos);
|
|
1038
|
+
apos = null;
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it('should return 403 without external front headers', async function () {
|
|
1042
|
+
await assert.rejects(
|
|
1043
|
+
apos.http.get('/api/v1/@apostrophecms/url', {}),
|
|
1044
|
+
{ status: 403 }
|
|
1045
|
+
);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('should return 403 with wrong external front key', async function () {
|
|
1049
|
+
await assert.rejects(
|
|
1050
|
+
apos.http.get('/api/v1/@apostrophecms/url', {
|
|
1051
|
+
headers: {
|
|
1052
|
+
'x-requested-with': 'AposExternalFront',
|
|
1053
|
+
'apos-external-front-key': 'wrong-key'
|
|
1054
|
+
}
|
|
1055
|
+
}),
|
|
1056
|
+
{ status: 403 }
|
|
1057
|
+
);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('should return URL metadata with valid external front key', async function () {
|
|
1061
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
1062
|
+
headers: {
|
|
1063
|
+
'x-requested-with': 'AposExternalFront',
|
|
1064
|
+
'apos-external-front-key': externalFrontKey
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
assert(response);
|
|
1068
|
+
assert(Array.isArray(response.pages));
|
|
1069
|
+
assert(response.pages.length > 0);
|
|
1070
|
+
// Should include at least the home page and articles page
|
|
1071
|
+
assert(
|
|
1072
|
+
response.pages.some(r => r.url === '/'),
|
|
1073
|
+
'Should include home page'
|
|
1074
|
+
);
|
|
1075
|
+
assert(
|
|
1076
|
+
response.pages.some(r => r.url === '/articles'),
|
|
1077
|
+
'Should include articles page'
|
|
1078
|
+
);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('each result should have url and i18nId', async function () {
|
|
1082
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
1083
|
+
headers: {
|
|
1084
|
+
'x-requested-with': 'AposExternalFront',
|
|
1085
|
+
'apos-external-front-key': externalFrontKey
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
for (const entry of response.pages) {
|
|
1089
|
+
assert(entry.url, `Entry should have url: ${JSON.stringify(entry)}`);
|
|
1090
|
+
assert(entry.i18nId, `Entry should have i18nId: ${JSON.stringify(entry)}`);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
it('should return attachments as null without query param', async function () {
|
|
1095
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
1096
|
+
headers: {
|
|
1097
|
+
'x-requested-with': 'AposExternalFront',
|
|
1098
|
+
'apos-external-front-key': externalFrontKey
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
assert.strictEqual(response.attachments, null);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('should return attachment metadata with attachments=1', async function () {
|
|
1105
|
+
// Seed an attachment referencing an image doc ID that
|
|
1106
|
+
// the article doc points to via imageIds idsStorage
|
|
1107
|
+
await apos.attachment.db.insertOne({
|
|
1108
|
+
_id: 'att-api-jpg',
|
|
1109
|
+
name: 'api-photo',
|
|
1110
|
+
extension: 'jpg',
|
|
1111
|
+
group: 'images',
|
|
1112
|
+
width: 400,
|
|
1113
|
+
height: 300,
|
|
1114
|
+
archived: false,
|
|
1115
|
+
docIds: [ 'api-img-1:en:published' ],
|
|
1116
|
+
crops: [],
|
|
1117
|
+
used: true,
|
|
1118
|
+
utilized: true
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const response = await apos.http.get(
|
|
1122
|
+
'/api/v1/@apostrophecms/url?attachments=1',
|
|
1123
|
+
{
|
|
1124
|
+
headers: {
|
|
1125
|
+
'x-requested-with': 'AposExternalFront',
|
|
1126
|
+
'apos-external-front-key': externalFrontKey
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
);
|
|
1130
|
+
assert(response.attachments);
|
|
1131
|
+
assert(typeof response.attachments.uploadsUrl === 'string');
|
|
1132
|
+
assert(Array.isArray(response.attachments.results));
|
|
1133
|
+
const att = response.attachments.results.find(a => a._id === 'att-api-jpg');
|
|
1134
|
+
assert(att, 'Should include the seeded attachment');
|
|
1135
|
+
assert(att.urls.length > 1, 'Sized image should have multiple URL entries');
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('should accept attachmentSkipSizes as comma-separated list', async function () {
|
|
1139
|
+
const response = await apos.http.get(
|
|
1140
|
+
'/api/v1/@apostrophecms/url?attachments=1&attachmentSkipSizes=original,max',
|
|
1141
|
+
{
|
|
1142
|
+
headers: {
|
|
1143
|
+
'x-requested-with': 'AposExternalFront',
|
|
1144
|
+
'apos-external-front-key': externalFrontKey
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
const att = response.attachments.results.find(a => a._id === 'att-api-jpg');
|
|
1149
|
+
const sizeNames = att.urls.map(u => u.size);
|
|
1150
|
+
assert(!sizeNames.includes('original'), 'original should be skipped');
|
|
1151
|
+
assert(!sizeNames.includes('max'), 'max should be skipped');
|
|
1152
|
+
assert(sizeNames.includes('full'), 'full should remain');
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('should accept attachmentSizes as comma-separated list', async function () {
|
|
1156
|
+
const response = await apos.http.get(
|
|
1157
|
+
'/api/v1/@apostrophecms/url?attachments=1&attachmentSizes=full,one-half',
|
|
1158
|
+
{
|
|
1159
|
+
headers: {
|
|
1160
|
+
'x-requested-with': 'AposExternalFront',
|
|
1161
|
+
'apos-external-front-key': externalFrontKey
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
const att = response.attachments.results.find(a => a._id === 'att-api-jpg');
|
|
1166
|
+
const sizeNames = att.urls.map(u => u.size);
|
|
1167
|
+
assert(sizeNames.includes('full'));
|
|
1168
|
+
assert(sizeNames.includes('one-half'));
|
|
1169
|
+
assert(!sizeNames.includes('original'));
|
|
1170
|
+
assert(!sizeNames.includes('max'));
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
it('should accept attachmentScope=all', async function () {
|
|
1174
|
+
// Insert an attachment not referenced by any content doc
|
|
1175
|
+
await apos.attachment.db.insertOne({
|
|
1176
|
+
_id: 'att-api-orphan',
|
|
1177
|
+
name: 'api-orphan',
|
|
1178
|
+
extension: 'png',
|
|
1179
|
+
group: 'images',
|
|
1180
|
+
width: 50,
|
|
1181
|
+
height: 50,
|
|
1182
|
+
archived: false,
|
|
1183
|
+
docIds: [ 'unreferenced-img:en:published' ],
|
|
1184
|
+
crops: [],
|
|
1185
|
+
used: false,
|
|
1186
|
+
utilized: false
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
const response = await apos.http.get(
|
|
1190
|
+
'/api/v1/@apostrophecms/url?attachments=1&attachmentScope=all',
|
|
1191
|
+
{
|
|
1192
|
+
headers: {
|
|
1193
|
+
'x-requested-with': 'AposExternalFront',
|
|
1194
|
+
'apos-external-front-key': externalFrontKey
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
const ids = response.attachments.results.map(a => a._id);
|
|
1199
|
+
assert(ids.includes('att-api-orphan'), 'all scope should include attachments not in URL results');
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('should default scope to used and exclude orphaned attachments', async function () {
|
|
1203
|
+
const response = await apos.http.get(
|
|
1204
|
+
'/api/v1/@apostrophecms/url?attachments=1',
|
|
1205
|
+
{
|
|
1206
|
+
headers: {
|
|
1207
|
+
'x-requested-with': 'AposExternalFront',
|
|
1208
|
+
'apos-external-front-key': externalFrontKey
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
const ids = response.attachments.results.map(a => a._id);
|
|
1213
|
+
assert(!ids.includes('att-api-orphan'), 'used scope should not include attachments not in URL results');
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
it('should ignore invalid attachmentScope values', async function () {
|
|
1217
|
+
const response = await apos.http.get(
|
|
1218
|
+
'/api/v1/@apostrophecms/url?attachments=1&attachmentScope=evil',
|
|
1219
|
+
{
|
|
1220
|
+
headers: {
|
|
1221
|
+
'x-requested-with': 'AposExternalFront',
|
|
1222
|
+
'apos-external-front-key': externalFrontKey
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
// Invalid scope falls back to 'used' via launder.select
|
|
1227
|
+
const ids = response.attachments.results.map(a => a._id);
|
|
1228
|
+
assert(!ids.includes('att-api-orphan'), 'invalid scope should fall back to used');
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should ignore non-boolean attachments values', async function () {
|
|
1232
|
+
const response = await apos.http.get(
|
|
1233
|
+
'/api/v1/@apostrophecms/url?attachments=evil',
|
|
1234
|
+
{
|
|
1235
|
+
headers: {
|
|
1236
|
+
'x-requested-with': 'AposExternalFront',
|
|
1237
|
+
'apos-external-front-key': externalFrontKey
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
);
|
|
1241
|
+
assert.strictEqual(response.attachments, null, 'Non-boolean value should result in null attachments');
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
describe('REST API endpoint without static option', function () {
|
|
1246
|
+
let apos;
|
|
1247
|
+
const externalFrontKey = 'test-no-static-key';
|
|
1248
|
+
|
|
1249
|
+
before(async function () {
|
|
1250
|
+
apos = await t.create({
|
|
1251
|
+
root: module,
|
|
1252
|
+
modules: {
|
|
1253
|
+
'@apostrophecms/express': {
|
|
1254
|
+
options: {
|
|
1255
|
+
externalFrontKey
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
after(async function () {
|
|
1263
|
+
await t.destroy(apos);
|
|
1264
|
+
apos = null;
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it('should return 400 when static option is not enabled', async function () {
|
|
1268
|
+
await assert.rejects(
|
|
1269
|
+
() => apos.http.get('/api/v1/@apostrophecms/url', {
|
|
1270
|
+
headers: {
|
|
1271
|
+
'x-requested-with': 'AposExternalFront',
|
|
1272
|
+
'apos-external-front-key': externalFrontKey
|
|
1273
|
+
}
|
|
1274
|
+
}),
|
|
1275
|
+
(err) => {
|
|
1276
|
+
assert.strictEqual(err.status, 400);
|
|
1277
|
+
assert(
|
|
1278
|
+
err.body?.message?.includes('static: true'),
|
|
1279
|
+
'Error message should mention the static option'
|
|
1280
|
+
);
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
describe('Piece page dispatch routes in static mode', function () {
|
|
1288
|
+
let apos;
|
|
1289
|
+
|
|
1290
|
+
before(async function () {
|
|
1291
|
+
apos = await t.create({
|
|
1292
|
+
root: module,
|
|
1293
|
+
modules: {
|
|
1294
|
+
'@apostrophecms/url': {
|
|
1295
|
+
options: { static: true }
|
|
1296
|
+
},
|
|
1297
|
+
article: {
|
|
1298
|
+
extend: '@apostrophecms/piece-type',
|
|
1299
|
+
options: {
|
|
1300
|
+
name: 'article',
|
|
1301
|
+
label: 'Article',
|
|
1302
|
+
alias: 'article',
|
|
1303
|
+
sort: { title: 1 }
|
|
1304
|
+
},
|
|
1305
|
+
fields: {
|
|
1306
|
+
add: {
|
|
1307
|
+
category: {
|
|
1308
|
+
type: 'select',
|
|
1309
|
+
label: 'Category',
|
|
1310
|
+
choices: [
|
|
1311
|
+
{
|
|
1312
|
+
label: 'Tech',
|
|
1313
|
+
value: 'tech'
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
label: 'Science',
|
|
1317
|
+
value: 'science'
|
|
1318
|
+
}
|
|
1319
|
+
]
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
'article-page': {
|
|
1325
|
+
extend: '@apostrophecms/piece-page-type',
|
|
1326
|
+
options: {
|
|
1327
|
+
name: 'articlePage',
|
|
1328
|
+
label: 'Articles',
|
|
1329
|
+
alias: 'articlePage',
|
|
1330
|
+
perPage: 5,
|
|
1331
|
+
piecesFilters: [
|
|
1332
|
+
{ name: 'category' }
|
|
1333
|
+
]
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
'@apostrophecms/page': {
|
|
1337
|
+
options: {
|
|
1338
|
+
park: [
|
|
1339
|
+
{
|
|
1340
|
+
title: 'Articles',
|
|
1341
|
+
type: 'articlePage',
|
|
1342
|
+
slug: '/articles',
|
|
1343
|
+
parkedId: 'articles'
|
|
1344
|
+
}
|
|
1345
|
+
]
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const req = apos.task.getReq();
|
|
1352
|
+
for (let i = 1; i <= 12; i++) {
|
|
1353
|
+
const padded = String(i).padStart(3, '0');
|
|
1354
|
+
const category = i <= 6 ? 'tech' : 'science';
|
|
1355
|
+
await apos.article.insert(req, {
|
|
1356
|
+
title: `Article ${padded}`,
|
|
1357
|
+
slug: `article-${padded}`,
|
|
1358
|
+
visibility: 'public',
|
|
1359
|
+
category
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
after(async function () {
|
|
1365
|
+
await t.destroy(apos);
|
|
1366
|
+
apos = null;
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('should serve index page at /', async function () {
|
|
1370
|
+
const body = await apos.http.get('/articles');
|
|
1371
|
+
assert(body.includes('article-001'));
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it('should serve paginated page via path in static mode', async function () {
|
|
1375
|
+
const body = await apos.http.get('/articles/page/2');
|
|
1376
|
+
// Page 2 with perPage=5 should show articles 6-10
|
|
1377
|
+
assert(body.includes('article-006'));
|
|
1378
|
+
assert(!body.includes('article-001'));
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it('should serve filter page via path in static mode', async function () {
|
|
1382
|
+
const body = await apos.http.get('/articles/category/tech');
|
|
1383
|
+
// Should only show tech articles (1-6)
|
|
1384
|
+
assert(body.includes('article-001'));
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it('should serve filter + pagination via path in static mode', async function () {
|
|
1388
|
+
const body = await apos.http.get('/articles/category/tech/page/2');
|
|
1389
|
+
// 6 tech articles with perPage=5 means page 2 has 1 article
|
|
1390
|
+
assert(body.includes('article-006'));
|
|
1391
|
+
assert(!body.includes('article-001'));
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('should still serve individual piece show pages', async function () {
|
|
1395
|
+
const body = await apos.http.get('/articles/article-001');
|
|
1396
|
+
assert(body.includes('Article 001'));
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
describe('getAllUrlMetadata event', function () {
|
|
1401
|
+
let apos;
|
|
1402
|
+
let eventFired = false;
|
|
1403
|
+
|
|
1404
|
+
before(async function () {
|
|
1405
|
+
apos = await t.create({
|
|
1406
|
+
root: module,
|
|
1407
|
+
modules: {
|
|
1408
|
+
'@apostrophecms/url': {
|
|
1409
|
+
options: { static: true }
|
|
1410
|
+
},
|
|
1411
|
+
'custom-urls': {
|
|
1412
|
+
handlers(self) {
|
|
1413
|
+
return {
|
|
1414
|
+
'@apostrophecms/url:getAllUrlMetadata': {
|
|
1415
|
+
addCustomUrl(req, results, { excludeTypes }) {
|
|
1416
|
+
eventFired = true;
|
|
1417
|
+
results.push({
|
|
1418
|
+
url: '/custom-resource.txt',
|
|
1419
|
+
contentType: 'text/plain',
|
|
1420
|
+
i18nId: 'custom:resource',
|
|
1421
|
+
sitemap: false
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
after(async function () {
|
|
1433
|
+
await t.destroy(apos);
|
|
1434
|
+
apos = null;
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it('should emit getAllUrlMetadata event and include custom URLs', async function () {
|
|
1438
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1439
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
1440
|
+
|
|
1441
|
+
assert(eventFired, 'Event should have been fired');
|
|
1442
|
+
|
|
1443
|
+
const custom = results.find(r => r.i18nId === 'custom:resource');
|
|
1444
|
+
assert(custom, 'Should include custom URL from event handler');
|
|
1445
|
+
assert.strictEqual(custom.url, '/custom-resource.txt');
|
|
1446
|
+
assert.strictEqual(custom.contentType, 'text/plain');
|
|
1447
|
+
assert.strictEqual(custom.sitemap, false);
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
describe('getFiltersWithChoices', function () {
|
|
1452
|
+
let apos;
|
|
1453
|
+
|
|
1454
|
+
before(async function () {
|
|
1455
|
+
apos = await t.create({
|
|
1456
|
+
root: module,
|
|
1457
|
+
modules: {
|
|
1458
|
+
'@apostrophecms/url': {
|
|
1459
|
+
options: { static: true }
|
|
1460
|
+
},
|
|
1461
|
+
article: {
|
|
1462
|
+
extend: '@apostrophecms/piece-type',
|
|
1463
|
+
options: {
|
|
1464
|
+
name: 'article',
|
|
1465
|
+
label: 'Article',
|
|
1466
|
+
alias: 'article',
|
|
1467
|
+
sort: { title: 1 }
|
|
1468
|
+
},
|
|
1469
|
+
fields: {
|
|
1470
|
+
add: {
|
|
1471
|
+
category: {
|
|
1472
|
+
type: 'select',
|
|
1473
|
+
label: 'Category',
|
|
1474
|
+
choices: [
|
|
1475
|
+
{
|
|
1476
|
+
label: 'Tech',
|
|
1477
|
+
value: 'tech'
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
label: 'Science',
|
|
1481
|
+
value: 'science'
|
|
1482
|
+
}
|
|
1483
|
+
]
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
'article-page': {
|
|
1489
|
+
extend: '@apostrophecms/piece-page-type',
|
|
1490
|
+
options: {
|
|
1491
|
+
name: 'articlePage',
|
|
1492
|
+
label: 'Articles',
|
|
1493
|
+
alias: 'articlePage',
|
|
1494
|
+
perPage: 10,
|
|
1495
|
+
piecesFilters: [
|
|
1496
|
+
{ name: 'category' }
|
|
1497
|
+
]
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
'@apostrophecms/page': {
|
|
1501
|
+
options: {
|
|
1502
|
+
park: [
|
|
1503
|
+
{
|
|
1504
|
+
title: 'Articles',
|
|
1505
|
+
type: 'articlePage',
|
|
1506
|
+
slug: '/articles',
|
|
1507
|
+
parkedId: 'articles'
|
|
1508
|
+
}
|
|
1509
|
+
]
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
const req = apos.task.getReq();
|
|
1516
|
+
for (let i = 1; i <= 6; i++) {
|
|
1517
|
+
const category = i <= 3 ? 'tech' : 'science';
|
|
1518
|
+
await apos.article.insert(req, {
|
|
1519
|
+
title: `Article ${i}`,
|
|
1520
|
+
visibility: 'public',
|
|
1521
|
+
category
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
after(async function () {
|
|
1527
|
+
await t.destroy(apos);
|
|
1528
|
+
apos = null;
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
it('should return filter choices with counts when requested', async function () {
|
|
1532
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1533
|
+
const query = apos.articlePage.indexQuery(req);
|
|
1534
|
+
const filters = await apos.articlePage.getFiltersWithChoices(query, {
|
|
1535
|
+
allCounts: true
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
assert(Array.isArray(filters));
|
|
1539
|
+
assert.strictEqual(filters.length, 1);
|
|
1540
|
+
assert.strictEqual(filters[0].name, 'category');
|
|
1541
|
+
assert(Array.isArray(filters[0].choices));
|
|
1542
|
+
|
|
1543
|
+
// Should have choices for tech and science
|
|
1544
|
+
const techChoice = filters[0].choices.find(c => c.value === 'tech');
|
|
1545
|
+
const scienceChoice = filters[0].choices.find(c => c.value === 'science');
|
|
1546
|
+
assert(techChoice, 'Should have tech choice');
|
|
1547
|
+
assert(scienceChoice, 'Should have science choice');
|
|
1548
|
+
assert.strictEqual(techChoice.count, 3);
|
|
1549
|
+
assert.strictEqual(scienceChoice.count, 3);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
it('choices should have _url with path format when page context exists', async function () {
|
|
1553
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1554
|
+
|
|
1555
|
+
// Simulate a real page context by fetching the articles page
|
|
1556
|
+
const articlesPage = await apos.page.find(req, { slug: '/articles' }).toObject();
|
|
1557
|
+
req.data.page = articlesPage;
|
|
1558
|
+
const query = apos.articlePage.indexQuery(req);
|
|
1559
|
+
const filters = await apos.articlePage.getFiltersWithChoices(query, {
|
|
1560
|
+
allCounts: true
|
|
1561
|
+
});
|
|
1562
|
+
const techChoice = filters[0].choices.find(c => c.value === 'tech');
|
|
1563
|
+
const scienceChoice = filters[0].choices.find(c => c.value === 'science');
|
|
1564
|
+
|
|
1565
|
+
// In static mode, _url should use path segments, not query strings
|
|
1566
|
+
assert.strictEqual(
|
|
1567
|
+
techChoice._url,
|
|
1568
|
+
'/articles/category/tech',
|
|
1569
|
+
'Tech choice _url should use path format'
|
|
1570
|
+
);
|
|
1571
|
+
assert.strictEqual(
|
|
1572
|
+
scienceChoice._url,
|
|
1573
|
+
'/articles/category/science',
|
|
1574
|
+
'Science choice _url should use path format'
|
|
1575
|
+
);
|
|
1576
|
+
assert(
|
|
1577
|
+
!techChoice._url.includes('?'),
|
|
1578
|
+
'Static mode _url should not contain query string'
|
|
1579
|
+
);
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
describe('getFiltersWithChoices for relationship fields', function () {
|
|
1584
|
+
let apos;
|
|
1585
|
+
|
|
1586
|
+
before(async function () {
|
|
1587
|
+
apos = await t.create({
|
|
1588
|
+
root: module,
|
|
1589
|
+
modules: {
|
|
1590
|
+
'@apostrophecms/url': {
|
|
1591
|
+
options: { static: true }
|
|
1592
|
+
},
|
|
1593
|
+
category: {
|
|
1594
|
+
extend: '@apostrophecms/piece-type',
|
|
1595
|
+
options: {
|
|
1596
|
+
name: 'category',
|
|
1597
|
+
label: 'Category',
|
|
1598
|
+
alias: 'category'
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1601
|
+
article: {
|
|
1602
|
+
extend: '@apostrophecms/piece-type',
|
|
1603
|
+
options: {
|
|
1604
|
+
name: 'article',
|
|
1605
|
+
label: 'Article',
|
|
1606
|
+
alias: 'article',
|
|
1607
|
+
sort: { title: 1 }
|
|
1608
|
+
},
|
|
1609
|
+
fields: {
|
|
1610
|
+
add: {
|
|
1611
|
+
_categories: {
|
|
1612
|
+
type: 'relationship',
|
|
1613
|
+
withType: 'category',
|
|
1614
|
+
label: 'Categories'
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
'article-page': {
|
|
1620
|
+
extend: '@apostrophecms/piece-page-type',
|
|
1621
|
+
options: {
|
|
1622
|
+
name: 'articlePage',
|
|
1623
|
+
label: 'Articles',
|
|
1624
|
+
alias: 'articlePage',
|
|
1625
|
+
perPage: 10,
|
|
1626
|
+
piecesFilters: [
|
|
1627
|
+
{ name: 'categories' }
|
|
1628
|
+
]
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
'@apostrophecms/page': {
|
|
1632
|
+
options: {
|
|
1633
|
+
park: [
|
|
1634
|
+
{
|
|
1635
|
+
title: 'Articles',
|
|
1636
|
+
type: 'articlePage',
|
|
1637
|
+
slug: '/articles',
|
|
1638
|
+
parkedId: 'articles'
|
|
1639
|
+
}
|
|
1640
|
+
]
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
const req = apos.task.getReq();
|
|
1647
|
+
const tech = await apos.category.insert(req, {
|
|
1648
|
+
title: 'Tech',
|
|
1649
|
+
visibility: 'public'
|
|
1650
|
+
});
|
|
1651
|
+
const science = await apos.category.insert(req, {
|
|
1652
|
+
title: 'Science',
|
|
1653
|
+
visibility: 'public'
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
for (let i = 1; i <= 6; i++) {
|
|
1657
|
+
const cats = i <= 3 ? [ tech ] : [ science ];
|
|
1658
|
+
await apos.article.insert(req, {
|
|
1659
|
+
title: `Article ${i}`,
|
|
1660
|
+
visibility: 'public',
|
|
1661
|
+
_categories: cats
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
after(async function () {
|
|
1667
|
+
await t.destroy(apos);
|
|
1668
|
+
apos = null;
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
it('should return relationship filter choices with counts', async function () {
|
|
1672
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1673
|
+
const query = apos.articlePage.indexQuery(req);
|
|
1674
|
+
const filters = await apos.articlePage.getFiltersWithChoices(query, {
|
|
1675
|
+
allCounts: true
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
assert(Array.isArray(filters));
|
|
1679
|
+
assert.strictEqual(filters.length, 1);
|
|
1680
|
+
assert.strictEqual(filters[0].name, 'categories');
|
|
1681
|
+
|
|
1682
|
+
const techChoice = filters[0].choices.find(c => c.value === 'tech');
|
|
1683
|
+
const scienceChoice = filters[0].choices.find(c => c.value === 'science');
|
|
1684
|
+
assert(techChoice, 'Should have tech choice');
|
|
1685
|
+
assert(scienceChoice, 'Should have science choice');
|
|
1686
|
+
assert.strictEqual(techChoice.count, 3, 'Tech should have count 3');
|
|
1687
|
+
assert.strictEqual(scienceChoice.count, 3, 'Science should have count 3');
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
it('should enumerate relationship filter URLs in getUrlMetadata', async function () {
|
|
1691
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1692
|
+
const { pages: results } = await apos.url.getAllUrlMetadata(req);
|
|
1693
|
+
|
|
1694
|
+
const filterUrls = results.filter(r =>
|
|
1695
|
+
r.url && r.url.startsWith('/articles/categories/')
|
|
1696
|
+
);
|
|
1697
|
+
assert(filterUrls.length >= 2, `Should have at least 2 filter URLs, got ${filterUrls.length}`);
|
|
1698
|
+
assert(
|
|
1699
|
+
filterUrls.some(r => r.url === '/articles/categories/tech'),
|
|
1700
|
+
'Should enumerate /articles/categories/tech'
|
|
1701
|
+
);
|
|
1702
|
+
assert(
|
|
1703
|
+
filterUrls.some(r => r.url === '/articles/categories/science'),
|
|
1704
|
+
'Should enumerate /articles/categories/science'
|
|
1705
|
+
);
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
describe('URL support', function () {
|
|
1710
|
+
|
|
1711
|
+
describe('baseUrl configured, no staticBaseUrl', function () {
|
|
1712
|
+
let apos;
|
|
1713
|
+
|
|
1714
|
+
before(async function () {
|
|
1715
|
+
apos = await t.create({
|
|
1716
|
+
root: module,
|
|
1717
|
+
baseUrl: 'http://localhost:3000',
|
|
1718
|
+
modules: {
|
|
1719
|
+
'@apostrophecms/url': {
|
|
1720
|
+
options: { static: true }
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
after(async function () {
|
|
1727
|
+
await t.destroy(apos);
|
|
1728
|
+
apos = null;
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
it('apos.baseUrl is set correctly', function () {
|
|
1732
|
+
assert.strictEqual(apos.baseUrl, 'http://localhost:3000');
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
it('apos.staticBaseUrl is undefined when not configured', function () {
|
|
1736
|
+
assert.strictEqual(apos.staticBaseUrl, undefined);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
it('getBaseUrl returns empty string when req.aposStaticBuild is true and no staticBaseUrl', function () {
|
|
1740
|
+
const req = apos.task.getAnonReq({
|
|
1741
|
+
mode: 'published',
|
|
1742
|
+
staticBuild: true
|
|
1743
|
+
});
|
|
1744
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1745
|
+
assert.strictEqual(baseUrl, '');
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
it('getBaseUrl returns apos.baseUrl when req.aposStaticBuild is not set', function () {
|
|
1749
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1750
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1751
|
+
assert.strictEqual(baseUrl, 'http://localhost:3000');
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
it('getAllUrlMetadata strips baseUrl from page URLs for static build requests', async function () {
|
|
1755
|
+
const req = apos.task.getAnonReq({
|
|
1756
|
+
mode: 'published',
|
|
1757
|
+
staticBuild: true
|
|
1758
|
+
});
|
|
1759
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
1760
|
+
assert(pages.length > 0);
|
|
1761
|
+
for (const entry of pages) {
|
|
1762
|
+
assert(
|
|
1763
|
+
!entry.url.startsWith('http://'),
|
|
1764
|
+
`URL should be path-only, got: ${entry.url}`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
const home = pages.find(r => r.url === '/');
|
|
1768
|
+
assert(home, 'Should include the home page with path-only URL');
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('getAllUrlMetadata strips origin from uploadsUrl, keeping it relative', async function () {
|
|
1772
|
+
const req = apos.task.getAnonReq({
|
|
1773
|
+
mode: 'published',
|
|
1774
|
+
staticBuild: true
|
|
1775
|
+
});
|
|
1776
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
1777
|
+
attachments: { scope: 'all' }
|
|
1778
|
+
});
|
|
1779
|
+
assert(attachments);
|
|
1780
|
+
// Without staticBaseUrl, uploadfs is initialized with the
|
|
1781
|
+
// full baseUrl. getAllUrlMetadata strips the origin so the
|
|
1782
|
+
// consumer receives a relative path.
|
|
1783
|
+
assert.strictEqual(
|
|
1784
|
+
apos.attachment.uploadfs.getUrl(),
|
|
1785
|
+
'http://localhost:3000/uploads'
|
|
1786
|
+
);
|
|
1787
|
+
assert.strictEqual(
|
|
1788
|
+
attachments.uploadsUrl,
|
|
1789
|
+
'/uploads'
|
|
1790
|
+
);
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
it('apos.baseUrl is NOT modified after static build requests', async function () {
|
|
1794
|
+
const req = apos.task.getAnonReq({
|
|
1795
|
+
mode: 'published',
|
|
1796
|
+
staticBuild: true
|
|
1797
|
+
});
|
|
1798
|
+
await apos.url.getAllUrlMetadata(req);
|
|
1799
|
+
assert.strictEqual(apos.baseUrl, 'http://localhost:3000');
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
describe('baseUrl + staticBaseUrl configured', function () {
|
|
1804
|
+
let apos;
|
|
1805
|
+
|
|
1806
|
+
before(async function () {
|
|
1807
|
+
apos = await t.create({
|
|
1808
|
+
root: module,
|
|
1809
|
+
baseUrl: 'http://localhost:3000',
|
|
1810
|
+
staticBaseUrl: 'https://www.example.com',
|
|
1811
|
+
modules: {
|
|
1812
|
+
'@apostrophecms/url': {
|
|
1813
|
+
options: { static: true }
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
after(async function () {
|
|
1820
|
+
await t.destroy(apos);
|
|
1821
|
+
apos = null;
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it('apos.staticBaseUrl is set correctly', function () {
|
|
1825
|
+
assert.strictEqual(apos.staticBaseUrl, 'https://www.example.com');
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
it('getBaseUrl returns staticBaseUrl when req.aposStaticBuild is true', function () {
|
|
1829
|
+
const req = apos.task.getAnonReq({
|
|
1830
|
+
mode: 'published',
|
|
1831
|
+
staticBuild: true
|
|
1832
|
+
});
|
|
1833
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1834
|
+
assert.strictEqual(baseUrl, 'https://www.example.com');
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
it('getBaseUrl returns apos.baseUrl when req.aposStaticBuild is not set', function () {
|
|
1838
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1839
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1840
|
+
assert.strictEqual(baseUrl, 'http://localhost:3000');
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('getAllUrlMetadata strips staticBaseUrl from page URLs for static build requests', async function () {
|
|
1844
|
+
const req = apos.task.getAnonReq({
|
|
1845
|
+
mode: 'published',
|
|
1846
|
+
staticBuild: true
|
|
1847
|
+
});
|
|
1848
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
1849
|
+
assert(pages.length > 0);
|
|
1850
|
+
for (const entry of pages) {
|
|
1851
|
+
assert(
|
|
1852
|
+
!entry.url.startsWith('https://'),
|
|
1853
|
+
`URL should be path-only, got: ${entry.url}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
const home = pages.find(r => r.url === '/');
|
|
1857
|
+
assert(home, 'Should include the home page with path-only URL');
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
it('getAllUrlMetadata strips original baseUrl from uploadsUrl', async function () {
|
|
1861
|
+
const req = apos.task.getAnonReq({
|
|
1862
|
+
mode: 'published',
|
|
1863
|
+
staticBuild: true
|
|
1864
|
+
});
|
|
1865
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
1866
|
+
attachments: { scope: 'all' }
|
|
1867
|
+
});
|
|
1868
|
+
assert(attachments);
|
|
1869
|
+
assert.strictEqual(
|
|
1870
|
+
attachments.uploadsUrl,
|
|
1871
|
+
'/uploads',
|
|
1872
|
+
'uploadsUrl should strip the original baseUrl, not staticBaseUrl'
|
|
1873
|
+
);
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it('req.absoluteUrl uses staticBaseUrl during static builds', function () {
|
|
1877
|
+
const req = apos.task.getAnonReq({
|
|
1878
|
+
mode: 'published',
|
|
1879
|
+
staticBuild: true,
|
|
1880
|
+
url: '/some-page'
|
|
1881
|
+
});
|
|
1882
|
+
assert(
|
|
1883
|
+
req.absoluteUrl.startsWith('https://www.example.com'),
|
|
1884
|
+
`req.absoluteUrl should use staticBaseUrl, got: ${req.absoluteUrl}`
|
|
1885
|
+
);
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
it('apos.baseUrl is NOT modified after static build requests', async function () {
|
|
1889
|
+
const req = apos.task.getAnonReq({
|
|
1890
|
+
mode: 'published',
|
|
1891
|
+
staticBuild: true
|
|
1892
|
+
});
|
|
1893
|
+
await apos.url.getAllUrlMetadata(req);
|
|
1894
|
+
assert.strictEqual(apos.baseUrl, 'http://localhost:3000');
|
|
1895
|
+
});
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
describe('staticBaseUrl only (no baseUrl)', function () {
|
|
1899
|
+
let apos;
|
|
1900
|
+
|
|
1901
|
+
before(async function () {
|
|
1902
|
+
apos = await t.create({
|
|
1903
|
+
root: module,
|
|
1904
|
+
staticBaseUrl: 'https://www.example.com',
|
|
1905
|
+
modules: {
|
|
1906
|
+
'@apostrophecms/url': {
|
|
1907
|
+
options: { static: true }
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
after(async function () {
|
|
1914
|
+
await t.destroy(apos);
|
|
1915
|
+
apos = null;
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
it('apos.baseUrl is undefined', function () {
|
|
1919
|
+
assert.strictEqual(apos.baseUrl, undefined);
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it('apos.staticBaseUrl is set', function () {
|
|
1923
|
+
assert.strictEqual(apos.staticBaseUrl, 'https://www.example.com');
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
it('getBaseUrl returns staticBaseUrl when req.aposStaticBuild is true', function () {
|
|
1927
|
+
const req = apos.task.getAnonReq({
|
|
1928
|
+
mode: 'published',
|
|
1929
|
+
staticBuild: true
|
|
1930
|
+
});
|
|
1931
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1932
|
+
assert.strictEqual(baseUrl, 'https://www.example.com');
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
it('getBaseUrl returns empty string without static build flag', function () {
|
|
1936
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
1937
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
1938
|
+
assert.strictEqual(baseUrl, '');
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('getAllUrlMetadata strips staticBaseUrl from page URLs', async function () {
|
|
1942
|
+
const req = apos.task.getAnonReq({
|
|
1943
|
+
mode: 'published',
|
|
1944
|
+
staticBuild: true
|
|
1945
|
+
});
|
|
1946
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
1947
|
+
assert(pages.length > 0);
|
|
1948
|
+
for (const entry of pages) {
|
|
1949
|
+
assert(
|
|
1950
|
+
!entry.url.startsWith('https://'),
|
|
1951
|
+
`URL should be path-only, got: ${entry.url}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
it('uploadsUrl is path-only (no baseUrl to strip)', async function () {
|
|
1957
|
+
const req = apos.task.getAnonReq({
|
|
1958
|
+
mode: 'published',
|
|
1959
|
+
staticBuild: true
|
|
1960
|
+
});
|
|
1961
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
1962
|
+
attachments: { scope: 'all' }
|
|
1963
|
+
});
|
|
1964
|
+
assert(attachments);
|
|
1965
|
+
assert.strictEqual(attachments.uploadsUrl, '/uploads');
|
|
1966
|
+
});
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
describe('baseUrl + prefix', function () {
|
|
1970
|
+
let apos;
|
|
1971
|
+
|
|
1972
|
+
before(async function () {
|
|
1973
|
+
apos = await t.create({
|
|
1974
|
+
root: module,
|
|
1975
|
+
baseUrl: 'http://localhost:3000',
|
|
1976
|
+
prefix: '/cms',
|
|
1977
|
+
modules: {
|
|
1978
|
+
'@apostrophecms/url': {
|
|
1979
|
+
options: { static: true }
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
after(async function () {
|
|
1986
|
+
await t.destroy(apos);
|
|
1987
|
+
apos = null;
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
it('getAllUrlMetadata strips baseUrl and prefix from page URLs', async function () {
|
|
1991
|
+
const req = apos.task.getAnonReq({
|
|
1992
|
+
mode: 'published',
|
|
1993
|
+
staticBuild: true
|
|
1994
|
+
});
|
|
1995
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
1996
|
+
assert(pages.length > 0);
|
|
1997
|
+
for (const entry of pages) {
|
|
1998
|
+
assert(
|
|
1999
|
+
!entry.url.startsWith('http://'),
|
|
2000
|
+
`URL should not start with http://, got: ${entry.url}`
|
|
2001
|
+
);
|
|
2002
|
+
assert(
|
|
2003
|
+
!entry.url.startsWith('/cms'),
|
|
2004
|
+
`URL should not start with /cms prefix, got: ${entry.url}`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const home = pages.find(r => r.url === '/');
|
|
2008
|
+
assert(home, 'Should include the home page as /');
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
it('getAllUrlMetadata strips origin from uploadsUrl, preserves prefix', async function () {
|
|
2012
|
+
const req = apos.task.getAnonReq({
|
|
2013
|
+
mode: 'published',
|
|
2014
|
+
staticBuild: true
|
|
2015
|
+
});
|
|
2016
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
2017
|
+
attachments: { scope: 'all' }
|
|
2018
|
+
});
|
|
2019
|
+
assert(attachments);
|
|
2020
|
+
// Without staticBaseUrl, uploadfs is initialized with the
|
|
2021
|
+
// full baseUrl + prefix. getAllUrlMetadata strips the
|
|
2022
|
+
// origin, leaving a relative path that still includes the
|
|
2023
|
+
// prefix — the consumer strips the prefix separately.
|
|
2024
|
+
assert.strictEqual(
|
|
2025
|
+
apos.attachment.uploadfs.getUrl(),
|
|
2026
|
+
'http://localhost:3000/cms/uploads'
|
|
2027
|
+
);
|
|
2028
|
+
assert.strictEqual(
|
|
2029
|
+
attachments.uploadsUrl,
|
|
2030
|
+
'/cms/uploads'
|
|
2031
|
+
);
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
describe('no static header (normal external front)', function () {
|
|
2036
|
+
let apos;
|
|
2037
|
+
|
|
2038
|
+
before(async function () {
|
|
2039
|
+
apos = await t.create({
|
|
2040
|
+
root: module,
|
|
2041
|
+
baseUrl: 'http://localhost:3000',
|
|
2042
|
+
modules: {
|
|
2043
|
+
'@apostrophecms/url': {
|
|
2044
|
+
options: { static: true }
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
after(async function () {
|
|
2051
|
+
await t.destroy(apos);
|
|
2052
|
+
apos = null;
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
it('getAllUrlMetadata returns path-only page URLs even without static build flag', async function () {
|
|
2056
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
2057
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
2058
|
+
assert(pages.length > 0);
|
|
2059
|
+
const home = pages.find(r => r.url === '/');
|
|
2060
|
+
assert(home, 'Should return path-only URLs regardless of static build flag');
|
|
2061
|
+
for (const entry of pages) {
|
|
2062
|
+
assert(
|
|
2063
|
+
!entry.url.startsWith('http://'),
|
|
2064
|
+
`URL should be path-only, got: ${entry.url}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it('getAllUrlMetadata strips origin from uploadsUrl even without static flag', async function () {
|
|
2070
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
2071
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
2072
|
+
attachments: { scope: 'all' }
|
|
2073
|
+
});
|
|
2074
|
+
assert(attachments);
|
|
2075
|
+
// The origin is always stripped from uploadsUrl so the
|
|
2076
|
+
// consumer receives a consistent relative path.
|
|
2077
|
+
assert.strictEqual(
|
|
2078
|
+
apos.attachment.uploadfs.getUrl(),
|
|
2079
|
+
'http://localhost:3000/uploads'
|
|
2080
|
+
);
|
|
2081
|
+
assert.strictEqual(
|
|
2082
|
+
attachments.uploadsUrl,
|
|
2083
|
+
'/uploads'
|
|
2084
|
+
);
|
|
2085
|
+
});
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
describe('APOS_STATIC_BASE_URL environment variable', function () {
|
|
2089
|
+
let apos;
|
|
2090
|
+
let savedEnvVar;
|
|
2091
|
+
|
|
2092
|
+
before(async function () {
|
|
2093
|
+
savedEnvVar = process.env.APOS_STATIC_BASE_URL;
|
|
2094
|
+
process.env.APOS_STATIC_BASE_URL = 'https://env.example.com';
|
|
2095
|
+
apos = await t.create({
|
|
2096
|
+
root: module,
|
|
2097
|
+
baseUrl: 'http://localhost:3000',
|
|
2098
|
+
staticBaseUrl: 'https://option.example.com',
|
|
2099
|
+
modules: {
|
|
2100
|
+
'@apostrophecms/url': {
|
|
2101
|
+
options: { static: true }
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
after(async function () {
|
|
2108
|
+
if (savedEnvVar) {
|
|
2109
|
+
process.env.APOS_STATIC_BASE_URL = savedEnvVar;
|
|
2110
|
+
} else {
|
|
2111
|
+
delete process.env.APOS_STATIC_BASE_URL;
|
|
2112
|
+
}
|
|
2113
|
+
await t.destroy(apos);
|
|
2114
|
+
apos = null;
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
it('env variable overrides the staticBaseUrl option', function () {
|
|
2118
|
+
assert.strictEqual(apos.staticBaseUrl, 'https://env.example.com');
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
it('getBaseUrl uses env-based staticBaseUrl for static builds', function () {
|
|
2122
|
+
const req = apos.task.getAnonReq({
|
|
2123
|
+
mode: 'published',
|
|
2124
|
+
staticBuild: true
|
|
2125
|
+
});
|
|
2126
|
+
const baseUrl = apos.page.getBaseUrl(req);
|
|
2127
|
+
assert.strictEqual(baseUrl, 'https://env.example.com');
|
|
2128
|
+
});
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
describe('express middleware sets req.aposStaticBuild', function () {
|
|
2132
|
+
let apos;
|
|
2133
|
+
|
|
2134
|
+
before(async function () {
|
|
2135
|
+
apos = await t.create({
|
|
2136
|
+
root: module,
|
|
2137
|
+
baseUrl: 'http://localhost:3000',
|
|
2138
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2139
|
+
modules: {
|
|
2140
|
+
'@apostrophecms/express': {
|
|
2141
|
+
options: {
|
|
2142
|
+
externalFrontKey: 'test-key'
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
'@apostrophecms/url': {
|
|
2146
|
+
options: { static: true }
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
after(async function () {
|
|
2153
|
+
await t.destroy(apos);
|
|
2154
|
+
apos = null;
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
it('sets req.aposStaticBuild and req.staticBaseUrl when header is present', async function () {
|
|
2158
|
+
const jar = apos.http.jar();
|
|
2159
|
+
// Use the URL API endpoint which requires externalFront
|
|
2160
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
2161
|
+
jar,
|
|
2162
|
+
headers: {
|
|
2163
|
+
'x-requested-with': 'AposExternalFront',
|
|
2164
|
+
'apos-external-front-key': 'test-key',
|
|
2165
|
+
'x-apos-static-base-url': '1'
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
assert(response);
|
|
2169
|
+
assert(response.pages);
|
|
2170
|
+
// Page URLs should be path-only (stripped)
|
|
2171
|
+
for (const entry of response.pages) {
|
|
2172
|
+
assert(
|
|
2173
|
+
!entry.url.startsWith('http://') && !entry.url.startsWith('https://'),
|
|
2174
|
+
`URL should be path-only via HTTP, got: ${entry.url}`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
it('returns path-only URLs even without the static header', async function () {
|
|
2180
|
+
const jar = apos.http.jar();
|
|
2181
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
2182
|
+
jar,
|
|
2183
|
+
headers: {
|
|
2184
|
+
'x-requested-with': 'AposExternalFront',
|
|
2185
|
+
'apos-external-front-key': 'test-key'
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
assert(response);
|
|
2189
|
+
assert(response.pages);
|
|
2190
|
+
const home = response.pages.find(r => r.url === '/');
|
|
2191
|
+
assert(home, 'URLs should be path-only regardless of static header');
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
describe('CDN uploadsUrl (cloud provider)', function () {
|
|
2196
|
+
let apos;
|
|
2197
|
+
|
|
2198
|
+
before(async function () {
|
|
2199
|
+
apos = await t.create({
|
|
2200
|
+
root: module,
|
|
2201
|
+
baseUrl: 'http://localhost:3000',
|
|
2202
|
+
modules: {
|
|
2203
|
+
'@apostrophecms/url': {
|
|
2204
|
+
options: { static: true }
|
|
2205
|
+
},
|
|
2206
|
+
'@apostrophecms/uploadfs': {
|
|
2207
|
+
options: {
|
|
2208
|
+
uploadfs: {
|
|
2209
|
+
uploadsUrl: 'https://cdn.example.com/uploads'
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
after(async function () {
|
|
2218
|
+
await t.destroy(apos);
|
|
2219
|
+
apos = null;
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
it('does not strip CDN uploadsUrl that differs from baseUrl', async function () {
|
|
2223
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
2224
|
+
const { attachments } = await apos.url.getAllUrlMetadata(req, {
|
|
2225
|
+
attachments: { scope: 'all' }
|
|
2226
|
+
});
|
|
2227
|
+
assert(attachments);
|
|
2228
|
+
assert.strictEqual(
|
|
2229
|
+
attachments.uploadsUrl,
|
|
2230
|
+
'https://cdn.example.com/uploads',
|
|
2231
|
+
'CDN uploadsUrl should be left unchanged'
|
|
2232
|
+
);
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
it('still strips baseUrl from page URLs', async function () {
|
|
2236
|
+
const req = apos.task.getAnonReq({ mode: 'published' });
|
|
2237
|
+
const { pages } = await apos.url.getAllUrlMetadata(req);
|
|
2238
|
+
assert(pages.length > 0);
|
|
2239
|
+
for (const entry of pages) {
|
|
2240
|
+
assert(
|
|
2241
|
+
!entry.url.startsWith('http://localhost:3000'),
|
|
2242
|
+
`URL should be path-only, got: ${entry.url}`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
describe('uploadfs relative URLs', function () {
|
|
2250
|
+
|
|
2251
|
+
describe('default (no staticBaseUrl, no relativeUrls)', function () {
|
|
2252
|
+
let apos;
|
|
2253
|
+
|
|
2254
|
+
before(async function () {
|
|
2255
|
+
apos = await t.create({
|
|
2256
|
+
root: module,
|
|
2257
|
+
baseUrl: 'http://localhost:3000',
|
|
2258
|
+
modules: {}
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
after(async function () {
|
|
2263
|
+
await t.destroy(apos);
|
|
2264
|
+
apos = null;
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
it('uploadsUrl includes baseUrl (BC)', function () {
|
|
2268
|
+
assert.strictEqual(
|
|
2269
|
+
apos.uploadfs.getUrl(),
|
|
2270
|
+
'http://localhost:3000/uploads'
|
|
2271
|
+
);
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
describe('with staticBaseUrl configured', function () {
|
|
2276
|
+
let apos;
|
|
2277
|
+
|
|
2278
|
+
before(async function () {
|
|
2279
|
+
apos = await t.create({
|
|
2280
|
+
root: module,
|
|
2281
|
+
baseUrl: 'http://localhost:3000',
|
|
2282
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2283
|
+
modules: {}
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
after(async function () {
|
|
2288
|
+
await t.destroy(apos);
|
|
2289
|
+
apos = null;
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
it('uploadsUrl is path-only (no host)', function () {
|
|
2293
|
+
assert.strictEqual(apos.uploadfs.getUrl(), '/uploads');
|
|
2294
|
+
});
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
describe('with staticBaseUrl as empty string', function () {
|
|
2298
|
+
let apos;
|
|
2299
|
+
|
|
2300
|
+
before(async function () {
|
|
2301
|
+
apos = await t.create({
|
|
2302
|
+
root: module,
|
|
2303
|
+
baseUrl: 'http://localhost:3000',
|
|
2304
|
+
staticBaseUrl: '',
|
|
2305
|
+
modules: {}
|
|
2306
|
+
});
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
after(async function () {
|
|
2310
|
+
await t.destroy(apos);
|
|
2311
|
+
apos = null;
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
it('uploadsUrl includes baseUrl (empty string is falsy, BC preserved)', function () {
|
|
2315
|
+
assert.strictEqual(
|
|
2316
|
+
apos.uploadfs.getUrl(),
|
|
2317
|
+
'http://localhost:3000/uploads'
|
|
2318
|
+
);
|
|
2319
|
+
});
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
describe('with relativeUrls option (no staticBaseUrl)', function () {
|
|
2323
|
+
let apos;
|
|
2324
|
+
|
|
2325
|
+
before(async function () {
|
|
2326
|
+
apos = await t.create({
|
|
2327
|
+
root: module,
|
|
2328
|
+
baseUrl: 'http://localhost:3000',
|
|
2329
|
+
modules: {
|
|
2330
|
+
'@apostrophecms/uploadfs': {
|
|
2331
|
+
options: {
|
|
2332
|
+
relativeUrls: true
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
after(async function () {
|
|
2340
|
+
await t.destroy(apos);
|
|
2341
|
+
apos = null;
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
it('uploadsUrl is path-only', function () {
|
|
2345
|
+
assert.strictEqual(apos.uploadfs.getUrl(), '/uploads');
|
|
2346
|
+
});
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
describe('with both staticBaseUrl and relativeUrls', function () {
|
|
2350
|
+
let apos;
|
|
2351
|
+
|
|
2352
|
+
before(async function () {
|
|
2353
|
+
apos = await t.create({
|
|
2354
|
+
root: module,
|
|
2355
|
+
baseUrl: 'http://localhost:3000',
|
|
2356
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2357
|
+
modules: {
|
|
2358
|
+
'@apostrophecms/uploadfs': {
|
|
2359
|
+
options: {
|
|
2360
|
+
relativeUrls: true
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
after(async function () {
|
|
2368
|
+
await t.destroy(apos);
|
|
2369
|
+
apos = null;
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
it('uploadsUrl is path-only', function () {
|
|
2373
|
+
assert.strictEqual(apos.uploadfs.getUrl(), '/uploads');
|
|
2374
|
+
});
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
describe('with prefix and staticBaseUrl', function () {
|
|
2378
|
+
let apos;
|
|
2379
|
+
|
|
2380
|
+
before(async function () {
|
|
2381
|
+
apos = await t.create({
|
|
2382
|
+
root: module,
|
|
2383
|
+
baseUrl: 'http://localhost:3000',
|
|
2384
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2385
|
+
prefix: '/cms',
|
|
2386
|
+
modules: {}
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
after(async function () {
|
|
2391
|
+
await t.destroy(apos);
|
|
2392
|
+
apos = null;
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
it('uploadsUrl preserves prefix but omits host', function () {
|
|
2396
|
+
assert.strictEqual(apos.uploadfs.getUrl(), '/cms/uploads');
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
|
|
2400
|
+
describe('with prefix and relativeUrls (no staticBaseUrl)', function () {
|
|
2401
|
+
let apos;
|
|
2402
|
+
|
|
2403
|
+
before(async function () {
|
|
2404
|
+
apos = await t.create({
|
|
2405
|
+
root: module,
|
|
2406
|
+
baseUrl: 'http://localhost:3000',
|
|
2407
|
+
prefix: '/cms',
|
|
2408
|
+
modules: {
|
|
2409
|
+
'@apostrophecms/uploadfs': {
|
|
2410
|
+
options: {
|
|
2411
|
+
relativeUrls: true
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
after(async function () {
|
|
2419
|
+
await t.destroy(apos);
|
|
2420
|
+
apos = null;
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
it('uploadsUrl preserves prefix but omits host', function () {
|
|
2424
|
+
assert.strictEqual(apos.uploadfs.getUrl(), '/cms/uploads');
|
|
2425
|
+
});
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
describe('cloud storage overrides uploadsUrl', function () {
|
|
2429
|
+
let apos;
|
|
2430
|
+
|
|
2431
|
+
before(async function () {
|
|
2432
|
+
apos = await t.create({
|
|
2433
|
+
root: module,
|
|
2434
|
+
baseUrl: 'http://localhost:3000',
|
|
2435
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2436
|
+
modules: {
|
|
2437
|
+
'@apostrophecms/uploadfs': {
|
|
2438
|
+
options: {
|
|
2439
|
+
uploadfs: {
|
|
2440
|
+
uploadsUrl: 'https://cdn.example.com/uploads'
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
after(async function () {
|
|
2449
|
+
await t.destroy(apos);
|
|
2450
|
+
apos = null;
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
it('explicit uploadsUrl from cloud config takes precedence', function () {
|
|
2454
|
+
assert.strictEqual(
|
|
2455
|
+
apos.uploadfs.getUrl(),
|
|
2456
|
+
'https://cdn.example.com/uploads'
|
|
2457
|
+
);
|
|
2458
|
+
});
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
describe('staticBuildHeader middleware', function () {
|
|
2464
|
+
|
|
2465
|
+
describe('with staticBaseUrl configured', function () {
|
|
2466
|
+
let apos;
|
|
2467
|
+
|
|
2468
|
+
before(async function () {
|
|
2469
|
+
apos = await t.create({
|
|
2470
|
+
root: module,
|
|
2471
|
+
baseUrl: 'http://localhost:3000',
|
|
2472
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2473
|
+
modules: {
|
|
2474
|
+
'@apostrophecms/express': {
|
|
2475
|
+
options: {
|
|
2476
|
+
externalFrontKey: 'test-key'
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
'@apostrophecms/url': {
|
|
2480
|
+
options: { static: true }
|
|
2481
|
+
},
|
|
2482
|
+
article: {
|
|
2483
|
+
extend: '@apostrophecms/piece-type',
|
|
2484
|
+
options: {
|
|
2485
|
+
name: 'article',
|
|
2486
|
+
label: 'Article',
|
|
2487
|
+
alias: 'article',
|
|
2488
|
+
publicApiProjection: {
|
|
2489
|
+
title: 1,
|
|
2490
|
+
_url: 1
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
'article-page': {
|
|
2495
|
+
extend: '@apostrophecms/piece-page-type',
|
|
2496
|
+
options: {
|
|
2497
|
+
name: 'articlePage',
|
|
2498
|
+
label: 'Articles',
|
|
2499
|
+
alias: 'articlePage'
|
|
2500
|
+
}
|
|
2501
|
+
},
|
|
2502
|
+
'@apostrophecms/page': {
|
|
2503
|
+
options: {
|
|
2504
|
+
park: [
|
|
2505
|
+
{
|
|
2506
|
+
title: 'Articles',
|
|
2507
|
+
type: 'articlePage',
|
|
2508
|
+
slug: '/articles',
|
|
2509
|
+
parkedId: 'articles'
|
|
2510
|
+
}
|
|
2511
|
+
]
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
const req = apos.task.getReq();
|
|
2518
|
+
await apos.article.insert(req, {
|
|
2519
|
+
title: 'Middleware Test Article',
|
|
2520
|
+
visibility: 'public'
|
|
2521
|
+
});
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
after(async function () {
|
|
2525
|
+
await t.destroy(apos);
|
|
2526
|
+
apos = null;
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
it('sets req.aposStaticBuild via direct API call with header', async function () {
|
|
2530
|
+
const jar = apos.http.jar();
|
|
2531
|
+
const response = await apos.http.get('/api/v1/article', {
|
|
2532
|
+
jar,
|
|
2533
|
+
headers: {
|
|
2534
|
+
'x-apos-static-base-url': '1'
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2537
|
+
assert(response);
|
|
2538
|
+
assert(response.results);
|
|
2539
|
+
assert(response.results.length > 0);
|
|
2540
|
+
// With aposStaticBuild and staticBaseUrl configured, piece _url
|
|
2541
|
+
// should use staticBaseUrl (not baseUrl)
|
|
2542
|
+
for (const piece of response.results) {
|
|
2543
|
+
assert(
|
|
2544
|
+
piece._url.startsWith('https://www.example.com'),
|
|
2545
|
+
`_url should use staticBaseUrl, got: ${piece._url}`
|
|
2546
|
+
);
|
|
2547
|
+
assert(
|
|
2548
|
+
!piece._url.startsWith('http://localhost:3000'),
|
|
2549
|
+
`_url should NOT use baseUrl, got: ${piece._url}`
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
it('does not set aposStaticBuild without the header', async function () {
|
|
2555
|
+
const jar = apos.http.jar();
|
|
2556
|
+
const response = await apos.http.get('/api/v1/article', {
|
|
2557
|
+
jar
|
|
2558
|
+
});
|
|
2559
|
+
assert(response);
|
|
2560
|
+
assert(response.results);
|
|
2561
|
+
assert(response.results.length > 0);
|
|
2562
|
+
// Without the header, piece _url should include baseUrl
|
|
2563
|
+
for (const piece of response.results) {
|
|
2564
|
+
assert(
|
|
2565
|
+
piece._url.startsWith('http://localhost:3000'),
|
|
2566
|
+
`_url should include baseUrl without the header, got: ${piece._url}`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
|
|
2571
|
+
it('externalFront still works when both headers are sent', async function () {
|
|
2572
|
+
const jar = apos.http.jar();
|
|
2573
|
+
const response = await apos.http.get('/api/v1/@apostrophecms/url', {
|
|
2574
|
+
jar,
|
|
2575
|
+
headers: {
|
|
2576
|
+
'x-requested-with': 'AposExternalFront',
|
|
2577
|
+
'apos-external-front-key': 'test-key',
|
|
2578
|
+
'x-apos-static-base-url': '1'
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
assert(response);
|
|
2582
|
+
assert(response.pages);
|
|
2583
|
+
for (const entry of response.pages) {
|
|
2584
|
+
assert(
|
|
2585
|
+
!entry.url.startsWith('http://'),
|
|
2586
|
+
`URL should be path-only via externalFront, got: ${entry.url}`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
describe('without staticBaseUrl (header still sets aposStaticBuild)', function () {
|
|
2593
|
+
let apos;
|
|
2594
|
+
|
|
2595
|
+
before(async function () {
|
|
2596
|
+
apos = await t.create({
|
|
2597
|
+
root: module,
|
|
2598
|
+
baseUrl: 'http://localhost:3000',
|
|
2599
|
+
modules: {
|
|
2600
|
+
article: {
|
|
2601
|
+
extend: '@apostrophecms/piece-type',
|
|
2602
|
+
options: {
|
|
2603
|
+
name: 'article',
|
|
2604
|
+
label: 'Article',
|
|
2605
|
+
alias: 'article',
|
|
2606
|
+
publicApiProjection: {
|
|
2607
|
+
title: 1,
|
|
2608
|
+
_url: 1
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
},
|
|
2612
|
+
'article-page': {
|
|
2613
|
+
extend: '@apostrophecms/piece-page-type',
|
|
2614
|
+
options: {
|
|
2615
|
+
name: 'articlePage',
|
|
2616
|
+
label: 'Articles',
|
|
2617
|
+
alias: 'articlePage'
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
'@apostrophecms/page': {
|
|
2621
|
+
options: {
|
|
2622
|
+
park: [
|
|
2623
|
+
{
|
|
2624
|
+
title: 'Articles',
|
|
2625
|
+
type: 'articlePage',
|
|
2626
|
+
slug: '/articles',
|
|
2627
|
+
parkedId: 'articles'
|
|
2628
|
+
}
|
|
2629
|
+
]
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
const req = apos.task.getReq();
|
|
2636
|
+
await apos.article.insert(req, {
|
|
2637
|
+
title: 'No StaticBaseUrl Article',
|
|
2638
|
+
visibility: 'public'
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
after(async function () {
|
|
2643
|
+
await t.destroy(apos);
|
|
2644
|
+
apos = null;
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
it('sets aposStaticBuild even without staticBaseUrl (matches externalFront behavior)', async function () {
|
|
2648
|
+
const jar = apos.http.jar();
|
|
2649
|
+
const response = await apos.http.get('/api/v1/article', {
|
|
2650
|
+
jar,
|
|
2651
|
+
headers: {
|
|
2652
|
+
'x-apos-static-base-url': '1'
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
assert(response);
|
|
2656
|
+
assert(response.results);
|
|
2657
|
+
assert(response.results.length > 0);
|
|
2658
|
+
// With aposStaticBuild=true and no staticBaseUrl, getBaseUrl returns ''
|
|
2659
|
+
// so _url should be path-only
|
|
2660
|
+
for (const piece of response.results) {
|
|
2661
|
+
assert(
|
|
2662
|
+
!piece._url.startsWith('http://'),
|
|
2663
|
+
`_url should be path-only when aposStaticBuild is true, got: ${piece._url}`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
describe('non-API routes ignore the header', function () {
|
|
2670
|
+
let apos;
|
|
2671
|
+
|
|
2672
|
+
before(async function () {
|
|
2673
|
+
apos = await t.create({
|
|
2674
|
+
root: module,
|
|
2675
|
+
baseUrl: 'http://localhost:3000',
|
|
2676
|
+
staticBaseUrl: 'https://www.example.com',
|
|
2677
|
+
modules: {}
|
|
2678
|
+
});
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
after(async function () {
|
|
2682
|
+
await t.destroy(apos);
|
|
2683
|
+
apos = null;
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
it('non-API request with the header does not cause errors', async function () {
|
|
2687
|
+
const jar = apos.http.jar();
|
|
2688
|
+
// Request the home page (non-API route) with the static header
|
|
2689
|
+
// — should be ignored and not cause issues
|
|
2690
|
+
const response = await apos.http.get('/', {
|
|
2691
|
+
jar,
|
|
2692
|
+
headers: {
|
|
2693
|
+
'x-apos-static-base-url': '1'
|
|
2694
|
+
},
|
|
2695
|
+
fullResponse: true
|
|
2696
|
+
});
|
|
2697
|
+
assert.strictEqual(response.status, 200);
|
|
2698
|
+
});
|
|
2699
|
+
});
|
|
2700
|
+
});
|
|
2701
|
+
});
|