UNPKG

apostrophe

Version:
1,721 lines (1,583 loc) • 79.4 kB
const { createId } = require('@paralleldrive/cuid2'); const assert = require('assert').strict; const fs = require('fs'); const path = require('path'); const _ = require('lodash'); const FormData = require('form-data'); const t = require('../test-lib/test.js'); describe('Pieces', function() { let apos; let jar; const apiKey = 'this is a test api key'; this.timeout(t.timeout); let editor; let contributor; let guest; before(async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/express': { options: { apiKeys: { [apiKey]: { role: 'admin' } } } }, thing: { extend: '@apostrophecms/piece-type', options: { alias: 'thing', name: 'thing', label: 'Thing', publicApiProjection: { title: 1, _url: 1 } }, fields: { add: { foo: { label: 'Foo', type: 'string' } } } }, person: { extend: '@apostrophecms/piece-type', options: { alias: 'person', name: 'person', label: 'Person', publicApiProjection: { title: 1, _url: 1 } }, fields: { add: { _things: { type: 'relationship' }, _tools: { type: 'relationship', withType: 'thing', fields: { add: { skillLevel: { type: 'integer' } } } } } } }, product: { extend: '@apostrophecms/piece-type', options: { publicApiProjection: { title: 1, _url: 1, _articles: 1, relationshipsInArray: 1, relationshipsInObject: 1 } }, fields: { add: { body: { type: 'area', options: { widgets: { '@apostrophecms/rich-text': {}, '@apostrophecms/image': {} } } }, color: { type: 'select', choices: [ { label: 'Red', value: 'red' }, { label: 'Blue', value: 'blue' } ] }, photo: { type: 'attachment', group: 'images' }, _articles: { type: 'relationship', withType: 'article', builders: { project: { _url: 1, title: 1 } }, fields: { add: { relevance: { // Explains the relevance of the article to the // product in 1 sentence type: 'string' } } } }, relationshipsInArray: { type: 'array', fields: { add: { _articles: { type: 'relationship', withType: 'article' } } } }, relationshipsInObject: { type: 'object', fields: { add: { _articles: { type: 'relationship', withType: 'article' } } } } } } }, article: { extend: '@apostrophecms/piece-type', options: { publicApiProjection: { title: 1, _url: 1 } }, fields: { add: { name: { type: 'string' }, _products: { type: 'relationshipReverse', withType: 'product' } } } }, constrained: { options: { alias: 'constrained' }, extend: '@apostrophecms/piece-type', fields: { add: { description: { type: 'string', min: 5, max: 10 } } } }, resume: { options: { alias: 'resume' }, extend: '@apostrophecms/piece-type', fields: { add: { attachment: { type: 'attachment', required: true } } } }, 'product-page': { extend: '@apostrophecms/piece-page-type', options: { name: 'productPage', label: 'Products', alias: 'productPage', perPage: 10 } }, '@apostrophecms/page': { options: { park: [ { title: 'Products', type: 'productPage', slug: '/products', parkedId: 'products' } ] } }, board: { extend: '@apostrophecms/piece-type', options: { alias: 'board', name: 'board', label: 'Board', editRole: 'contributor', publishRole: 'admin' }, fields: { add: { stock: { name: 'stock', type: 'integer', label: 'stock', permission: { action: 'edit', type: 'board' } }, discontinued: { name: 'discontinued', type: 'string', label: 'Discontinued', viewPermission: { action: 'publish', type: 'board' } }, nickname: { name: 'nickname', type: 'string', label: 'nickname', editPermission: { action: 'edit', type: 'board' } }, sku: { name: 'sku', type: 'string', label: 'SKU', editPermission: { action: 'publish', type: 'board' } }, hidden: { name: 'hidden', type: 'boolean', label: 'Hidden?', viewPermission: { action: 'publish', type: 'board' }, editPermission: { action: 'publish', type: 'board' } }, _users: { type: 'relationship', label: 'users', withType: '@apostrophecms/user', required: true }, array: { type: 'array', label: 'array', viewPermission: { action: 'publish', type: 'board' }, editPermission: { action: 'publish', type: 'board' }, fields: { add: { title: { type: 'string', label: 'title', required: true }, description: { type: 'string', label: 'description' }, _users: { type: 'relationship', label: 'users', withType: '@apostrophecms/user', required: true } } } }, object: { type: 'object', label: 'object', viewPermission: { action: 'publish', type: 'board' }, editPermission: { action: 'publish', type: 'board' }, fields: { add: { title: { type: 'string', label: 'title', required: true }, description: { type: 'string', label: 'description' }, _users: { type: 'relationship', label: 'users', withType: '@apostrophecms/user', required: true } } } } } } } } }); }); after(function () { return t.destroy(apos); }); /// /// // EXISTENCE /// /// it('should initialize with a schema', function() { assert(apos.modules.thing); assert(apos.modules.thing.schema); }); // little test-helper function to get piece by id regardless of archive status async function findPiece(req, id) { const piece = apos.modules.thing.find(req, { _id: id }) .permission('edit') .archived(null) .toObject(); if (!piece) { throw apos.error('notfound'); } return piece; } const testThing = { _id: 'testThing:en:published', aposDocId: 'testThing', aposLocale: 'en:published', title: 'hello', foo: 'bar' }; let insertedOne, insertedTwo; const additionalThings = [ { _id: 'thing1:en:published', title: 'Red' }, { _id: 'thing2:en:published', title: 'Blue' }, { _id: 'thing3:en:published', title: 'Green' } ]; const testPeople = [ { _id: 'person1:en:published', title: 'Bob', type: 'person', thingsIds: [ 'thing2', 'thing3' ] } ]; // Test pieces.newInstance() it('should be able to create a new piece', function() { assert(apos.modules.thing.newInstance); const thing = apos.modules.thing.newInstance(); assert(thing); assert(thing.type === 'thing'); }); // Test pieces.insert() it('should be able to insert a piece into the database', async function() { assert(apos.modules.thing.insert); insertedOne = await apos.modules.thing.insert(apos.task.getReq(), testThing); }); it('should be able to insert a second piece into the database', async function() { assert(apos.modules.thing.insert); const template = { ...testThing }; template._id = null; template.aposDocId = null; template.title = 'hello #2'; insertedTwo = await apos.modules.thing.insert(apos.task.getReq(), template); }); it('should be able to retrieve a piece by id from the database', async function() { assert(apos.modules.thing.requireOneForEditing); const req = apos.task.getReq(); req.piece = await apos.modules.thing.requireOneForEditing(req, { _id: 'testThing:en:published' }); assert(req.piece); assert(req.piece._id === 'testThing:en:published'); assert(req.piece.title === 'hello'); assert(req.piece.foo === 'bar'); }); it('should be able to retrieve the next piece from the database per sort order', async function() { const req = apos.task.getReq(); // The default sort order is reverse chronological, so "next" is older, not // newer const next = await apos.modules.thing.find(req).next(insertedTwo).toObject(); assert(next.title === 'hello'); }); it('should be able to retrieve the previous piece from the database', async function() { const req = apos.task.getReq(); // The default sort order is reverse chronological, so "previous" is newer, // not older const previous = await apos.modules.thing.find(req).previous(insertedOne).toObject(); assert(previous.title === 'hello #2'); }); // Test pieces.update() it('should be able to update a piece in the database', async function() { assert(apos.modules.thing.update); testThing.foo = 'moo'; const piece = await apos.modules.thing.update(apos.task.getReq(), testThing); assert(testThing === piece); // Now let's get the piece and check if it was updated const req = apos.task.getReq(); req.piece = await apos.modules.thing.requireOneForEditing(req, { _id: 'testThing:en:published' }); assert(req.piece); assert(req.piece._id === 'testThing:en:published'); assert(req.piece.foo === 'moo'); }); // Test pieces.addListFilters() it('should only execute filters that are safe and have a launder method', function() { let publicTest = false; let manageTest = false; // addListFilters should execute launder and filters for filter // definitions that are safe for 'public' or 'manage' contexts const mockCursor = apos.doc.find(apos.task.getAnonReq()); _.merge(mockCursor, { builders: { publicTest: { launder: function(s) { return 'laundered'; } }, manageTest: { launder: function(s) { return 'laundered'; } }, unsafeTest: {} }, publicTest: function(value) { assert(value === 'laundered'); publicTest = true; }, manageTest: function(value) { assert(value === 'laundered'); manageTest = true; }, unsafeTest: function(value) { assert.fail('unsafe filter ran'); } }); const filters = { publicTest: 'foo', manageTest: 'bar', unsafeTest: 'nope', fakeTest: 'notEvenReal' }; mockCursor.applyBuildersSafely(filters); assert(publicTest === true); assert(manageTest === true); }); it('should be able to archive a piece with proper deduplication', async function() { assert(apos.modules.thing.requireOneForEditing); const req = apos.task.getReq(); const id = 'testThing:en:published'; req.body = { _id: id }; // let's make sure the piece is not archived to start const piece = await findPiece(req, id); assert(!piece.archived); piece.archived = true; await apos.modules.thing.update(req, piece); // let's get the piece to make sure it is archived const piece2 = await findPiece(req, id); assert(piece2); assert(piece2.archived === true); assert(piece2.aposWasArchived === true); assert.equal(piece2.slug, 'deduplicate-testThing-hello'); }); it('should be able to rescue a archived piece with proper deduplication', async function() { const req = apos.task.getReq(); const id = 'testThing:en:published'; req.body = { _id: id }; // let's make sure the piece is archived to start const piece = await findPiece(req, id); assert(piece.archived === true); piece.archived = false; await apos.modules.thing.update(req, piece); const piece2 = await findPiece(req, id); assert(piece2); assert(!piece2.archived); assert(!piece2.aposWasArchived); assert(piece2.slug === 'hello'); }); it('should be able to insert test users', async function() { assert(apos.user.newInstance); const user = apos.user.newInstance(); assert(user); user.title = 'admin'; user.username = 'admin'; user.password = 'admin'; user.email = 'ad@min.com'; user.role = 'admin'; await apos.user.insert(apos.task.getReq(), user); const user2 = apos.user.newInstance(); user2.title = 'admin2'; user2.username = 'admin2'; user2.password = 'admin2'; user2.email = 'ad@min2.com'; user2.role = 'admin'; return apos.user.insert(apos.task.getReq(), user2); }); it('people can find things via a relationship', async function() { const req = apos.task.getReq(); for (const person of testPeople) { await apos.person.insert(req, person); } for (const thing of additionalThings) { await apos.thing.insert(req, thing); } const person = await apos.doc.getManager('person').find(req, {}).toObject(); assert(person); assert(person.title === 'Bob'); assert(person._things); assert(person._things.length === 2); }); it('people cannot find things via a relationship with an inadequate projection', function() { const req = apos.task.getReq(); return apos.doc.getManager('person').find(req, {}, { // Use the options object rather than a chainable method project: { title: 1 } }).toObject() .then(function(person) { assert(person); assert(person.title === 'Bob'); // Verify projection assert(!person.slug); assert((!person._things) || (person._things.length === 0)); }); }); it('people can find things via a relationship with a "projection" of the relationship name', function() { const req = apos.task.getReq(); return apos.doc.getManager('person').find(req, {}, { project: { title: 1, _things: 1 } }).toObject() .then(function(person) { assert(person); assert(person.title === 'Bob'); assert(person._things); assert(person._things.length === 2); }); }); it('should be able to log in as admin', async function() { jar = apos.http.jar(); // establish session let page = await apos.http.get('/', { jar }); assert(page.match(/logged out/)); // Log in await apos.http.post('/api/v1/@apostrophecms/login/login', { body: { username: 'admin', password: 'admin', session: true }, jar }); // Confirm login page = await apos.http.get('/', { jar }); assert(page.match(/logged in/)); }); it('can attach a tool to a person via the REST API', async function() { const person1 = await apos.http.get('/api/v1/person/person1:en:published'); assert(person1); const thing1 = await apos.http.get('/api/v1/thing/thing1:en:published'); assert(thing1); person1._tools = [ { ...thing1, _fields: { skillLevel: 5 } } ]; await apos.http.put('/api/v1/person/person1:en:published', { body: person1, jar }); const person1After = await apos.http.get('/api/v1/person/person1:en:published', { jar }); assert(person1After); assert(person1After._tools); assert(person1After._tools.length); assert(person1After._tools[0].title === 'Red'); assert(person1After._tools[0]._fields); assert(person1After._tools[0]._fields.skillLevel === 5); }); it('cannot POST a product without a session', async function() { try { await apos.http.post('/api/v1/product', { body: { title: 'Fake Product', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is fake</p>' } ] } } }); // Should not get here assert(false); } catch (e) { assert(e.status === 403); } }); let updateProduct; it('can POST products with a session, some visible', async function() { // range is exclusive at the top end, I want 10 things let widgetId; for (let i = 1; (i <= 10); i++) { if (i === 1) { widgetId = createId(); } const response = await apos.http.post('/api/v1/product', { body: { title: 'Cool Product #' + i, visibility: (i & 1) ? 'loginRequired' : 'public', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', _id: (i === 1) ? widgetId : null, content: '<p>This is thing ' + i + '</p>' }, // Intentional attempt to use duplicate _id { metaType: 'widget', type: '@apostrophecms/rich-text', _id: (i === 1) ? widgetId : null, content: '<p>This is thing ' + i + ' second widget</p>' } ] } }, jar }); assert(response); assert(response._id); assert(response.body); assert(response.title === 'Cool Product #' + i); assert(response.slug === 'cool-product-' + i); assert(response.type === 'product'); assert(response.body.items[0].content === `<p>This is thing ${i}</p>`); assert(response.body.items[1].content === `<p>This is thing ${i} second widget</p>`); if (i === 1) { // Deduplicate any duplicate ids we specified at doc level assert(response.body.items[0]._id === widgetId); assert(response.body.items[1]._id); // Quietly deduplicated for us assert(response.body.items[1]._id !== widgetId); updateProduct = response; } else { // All new _ids if we did not specify assert(response.body.items[0]._id); assert(response.body.items[1]._id); assert(response.body.items[0]._id !== response.body.items[1]._id); assert(response.body.items[0]._id !== widgetId); } } }); it('can GET five of those products without the user session', async function() { const response = await apos.http.get('/api/v1/product'); assert(response); assert(response.results); assert(response.results.length === 5); }); it('can GET all of those products with a user session', async function() { const response = await apos.http.get('/api/v1/product', { jar }); assert(response); assert(response.results); assert(response.results.length === 10); }); let firstId; it('can GET only 5 if perPage is 5', async function() { const response = await apos.http.get('/api/v1/product?perPage=5', { jar }); assert(response); assert(response.results); assert(response.results.length === 5); firstId = response.results[0]._id; assert(response.pages === 2); }); it('can GET a different 5 on page 2', async function() { const response = await apos.http.get('/api/v1/product?perPage=5&page=2', { jar }); assert(response); assert(response.results); assert(response.results.length === 5); assert(response.results[0]._id !== firstId); assert(response.pages === 2); }); it('can GET the results sorted ascending', async function() { const response = await apos.http.get('/api/v1/product?perPage=5&sort[title]=1', { jar }); const actual = response.results.map(result => result.title); const expected = [ 'Cool Product #1', 'Cool Product #10', 'Cool Product #2', 'Cool Product #3', 'Cool Product #4' ]; assert.deepEqual(actual, expected); }); it('can GET the results sorted descending', async function() { const response = await apos.http.get('/api/v1/product?perPage=5&sort[title]=-1', { jar }); const actual = response.results.map(result => result.title); const expected = [ 'Cool Product #9', 'Cool Product #8', 'Cool Product #7', 'Cool Product #6', 'Cool Product #5' ]; assert.deepEqual(actual, expected); }); it('can update a product with PUT', async function() { const args = { body: { ...updateProduct, title: 'I like cheese', _id: 'should-not-change:en:published' }, jar }; const response = await apos.http.put(`/api/v1/product/${updateProduct._id}`, args); assert(response); assert(response._id === updateProduct._id); assert(response.title === 'I like cheese'); assert(response.body.items.length); }); it('fetch of updated product shows updated content', async function() { const response = await apos.http.get(`/api/v1/product/${updateProduct._id}`, { jar }); assert(response); assert(response._id === updateProduct._id); assert(response.title === 'I like cheese'); assert(response.body.items.length); }); it('can archive a product', async function() { return apos.http.patch(`/api/v1/product/${updateProduct._id}`, { body: { archived: true }, jar }); }); it('cannot fetch a archived product', async function() { try { await apos.http.get(`/api/v1/product/${updateProduct._id}`, { jar }); // Should have been a 404, 200 = test fails assert(false); } catch (e) { assert(e.status === 404); } }); it('can fetch archived product with archived=any and the right user', async function() { const product = await apos.http.get(`/api/v1/product/${updateProduct._id}?archived=any`, { jar }); // Should have been a 404, 200 = test fails assert(product.archived); }); let relatedProductId; it('can insert a product with relationships', async function() { let response = await apos.http.post('/api/v1/article', { body: { title: 'First Article', name: 'first-article' }, jar }); const article = response; assert(article); assert(article.title === 'First Article'); article._fields = { relevance: 'The very first article that was ever published about this product' }; response = await apos.http.post('/api/v1/product', { body: { title: 'Product Key Product With Relationship', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is the product key product with relationship</p>' } ] }, _articles: [ article ], relationshipsInArray: [ { _articles: [ article ] } ], relationshipsInObject: { _articles: [ article ] } }, jar }); assert(response._id); assert(response.articlesIds[0] === article.aposDocId); assert(response.articlesFields[article.aposDocId].relevance === 'The very first article that was ever published about this product'); assert(response.relationshipsInArray[0].articlesIds[0] === article.aposDocId); assert(response.relationshipsInObject.articlesIds[0] === article.aposDocId); relatedProductId = response._id; }); it('can insert a product with _newInstance and additional properties', async function() { const newInstance = await apos.http.post('/api/v1/product', { body: { _newInstance: true, title: 'Product 01' }, jar }); const inserted = await apos.http.post('/api/v1/product', { body: { ...newInstance, body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is the product key product with relationship</p>' } ] } }, jar }); const actual = { newInstance, inserted }; const expected = { newInstance: { _articles: null, _previewable: true, archived: false, body: { _id: newInstance.body._id, items: [], metaType: 'area' }, color: null, photo: null, relationshipsInArray: [], relationshipsInObject: { _articles: null }, slug: '', title: 'Product 01', type: 'product', visibility: 'public' }, inserted: { _articles: [], _create: true, _delete: true, _edit: true, _id: inserted._id, _parent: inserted._parent, _parentSlug: '/products', _parentUrl: '/products', _publish: true, _url: '/products/product-01', aposDocId: inserted.aposDocId, aposLocale: 'en:published', aposMode: 'published', archived: false, articlesFields: {}, articlesIds: [], body: { _docId: inserted.body._docId, _edit: true, _id: inserted.body._id, items: [ { _docId: inserted.body.items.at(0)._docId, _edit: true, _id: inserted.body.items.at(0)._id, aposPlaceholder: false, content: '<p>This is the product key product with relationship</p>', imageIds: [], metaType: 'widget', permalinkIds: [], type: '@apostrophecms/rich-text' } ], metaType: 'area' }, cacheInvalidatedAt: inserted.cacheInvalidatedAt, color: null, createdAt: inserted.createdAt, highSearchText: inserted.highSearchText, highSearchWords: inserted.highSearchWords, lastPublishedAt: inserted.lastPublishedAt, lowSearchText: inserted.lowSearchText, metaType: 'doc', photo: null, relationshipsInArray: [], relationshipsInObject: { _articles: [], _id: inserted.relationshipsInObject._id, articlesFields: {}, articlesIds: [], metaType: 'object', scopedObjectName: 'doc.product.relationshipsInObject' }, searchSummary: inserted.searchSummary, slug: 'product-01', title: 'Product 01', titleSortified: inserted.titleSortified, type: 'product', updatedAt: inserted.updatedAt, updatedBy: { _id: inserted.updatedBy._id, title: 'admin', username: 'admin' }, visibility: 'public' } }; assert.deepEqual(actual, expected); }); it('can GET a product with relationships', async function() { const response = await apos.http.get('/api/v1/product'); assert(response); assert(response.results); const product = _.find(response.results, { slug: 'product-key-product-with-relationship' }); assert(Array.isArray(product._articles)); assert(product._articles.length === 1); assert(product._articles[0]._fields); assert.strictEqual(product._articles[0]._fields.relevance, 'The very first article that was ever published about this product'); assert(product.relationshipsInArray[0]._articles[0].title === 'First Article'); assert(product.relationshipsInObject._articles[0].title === 'First Article'); }); let relatedArticleId; it('can GET a single product with relationships', async function() { const response = await apos.http.get(`/api/v1/product/${relatedProductId}`); assert(response); assert(response._articles); assert(response._articles.length === 1); relatedArticleId = response._articles[0]._id; }); it('can GET a single product using projections', async function() { const response = await apos.http.get(`/api/v1/product/${relatedProductId}`, { qs: { project: { _id: 1, title: 1 } } }); const keys = Object.keys(response); assert(response); // type is available by default assert([ '_id', 'type', 'title' ].every(expectedKey => keys.includes(expectedKey))); }); it('can GET a single product using projections with fields omission', async function() { const response = await apos.http.get(`/api/v1/product/${relatedProductId}`, { qs: { project: { highSearchText: 0, highSearchWords: 0, lowSearchText: 0, searchSummary: 0 } } }); assert(response); }); it('can GET a single article with reverse relationships', async function() { const response = await apos.http.get(`/api/v1/article/${relatedArticleId}`); assert(response); assert(response._products); assert(response._products.length === 1); assert(response._products[0]._id === relatedProductId); }); it('can GET a single article with reverse relationships in draft mode', async function() { const draftRelatedArticleId = relatedArticleId.replace(':published', ':draft'); const draftRelatedProductId = relatedProductId.replace(':published', ':draft'); const response = await apos.http.get(`/api/v1/article/${draftRelatedArticleId}`, { jar }); assert(response); assert(response._products); assert(response._products.length === 1); assert(response._products[0]._id === draftRelatedProductId); }); it('can GET results plus filter choices and ignore bogus filter names in choices', async function() { const response = await apos.http.get('/api/v1/product?choices=title,visibility,_articles,articles,bogus', { jar }); assert(response); assert(response.results); assert(response.choices.title); assert(response.choices.title[0].label.match(/Cool Product/)); assert(response.choices.visibility); assert(response.choices.visibility.length === 2); assert(response.choices.visibility.find(item => item.value === 'loginRequired')); assert(response.choices.visibility.find(item => item.value === 'public')); assert(response.choices._articles); assert(response.choices._articles[0].label === 'First Article'); // an _id assert(response.choices._articles[0].value.match(/^.+:.+:.+$/)); assert(response.choices.articles[0].label === 'First Article'); // a slug assert(response.choices.articles[0].value === 'first-article'); }); it('can GET results plus filter counts, ignoring bogus filter names', async function() { const response = await apos.http.get('/api/v1/product?_edit=1&counts=title,visibility,_articles,articles,bogus', { jar }); assert(response); assert(response.results); assert(response.counts); assert(response.counts.title); assert(response.counts.title[0].label.match(/Cool Product/)); // Doesn't work for every field type, but does for this assert(response.counts.title[0].count === 1); assert(response.counts.visibility); assert(response.counts.visibility.length === 2); assert(response.counts.visibility.find(item => item.value === 'loginRequired')); assert(response.counts.visibility.find(item => item.value === 'public')); assert(response.counts._articles); assert(response.counts._articles[0].label === 'First Article'); // an _id assert(response.counts._articles[0].value.match(/^.+:.+:.+$/)); assert(response.counts.articles[0].label === 'First Article'); // a slug assert(response.counts.articles[0].value === 'first-article'); }); it('can patch a relationship', async function() { let response = await apos.http.post('/api/v1/article', { jar, body: { title: 'Relationship Article', name: 'relationship-article' } }); const article = response; assert(article); assert(article.title === 'Relationship Article'); response = await apos.http.post('/api/v1/product', { jar, body: { title: 'Initially No Relationship Value', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is the product key product without initial relationship</p>' } ] } } }); const product = response; assert(product._id); response = await apos.http.patch(`/api/v1/product/${product._id}`, { body: { _articles: [ article ] }, jar }); assert(response.title === 'Initially No Relationship Value'); assert(response.articlesIds); assert(response.articlesIds[0] === article.aposDocId); assert(response._articles); assert(response._articles[0]._id === article._id); }); it('can insert a constrained piece that validates', async function() { const constrained = await apos.http.post('/api/v1/constrained', { body: { title: 'First Constrained', description: 'longenough' }, jar }); assert(constrained); assert(constrained.title === 'First Constrained'); assert(constrained.description === 'longenough'); }); it('cannot insert a constrained piece that does not validate', async function() { try { await apos.http.post('/api/v1/constrained', { body: { title: 'Second Constrained', description: 'shrt' }, jar }); // Getting here is bad assert(false); } catch (e) { assert(e); assert(e.status === 400); assert(e.body.data.errors); assert(e.body.data.errors.length === 1); assert(e.body.data.errors[0].path === 'description'); assert(e.body.data.errors[0].name === 'min'); assert(e.body.data.errors[0].code === 400); } }); let advisoryLockTestId; it('can insert a product for advisory lock testing', async function() { const response = await apos.http.post('/api/v1/product', { body: { title: 'Advisory Test', name: 'advisory-test' }, jar }); const article = response; assert(article); assert(article.title === 'Advisory Test'); advisoryLockTestId = article._id; }); it('can get an advisory lock on a product while patching a property', async function() { const product = await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'xyz', lock: true }, title: 'Advisory Test Patched' } }); assert(product.title === 'Advisory Test Patched'); }); it('cannot get an advisory lock with a different context id', async function() { try { await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'pdq', lock: true } } }); assert(false); } catch (e) { assert(e.status === 409); assert(e.body.name === 'locked'); assert(e.body.data.me); } }); it('can get an advisory lock with a different context id if forcing', async function() { await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'pdq', lock: true, force: true } } }); }); it('can renew the advisory lock with the second context id after forcing', async function() { await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'pdq', lock: true } } }); }); it('can unlock the advisory lock while patching a property', async function() { const product = await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'pdq', lock: false }, title: 'Advisory Test Patched Again' } }); assert(product.title === 'Advisory Test Patched Again'); }); it('can relock with the first context id after unlocking', async function() { const doc = await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar, body: { _advisoryLock: { tabId: 'xyz', lock: true } } }); assert(doc.title === 'Advisory Test Patched Again'); }); let jar2; it('should be able to log in as second user', async function() { jar2 = apos.http.jar(); // establish session let page = await apos.http.get('/', { jar: jar2 }); assert(page.match(/logged out/)); // Log in await apos.http.post('/api/v1/@apostrophecms/login/login', { body: { username: 'admin2', password: 'admin2', session: true }, jar: jar2 }); // Confirm login page = await apos.http.get('/', { jar: jar2 }); assert(page.match(/logged in/)); }); it('second user with a distinct tabId gets an appropriate error specifying who has the lock', async function() { try { await apos.http.patch(`/api/v1/product/${advisoryLockTestId}`, { jar: jar2, body: { _advisoryLock: { tabId: 'nbc', lock: true } } }); assert(false); } catch (e) { assert(e.status === 409); assert(e.body.name === 'locked'); assert(!e.body.data.me); assert(e.body.data.username === 'admin'); } }); it('can log out to destroy a session', async function() { await apos.http.post('/api/v1/@apostrophecms/login/logout', { followAllRedirects: true, jar }); await apos.http.post('/api/v1/@apostrophecms/login/logout', { followAllRedirects: true, jar: jar2 }); }); it('cannot POST a product with a logged-out cookie jar', async function() { try { await apos.http.post('/api/v1/product', { body: { title: 'Fake Product After Logout', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is fake</p>' } ] } }, jar }); assert(false); } catch (e) { assert(e.status === 403); } }); let token; let bearerProductId; it('should be able to log in as admin and get a bearer token', async function() { // Log in const response = await apos.http.post('/api/v1/@apostrophecms/login/login', { body: { username: 'admin', password: 'admin' } }); assert(response.token); token = response.token; }); it('can POST a product with the bearer token', async function() { const response = await apos.http.post('/api/v1/product', { body: { title: 'Bearer Token Product', visibility: 'loginRequired', slug: 'bearer-token-product', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is a bearer token thing</p>' } ] } }, headers: { Authorization: `Bearer ${token}` } }); assert(response); assert(response._id); assert(response.body); assert(response.title === 'Bearer Token Product'); assert(response.slug === 'bearer-token-product'); assert(response.type === 'product'); bearerProductId = response._id; }); it('can GET a loginRequired product with the bearer token', async function() { const response = await apos.http.get(`/api/v1/product/${bearerProductId}`, { headers: { Authorization: `Bearer ${token}` } }); assert(response); assert(response.title === 'Bearer Token Product'); }); it('can log out to destroy a bearer token', async function() { return apos.http.post('/api/v1/@apostrophecms/login/logout', { headers: { Authorization: `Bearer ${token}` } }); }); it('cannot GET a loginRequired product with a destroyed bearer token', async function() { try { await apos.http.get(`/api/v1/product/${bearerProductId}`, { headers: { Authorization: `Bearer ${token}` } }); assert(false); } catch (e) { assert(e.status === 401); } }); let apiKeyProductId; it('can POST a product with the api key', async function() { const response = await apos.http.post('/api/v1/product', { body: { title: 'API Key Product', visibility: 'loginRequired', slug: 'api-key-product', body: { metaType: 'area', items: [ { metaType: 'widget', type: '@apostrophecms/rich-text', id: createId(), content: '<p>This is an api key thing</p>' } ] } }, headers: { Authorization: `ApiKey ${apiKey}` } }); assert(response); assert(response._id); assert(response.body); assert(response.title === 'API Key Product'); assert(response.slug === 'api-key-product'); assert(response.type === 'product'); apiKeyProductId = response._id; }); it('can GET a loginRequired product with the api key', async function() { const response = await apos.http.get(`/api/v1/product/${apiKeyProductId}`, { headers: { Authorization: `ApiKey ${apiKey}` } }); assert(response); assert(response.title === 'API Key Product'); }); it('can insert a resume with an attachment', async function() { const formData = new FormData(); formData.append('file', fs.createReadStream(path.join(__dirname, '/public/static-test.txt'))); // Make an async request to upload the image. const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', { headers: { Authorization: `ApiKey ${apiKey}` }, body: formData }); const resume = await apos.http.post('/api/v1/resume', { headers: { Authorization: `ApiKey ${apiKey}` }, body: { title: 'Jane Doe', attachment } }); assert(resume); assert(resume.title === 'Jane Doe'); assert(resume.attachment._url); assert(fs.readFileSync(path.join(__dirname, 'public', resume.attachment._url), 'utf8') === fs.readFileSync(path.join(__dirname, '/public/static-test.txt'), 'utf8')); }); it('should convert a piece keeping only the present fields', async function() { const req = apos.task.getReq(); const inputPiece = { title: 'new product name' }; const existingPiece = { color: 'red' }; await apos.modules.product.convert( req, inputPiece, existingPiece, { presentFieldsOnly: true } ); assert(Object.keys(existingPiece).length === 2); assert(existingPiece.title === 'new product name'); assert(existingPiece.color === 'red'); }); it('should not set a cache-control value when retrieving pieces, when cache option is not set', async function() { const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true }); const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true }); assert(response1.headers['cache-control'] === undefined); assert(response2.headers['cache-control'] === undefined); }); it('should not set a cache-control value when retrieving a single piece, when "etags" cache option is set', async function() { apos.thing.options.cache = { api: { maxAge: 5555, etags: true } }; const response = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true }); assert(response.headers['cache-control'] === undefined); }); it('should not set a cache-control value when retrieving pieces, when "api" cache option is not set', async function() { apos.thing.options.cache = { page: { maxAge: 5555 } }; const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true }); const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true }); assert(response1.headers['cache-control'] === undefined); assert(response2.headers['cache-control'] === undefined); delete apos.thing.options.cache; }); it('should set a "max-age" cache-control value when retrieving pieces, when "api" cache option is set', async function() { apos.thing.options.cache = { api: { maxAge: 3333 } }; const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true }); const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true }); assert(response1.headers['cache-control'] === 'max-age=3333'); assert(response2.headers['cache-control'] === 'max-age=3333'); delete apos.thing.options.cache; }); it('should set a "no-store" cache-control value when retrieving pieces, when user is connected', async function() { await apos.http.post('/api/v1/@apostrophecms/login/login', { body: { username: 'admin', password: 'admin', session: true }, jar }); const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true, jar }); const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true, jar });