0
Fork 0
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:
Naz 2023-06-06 22:04:02 +07:00
parent c8b713a679
commit fab5b1845c
No known key found for this signature in database
9 changed files with 150 additions and 10 deletions

View file

@ -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 || []
});
}

View file

@ -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);
}

View file

@ -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);
});
});

View file

@ -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({});

View file

@ -13,7 +13,8 @@ module.exports = {
options: [
'limit',
'order',
'page'
'page',
'filter'
],
// @NOTE: should have permissions when moving out of Alpha
permissions: false,

View file

@ -0,0 +1,8 @@
module.exports = [{
title: 'Featured Posts',
slug: 'featured',
description: 'Collection of featured posts',
type: 'automatic',
deletable: false,
filter: 'featured:true'
}];

View file

@ -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);
});
}
}

View file

@ -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 [

View file

@ -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 = {