mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Made builtin collections un-deletable
closes https://github.com/TryGhost/Team/issues/3376 - It should not be possible to delete a built-in collection.
This commit is contained in:
parent
c8b713a679
commit
fab5b1845c
9 changed files with 150 additions and 10 deletions
|
@ -22,12 +22,24 @@ export class Collection {
|
|||
featureImage: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deleted: boolean;
|
||||
deletable: boolean;
|
||||
_deleted: boolean = false;
|
||||
|
||||
_posts: string[];
|
||||
get posts() {
|
||||
return this._posts;
|
||||
}
|
||||
|
||||
public get deleted() {
|
||||
return this._deleted;
|
||||
}
|
||||
|
||||
public set deleted(value: boolean) {
|
||||
if (this.deletable) {
|
||||
this._deleted = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param post {{id: string}} - The post to add to the collection
|
||||
* @param index {number} - The index to insert the post at, use negative numbers to count from the end.
|
||||
|
@ -69,6 +81,7 @@ export class Collection {
|
|||
this.featureImage = data.featureImage;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
this.deletable = data.deletable;
|
||||
this.deleted = data.deleted;
|
||||
this._posts = data.posts;
|
||||
}
|
||||
|
@ -135,6 +148,7 @@ export class Collection {
|
|||
createdAt: Collection.validateDateField(data.created_at, 'created_at'),
|
||||
updatedAt: Collection.validateDateField(data.updated_at, 'updated_at'),
|
||||
deleted: data.deleted || false,
|
||||
deletable: (data.deletable !== false),
|
||||
posts: data.posts || []
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
import {Collection} from './Collection';
|
||||
import {CollectionRepository} from './CollectionRepository';
|
||||
import tpl from '@tryghost/tpl';
|
||||
import {MethodNotAllowedError} from '@tryghost/errors';
|
||||
|
||||
const messages = {
|
||||
cannotDeleteBuiltInCollectionError: {
|
||||
message: 'Cannot delete builtin collection',
|
||||
context: 'The collection {id} is a builtin collection and cannot be deleted'
|
||||
}
|
||||
};
|
||||
|
||||
type CollectionsServiceDeps = {
|
||||
collectionsRepository: CollectionRepository;
|
||||
|
@ -18,6 +27,7 @@ type ManualCollection = {
|
|||
description?: string;
|
||||
feature_image?: string;
|
||||
filter?: null;
|
||||
deletable?: boolean;
|
||||
};
|
||||
|
||||
type AutomaticCollection = {
|
||||
|
@ -27,6 +37,7 @@ type AutomaticCollection = {
|
|||
slug?: string;
|
||||
description?: string;
|
||||
feature_image?: string;
|
||||
deletable?: boolean;
|
||||
};
|
||||
|
||||
type CollectionInputDTO = ManualCollection | AutomaticCollection;
|
||||
|
@ -107,7 +118,8 @@ export class CollectionsService {
|
|||
description: data.description,
|
||||
type: data.type,
|
||||
filter: data.filter,
|
||||
featureImage: data.feature_image
|
||||
featureImage: data.feature_image,
|
||||
deletable: data.deletable
|
||||
});
|
||||
|
||||
if (collection.type === 'automatic' && collection.filter) {
|
||||
|
@ -218,6 +230,15 @@ export class CollectionsService {
|
|||
const collection = await this.getById(id);
|
||||
|
||||
if (collection) {
|
||||
if (collection.deletable === false) {
|
||||
throw new MethodNotAllowedError({
|
||||
message: tpl(messages.cannotDeleteBuiltInCollectionError.message),
|
||||
context: tpl(messages.cannotDeleteBuiltInCollectionError.context, {
|
||||
id: collection.id
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
collection.deleted = true;
|
||||
await this.collectionsRepository.save(collection);
|
||||
}
|
||||
|
|
|
@ -169,4 +169,30 @@ describe('Collection', function () {
|
|||
|
||||
assert.equal(collection.posts.length, 0);
|
||||
});
|
||||
|
||||
it('Cannot set non deletable collection to deleted', async function () {
|
||||
const collection = await Collection.create({
|
||||
title: 'Testing adding posts',
|
||||
deletable: false
|
||||
});
|
||||
|
||||
assert.equal(collection.deleted, false);
|
||||
|
||||
collection.deleted = true;
|
||||
|
||||
assert.equal(collection.deleted, false);
|
||||
});
|
||||
|
||||
it('Can set deletable collection to deleted', async function () {
|
||||
const collection = await Collection.create({
|
||||
title: 'Testing adding posts',
|
||||
deletable: true
|
||||
});
|
||||
|
||||
assert.equal(collection.deleted, false);
|
||||
|
||||
collection.deleted = true;
|
||||
|
||||
assert.equal(collection.deleted, true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -61,6 +61,25 @@ describe('CollectionsService', function () {
|
|||
assert.equal(deletedCollection, null, 'Collection should be deleted');
|
||||
});
|
||||
|
||||
it('Throws when built in collection is attempted to be deleted', async function () {
|
||||
const collection = await collectionsService.createCollection({
|
||||
title: 'Featured Posts',
|
||||
slug: 'featured',
|
||||
description: 'Collection of featured posts',
|
||||
type: 'automatic',
|
||||
deletable: false,
|
||||
filter: 'featured:true'
|
||||
});
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await collectionsService.destroy(collection.id);
|
||||
}, (err: any) => {
|
||||
assert.equal(err.message, 'Cannot delete builtin collection', 'Error message should match');
|
||||
assert.equal(err.context, `The collection ${collection.id} is a builtin collection and cannot be deleted`, 'Error context should match');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDTO', function () {
|
||||
it('Can map Collection entity to DTO object', async function () {
|
||||
const collection = await Collection.create({});
|
||||
|
|
|
@ -13,7 +13,8 @@ module.exports = {
|
|||
options: [
|
||||
'limit',
|
||||
'order',
|
||||
'page'
|
||||
'page',
|
||||
'filter'
|
||||
],
|
||||
// @NOTE: should have permissions when moving out of Alpha
|
||||
permissions: false,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = [{
|
||||
title: 'Featured Posts',
|
||||
slug: 'featured',
|
||||
description: 'Collection of featured posts',
|
||||
type: 'automatic',
|
||||
deletable: false,
|
||||
filter: 'featured:true'
|
||||
}];
|
|
@ -49,12 +49,8 @@ class CollectionsServiceWrapper {
|
|||
const featuredCollections = await this.api.browse({filter: 'slug:featured'});
|
||||
|
||||
if (!featuredCollections.data.length) {
|
||||
this.api.add({
|
||||
title: 'Featured Posts',
|
||||
slug: 'featured',
|
||||
description: 'Collection of featured posts',
|
||||
type: 'automatic',
|
||||
filter: 'featured:true'
|
||||
require('./built-in-collections').forEach((collection) => {
|
||||
this.api.add(collection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -446,6 +446,37 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Cannot delete a built in collection 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"code": null,
|
||||
"context": Any<String>,
|
||||
"details": null,
|
||||
"ghostErrorCode": null,
|
||||
"help": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
"message": "Method not allowed, cannot delete collection.",
|
||||
"property": null,
|
||||
"type": "MethodNotAllowedError",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Cannot delete a built in collection 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "355",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Edit Can add Posts and append Post to a Collection 1: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
|
|
|
@ -12,7 +12,8 @@ const {
|
|||
anyLocationFor,
|
||||
anyObjectId,
|
||||
anyISODateTime,
|
||||
anyNumber
|
||||
anyNumber,
|
||||
anyString
|
||||
} = matchers;
|
||||
|
||||
const matchCollection = {
|
||||
|
@ -333,6 +334,29 @@ describe('Collections API', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('Cannot delete a built in collection', async function () {
|
||||
const builtInCollection = await agent
|
||||
.get('/collections/?filter=slug:featured')
|
||||
.expectStatus(200);
|
||||
|
||||
assert.ok(builtInCollection.body.collections);
|
||||
assert.equal(builtInCollection.body.collections.length, 1);
|
||||
|
||||
await agent
|
||||
.delete(`/collections/${builtInCollection.body.collections[0].id}/`)
|
||||
.expectStatus(405)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
errors: [{
|
||||
id: anyErrorId,
|
||||
context: anyString
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Automatic Collection Filtering', function () {
|
||||
it('Creates an automatic Collection with a featured filter', async function () {
|
||||
const collection = {
|
||||
|
|
Loading…
Add table
Reference in a new issue