2023-01-25 19:13:48 +01:00
|
|
|
'use strict';
|
|
|
|
|
2023-08-09 10:44:43 +02:00
|
|
|
const { omit } = require('lodash/fp');
|
2023-04-18 09:26:13 +01:00
|
|
|
const { mapAsync } = require('@strapi/utils');
|
|
|
|
|
2023-04-11 14:15:43 +01:00
|
|
|
const { createStrapiInstance } = require('api-tests/strapi');
|
|
|
|
const { createAuthRequest, createRequest } = require('api-tests/request');
|
|
|
|
const { createTestBuilder } = require('api-tests/builder');
|
2023-08-23 14:41:47 +01:00
|
|
|
const { describeOnCondition, createUtils } = require('api-tests/utils');
|
2023-03-08 16:12:53 +00:00
|
|
|
|
2023-03-23 12:48:17 +00:00
|
|
|
const {
|
|
|
|
STAGE_MODEL_UID,
|
|
|
|
WORKFLOW_MODEL_UID,
|
|
|
|
ENTITY_STAGE_ATTRIBUTE,
|
2023-06-07 10:47:27 +02:00
|
|
|
ENTITY_ASSIGNEE_ATTRIBUTE,
|
2023-04-11 14:15:43 +01:00
|
|
|
} = require('../../../../packages/core/admin/ee/server/constants/workflows');
|
|
|
|
|
2023-01-25 19:13:48 +01:00
|
|
|
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
|
|
|
|
2023-03-08 16:12:53 +00:00
|
|
|
const productUID = 'api::product.product';
|
|
|
|
const model = {
|
2023-09-11 10:49:21 +01:00
|
|
|
draftAndPublish: false,
|
2023-03-08 16:12:53 +00:00
|
|
|
pluginOptions: {},
|
|
|
|
singularName: 'product',
|
|
|
|
pluralName: 'products',
|
|
|
|
displayName: 'Product',
|
|
|
|
kind: 'collectionType',
|
|
|
|
attributes: {
|
|
|
|
name: {
|
|
|
|
type: 'string',
|
|
|
|
},
|
|
|
|
},
|
2023-05-19 09:15:19 +02:00
|
|
|
options: {
|
|
|
|
reviewWorkflows: true,
|
2023-05-22 11:09:08 +02:00
|
|
|
},
|
2023-03-08 16:12:53 +00:00
|
|
|
};
|
|
|
|
|
2023-01-26 12:17:36 +01:00
|
|
|
describeOnCondition(edition === 'EE')('Review workflows', () => {
|
2023-03-08 16:12:53 +00:00
|
|
|
const builder = createTestBuilder();
|
|
|
|
|
2023-01-25 19:13:48 +01:00
|
|
|
const requests = {
|
|
|
|
public: null,
|
|
|
|
admin: null,
|
|
|
|
};
|
2023-01-25 19:13:48 +01:00
|
|
|
let strapi;
|
|
|
|
let hasRW;
|
2023-01-26 12:17:36 +01:00
|
|
|
let defaultStage;
|
2023-01-31 15:30:19 +01:00
|
|
|
let secondStage;
|
2023-01-31 20:16:53 +01:00
|
|
|
let testWorkflow;
|
2023-05-12 17:39:03 +02:00
|
|
|
let createdWorkflow;
|
2023-01-25 19:13:48 +01:00
|
|
|
|
2023-03-08 16:12:53 +00:00
|
|
|
const createEntry = async (uid, data) => {
|
|
|
|
const { body } = await requests.admin({
|
|
|
|
method: 'POST',
|
|
|
|
url: `/content-manager/collection-types/${uid}`,
|
|
|
|
body: data,
|
|
|
|
});
|
|
|
|
return body;
|
|
|
|
};
|
|
|
|
|
2023-04-25 10:22:35 +02:00
|
|
|
const updateEntry = async (uid, id, data) => {
|
|
|
|
const { body } = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/content-manager/collection-types/${uid}/${id}`,
|
|
|
|
body: data,
|
|
|
|
});
|
|
|
|
return body;
|
|
|
|
};
|
|
|
|
|
2023-03-29 15:33:10 +01:00
|
|
|
const findAll = async (uid) => {
|
|
|
|
const { body } = await requests.admin({
|
|
|
|
method: 'GET',
|
|
|
|
url: `/content-manager/collection-types/${uid}`,
|
|
|
|
});
|
|
|
|
return body;
|
|
|
|
};
|
|
|
|
|
2023-09-08 13:39:58 +01:00
|
|
|
/**
|
|
|
|
* Create a full access token to authenticate the content API with
|
|
|
|
*/
|
|
|
|
const getFullAccessToken = async () => {
|
|
|
|
const res = await requests.admin.post('/admin/api-tokens', {
|
|
|
|
body: {
|
|
|
|
lifespan: null,
|
|
|
|
description: '',
|
|
|
|
type: 'full-access',
|
|
|
|
name: 'Full Access',
|
|
|
|
permissions: null,
|
|
|
|
},
|
2023-03-08 16:12:53 +00:00
|
|
|
});
|
|
|
|
|
2023-09-08 13:39:58 +01:00
|
|
|
return res.body.data.accessKey;
|
2023-03-08 16:12:53 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
await builder.addContentTypes([model]).build();
|
2023-01-25 19:13:48 +01:00
|
|
|
// eslint-disable-next-line node/no-extraneous-require
|
2023-09-18 10:24:32 +02:00
|
|
|
hasRW = require('@strapi/strapi/dist/utils/ee').default.features.isEnabled('review-workflows');
|
2023-01-25 19:13:48 +01:00
|
|
|
|
2023-09-08 13:39:58 +01:00
|
|
|
strapi = await createStrapiInstance({ bypassAuth: false });
|
2023-01-25 19:13:48 +01:00
|
|
|
requests.admin = await createAuthRequest({ strapi });
|
2023-09-08 13:39:58 +01:00
|
|
|
requests.public = createRequest({ strapi }).setToken(await getFullAccessToken());
|
2023-01-25 19:13:48 +01:00
|
|
|
|
2023-01-26 12:17:36 +01:00
|
|
|
defaultStage = await strapi.query(STAGE_MODEL_UID).create({
|
|
|
|
data: { name: 'Stage' },
|
|
|
|
});
|
2023-01-31 15:30:19 +01:00
|
|
|
secondStage = await strapi.query(STAGE_MODEL_UID).create({
|
|
|
|
data: { name: 'Stage 2' },
|
|
|
|
});
|
2023-01-31 20:16:53 +01:00
|
|
|
testWorkflow = await strapi.query(WORKFLOW_MODEL_UID).create({
|
2023-01-26 12:17:36 +01:00
|
|
|
data: {
|
2023-07-17 12:02:29 +02:00
|
|
|
contentTypes: [],
|
2023-05-16 13:54:27 +02:00
|
|
|
name: 'workflow',
|
2023-01-31 15:30:19 +01:00
|
|
|
stages: [defaultStage.id, secondStage.id],
|
2023-01-26 12:17:36 +01:00
|
|
|
},
|
|
|
|
});
|
2023-01-25 19:13:48 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
await strapi.destroy();
|
2023-03-08 16:12:53 +00:00
|
|
|
await builder.cleanup();
|
2023-01-25 19:13:48 +01:00
|
|
|
});
|
|
|
|
|
2023-03-21 14:59:20 +01:00
|
|
|
beforeEach(async () => {
|
|
|
|
testWorkflow = await strapi.query(WORKFLOW_MODEL_UID).update({
|
|
|
|
where: { id: testWorkflow.id },
|
|
|
|
data: {
|
|
|
|
uid: 'workflow',
|
2023-07-19 17:16:34 +02:00
|
|
|
stages: { set: [defaultStage.id, secondStage.id] },
|
2023-03-21 14:59:20 +01:00
|
|
|
},
|
|
|
|
});
|
2023-06-23 14:26:49 +02:00
|
|
|
defaultStage = await strapi.query(STAGE_MODEL_UID).update({
|
|
|
|
where: { id: defaultStage.id },
|
|
|
|
data: { name: 'Stage' },
|
|
|
|
});
|
|
|
|
secondStage = await strapi.query(STAGE_MODEL_UID).update({
|
|
|
|
where: { id: secondStage.id },
|
|
|
|
data: { name: 'Stage 2' },
|
|
|
|
});
|
2023-03-21 14:59:20 +01:00
|
|
|
});
|
|
|
|
|
2023-01-25 19:13:48 +01:00
|
|
|
describe('Get workflows', () => {
|
2023-01-27 15:11:43 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
2023-01-27 11:54:12 +01:00
|
|
|
const res = await requests.public.get('/admin/review-workflows/workflows');
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(Array.isArray(res.body)).toBeFalsy();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
|
|
|
const res = await requests.admin.get('/admin/review-workflows/workflows');
|
2023-01-25 19:13:48 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
2023-01-30 12:26:06 +01:00
|
|
|
expect(Array.isArray(res.body.data)).toBeTruthy();
|
2023-01-31 20:16:53 +01:00
|
|
|
// Why 2 workflows ? One added by the test, the other one should be the default workflow added in bootstrap
|
|
|
|
expect(res.body.data).toHaveLength(2);
|
2023-01-25 19:13:48 +01:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(Array.isArray(res.body)).toBeFalsy();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Get one workflow', () => {
|
2023-01-27 15:11:43 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
2023-01-31 20:16:53 +01:00
|
|
|
const res = await requests.public.get(`/admin/review-workflows/workflows/${testWorkflow.id}`);
|
2023-01-27 11:54:12 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
2023-01-31 20:16:53 +01:00
|
|
|
const res = await requests.admin.get(`/admin/review-workflows/workflows/${testWorkflow.id}`);
|
2023-01-25 19:13:48 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
2023-01-25 19:13:48 +01:00
|
|
|
expect(res.body.data).toBeInstanceOf(Object);
|
2023-01-31 20:16:53 +01:00
|
|
|
expect(res.body.data).toEqual(testWorkflow);
|
2023-07-04 17:26:33 +02:00
|
|
|
expect(typeof res.body.meta.workflowCount).toBe('number');
|
2023-01-25 19:13:48 +01:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
2023-01-25 19:13:48 +01:00
|
|
|
expect(res.body.data).toBeUndefined();
|
2023-01-25 19:13:48 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2023-01-26 12:17:36 +01:00
|
|
|
|
2023-05-12 15:33:03 +02:00
|
|
|
describe('Create workflow', () => {
|
2023-05-17 10:43:23 +02:00
|
|
|
test('It should create a workflow without stages', async () => {
|
2023-05-12 15:33:03 +02:00
|
|
|
const res = await requests.admin.post('/admin/review-workflows/workflows', {
|
|
|
|
body: {
|
2023-05-22 11:09:08 +02:00
|
|
|
data: {
|
|
|
|
name: 'testWorkflow',
|
|
|
|
stages: [],
|
|
|
|
},
|
2023-05-12 15:33:03 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (hasRW) {
|
2023-05-12 17:00:53 +02:00
|
|
|
expect(res.status).toBe(400);
|
|
|
|
expect(res.body.error.message).toBe('Can not create a workflow without stages');
|
2023-05-12 15:33:03 +02:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
2023-05-17 10:43:23 +02:00
|
|
|
test('It should create a workflow with stages', async () => {
|
2023-05-12 15:33:03 +02:00
|
|
|
const res = await requests.admin.post('/admin/review-workflows/workflows?populate=stages', {
|
|
|
|
body: {
|
2023-05-22 11:09:08 +02:00
|
|
|
data: {
|
|
|
|
name: 'createdWorkflow',
|
2023-09-07 12:36:54 +02:00
|
|
|
stages: [
|
|
|
|
{ name: 'Stage 1', color: '#343434' },
|
|
|
|
{ name: 'Stage 2', color: '#141414' },
|
|
|
|
],
|
2023-05-22 11:09:08 +02:00
|
|
|
},
|
2023-05-12 15:33:03 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
expect(res.body.data).toMatchObject({
|
2023-05-12 17:39:03 +02:00
|
|
|
name: 'createdWorkflow',
|
2023-09-07 12:36:54 +02:00
|
|
|
stages: [
|
|
|
|
{ name: 'Stage 1', color: '#343434' },
|
|
|
|
{ name: 'Stage 2', color: '#141414' },
|
|
|
|
],
|
2023-05-12 15:33:03 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
2023-05-12 17:39:03 +02:00
|
|
|
|
|
|
|
createdWorkflow = res.body.data;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Update workflow', () => {
|
2023-05-17 10:43:23 +02:00
|
|
|
test('It should update a workflow', async () => {
|
2023-05-12 17:39:03 +02:00
|
|
|
const res = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${createdWorkflow.id}`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { name: 'updatedWorkflow' } } }
|
2023-05-12 17:39:03 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
expect(res.body.data).toMatchObject({ name: 'updatedWorkflow' });
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-05-17 10:43:23 +02:00
|
|
|
test('It should update a workflow with stages', async () => {
|
2023-05-12 17:39:03 +02:00
|
|
|
const res = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${createdWorkflow.id}?populate=stages`,
|
|
|
|
{
|
|
|
|
body: {
|
2023-05-22 11:09:08 +02:00
|
|
|
data: {
|
|
|
|
name: 'updatedWorkflow',
|
|
|
|
stages: [
|
|
|
|
{ id: createdWorkflow.stages[0].id, name: 'Stage 1_Updated' },
|
|
|
|
{ name: 'Stage 2' },
|
|
|
|
],
|
|
|
|
},
|
2023-05-12 17:39:03 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
expect(res.body.data).toMatchObject({
|
|
|
|
name: 'updatedWorkflow',
|
|
|
|
stages: [
|
|
|
|
{ id: createdWorkflow.stages[0].id, name: 'Stage 1_Updated' },
|
|
|
|
{ name: 'Stage 2' },
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
2023-05-12 15:33:03 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-05-17 10:43:23 +02:00
|
|
|
describe('Delete workflow', () => {
|
|
|
|
test('It should delete a workflow', async () => {
|
|
|
|
const createdRes = await requests.admin.post('/admin/review-workflows/workflows', {
|
2023-05-22 11:09:08 +02:00
|
|
|
body: { data: { name: 'testWorkflow', stages: [{ name: 'Stage 1' }] } },
|
2023-05-17 10:43:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const res = await requests.admin.delete(
|
|
|
|
`/admin/review-workflows/workflows/${createdRes.body.data.id}`
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
expect(res.body.data).toMatchObject({ name: 'testWorkflow' });
|
|
|
|
});
|
|
|
|
test("It shouldn't delete a workflow that does not exist", async () => {
|
|
|
|
const res = await requests.admin.delete(`/admin/review-workflows/workflows/123456789`);
|
|
|
|
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeNull();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-01-26 12:17:36 +01:00
|
|
|
describe('Get workflow stages', () => {
|
2023-01-31 12:26:58 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
2023-01-31 20:16:53 +01:00
|
|
|
const res = await requests.public.get(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=stages`
|
|
|
|
);
|
2023-01-31 12:26:58 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
2023-01-31 20:16:53 +01:00
|
|
|
const res = await requests.admin.get(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=stages`
|
|
|
|
);
|
2023-01-26 12:17:36 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
2023-01-31 20:16:53 +01:00
|
|
|
expect(res.body.data).toBeInstanceOf(Object);
|
|
|
|
expect(res.body.data.stages).toBeInstanceOf(Array);
|
|
|
|
expect(res.body.data.stages).toHaveLength(2);
|
2023-01-26 12:17:36 +01:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(Array.isArray(res.body)).toBeFalsy();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Get stages', () => {
|
2023-01-31 12:26:58 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
|
|
|
const res = await requests.public.get(
|
2023-01-31 20:16:53 +01:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}/stages`
|
2023-01-31 12:26:58 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
|
|
|
const res = await requests.admin.get(
|
2023-01-31 20:16:53 +01:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}/stages`
|
2023-01-31 12:26:58 +01:00
|
|
|
);
|
2023-01-26 12:17:36 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
2023-01-31 12:26:58 +01:00
|
|
|
expect(Array.isArray(res.body.data)).toBeTruthy();
|
2023-01-31 15:30:19 +01:00
|
|
|
expect(res.body.data).toHaveLength(2);
|
2023-01-26 12:17:36 +01:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(Array.isArray(res.body)).toBeFalsy();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Get stage by id', () => {
|
2023-01-31 12:26:58 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
|
|
|
const res = await requests.public.get(
|
2023-01-31 20:16:53 +01:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}/stages/${secondStage.id}`
|
2023-01-31 12:26:58 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
|
|
|
const res = await requests.admin.get(
|
2023-01-31 20:16:53 +01:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}/stages/${secondStage.id}`
|
2023-01-26 12:17:36 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
expect(res.body.data).toBeInstanceOf(Object);
|
2023-01-31 15:30:19 +01:00
|
|
|
expect(res.body.data).toEqual(secondStage);
|
2023-01-26 12:17:36 +01:00
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2023-02-03 16:47:34 +01:00
|
|
|
|
|
|
|
describe('Replace stages of a workflow', () => {
|
|
|
|
let stagesUpdateData;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
stagesUpdateData = [
|
|
|
|
defaultStage,
|
|
|
|
{ id: secondStage.id, name: 'new_name' },
|
|
|
|
{ name: 'new stage' },
|
|
|
|
];
|
|
|
|
});
|
|
|
|
|
2023-04-24 13:28:44 +02:00
|
|
|
test("It should assign a default color to stages if they don't have one", async () => {
|
2023-05-12 17:39:03 +02:00
|
|
|
const workflowRes = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
|
|
|
{
|
|
|
|
body: {
|
2023-05-22 11:09:08 +02:00
|
|
|
data: {
|
|
|
|
stages: [
|
|
|
|
defaultStage,
|
|
|
|
{ id: secondStage.id, name: secondStage.name, color: '#000000' },
|
|
|
|
],
|
|
|
|
},
|
2023-05-12 17:39:03 +02:00
|
|
|
},
|
|
|
|
}
|
2023-04-24 13:28:44 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
expect(workflowRes.status).toBe(200);
|
|
|
|
expect(workflowRes.body.data.stages[0].color).toBe('#4945FF');
|
|
|
|
expect(workflowRes.body.data.stages[1].color).toBe('#000000');
|
|
|
|
});
|
2023-02-03 16:47:34 +01:00
|
|
|
test("It shouldn't be available for public", async () => {
|
2023-02-07 14:49:29 +01:00
|
|
|
const workflowRes = await requests.public.get(
|
2023-05-12 17:39:03 +02:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`
|
2023-02-07 14:49:29 +01:00
|
|
|
);
|
2023-02-03 16:47:34 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
2023-02-07 14:49:29 +01:00
|
|
|
expect(workflowRes.status).toBe(401);
|
2023-02-03 16:47:34 +01:00
|
|
|
} else {
|
2023-02-07 14:49:29 +01:00
|
|
|
expect(workflowRes.status).toBe(404);
|
|
|
|
expect(workflowRes.body.data).toBeUndefined();
|
2023-02-03 16:47:34 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
test('It should be available for every connected users (admin)', async () => {
|
2023-05-12 17:39:03 +02:00
|
|
|
const workflowRes = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { stages: stagesUpdateData } } }
|
2023-02-07 14:49:29 +01:00
|
|
|
);
|
2023-02-03 16:47:34 +01:00
|
|
|
|
|
|
|
if (hasRW) {
|
2023-02-07 14:49:29 +01:00
|
|
|
expect(workflowRes.status).toBe(200);
|
|
|
|
expect(workflowRes.body.data).toBeInstanceOf(Object);
|
|
|
|
expect(workflowRes.body.data.stages).toBeInstanceOf(Array);
|
2023-08-09 10:44:43 +02:00
|
|
|
expect(workflowRes.body.data.stages[0]).toMatchObject(
|
|
|
|
omit(['updatedAt'], stagesUpdateData[0])
|
|
|
|
);
|
|
|
|
expect(workflowRes.body.data.stages[1]).toMatchObject(
|
|
|
|
omit(['updatedAt'], stagesUpdateData[1])
|
|
|
|
);
|
2023-02-07 14:49:29 +01:00
|
|
|
expect(workflowRes.body.data.stages[2]).toMatchObject({
|
|
|
|
id: expect.any(Number),
|
2023-08-09 10:44:43 +02:00
|
|
|
...omit(['updatedAt'], stagesUpdateData[2]),
|
2023-02-07 14:49:29 +01:00
|
|
|
});
|
2023-02-03 16:47:34 +01:00
|
|
|
} else {
|
2023-02-07 14:49:29 +01:00
|
|
|
expect(workflowRes.status).toBe(404);
|
|
|
|
expect(workflowRes.body.data).toBeUndefined();
|
2023-02-03 16:47:34 +01:00
|
|
|
}
|
|
|
|
});
|
2023-02-10 16:09:20 +01:00
|
|
|
test('It should throw an error if trying to delete all stages in a workflow', async () => {
|
2023-05-12 17:39:03 +02:00
|
|
|
const workflowRes = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { stages: [] } } }
|
2023-02-10 16:09:20 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
2023-05-12 17:39:03 +02:00
|
|
|
expect(workflowRes.status).toBe(400);
|
|
|
|
expect(workflowRes.body.error).toBeDefined();
|
|
|
|
expect(workflowRes.body.error.name).toEqual('ValidationError');
|
2023-02-10 16:09:20 +01:00
|
|
|
} else {
|
|
|
|
expect(workflowRes.status).toBe(404);
|
|
|
|
expect(workflowRes.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
2023-07-17 11:16:56 +02:00
|
|
|
test('It should throw an error if trying to create stages with duplicated names', async () => {
|
|
|
|
const stagesRes = await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
|
|
|
{
|
|
|
|
body: {
|
|
|
|
data: {
|
|
|
|
stages: [{ name: 'To Do' }, { name: 'To Do' }],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(stagesRes.status).toBe(400);
|
|
|
|
expect(stagesRes.body.error).toBeDefined();
|
|
|
|
expect(stagesRes.body.error.name).toEqual('ValidationError');
|
|
|
|
expect(stagesRes.body.error.message).toBeDefined();
|
|
|
|
}
|
|
|
|
});
|
2023-04-24 11:52:05 +02:00
|
|
|
test('It should throw an error if trying to create more than 200 stages', async () => {
|
|
|
|
const stagesRes = await requests.admin.put(
|
2023-05-12 17:39:03 +02:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { stages: Array(201).fill({ name: 'new stage' }) } } }
|
2023-04-24 11:52:05 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(stagesRes.status).toBe(400);
|
|
|
|
expect(stagesRes.body.error).toBeDefined();
|
|
|
|
expect(stagesRes.body.error.name).toEqual('ValidationError');
|
|
|
|
expect(stagesRes.body.error.message).toBeDefined();
|
|
|
|
}
|
|
|
|
});
|
2023-02-03 16:47:34 +01:00
|
|
|
});
|
2023-03-08 16:12:53 +00:00
|
|
|
|
2023-07-19 17:16:34 +02:00
|
|
|
describe('Update assignee on an entity', () => {
|
|
|
|
describe('Review Workflow is enabled', () => {
|
|
|
|
beforeAll(async () => {
|
|
|
|
// Assign Product to workflow so workflow is active on this CT
|
|
|
|
await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
|
|
|
{ body: { data: { contentTypes: [productUID] } } }
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('Should update the assignee on an entity', async () => {
|
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
const user = requests.admin.getLoggedUser();
|
2023-09-08 13:39:58 +01:00
|
|
|
|
2023-07-19 17:16:34 +02:00
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/assignee`,
|
|
|
|
body: {
|
|
|
|
data: { id: user.id },
|
|
|
|
},
|
|
|
|
});
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(200);
|
2023-07-19 17:16:34 +02:00
|
|
|
const assignee = response.body.data[ENTITY_ASSIGNEE_ATTRIBUTE];
|
|
|
|
expect(assignee.id).toEqual(user.id);
|
|
|
|
expect(assignee).not.toHaveProperty('password');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('Should throw an error if user does not exist', async () => {
|
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
|
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/assignee`,
|
|
|
|
body: {
|
|
|
|
data: { id: 1234 },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(400);
|
2023-07-19 17:16:34 +02:00
|
|
|
expect(response.body.error).toBeDefined();
|
|
|
|
expect(response.body.error.name).toEqual('ApplicationError');
|
|
|
|
expect(response.body.error.message).toEqual('Selected user does not exist');
|
|
|
|
});
|
2023-09-08 13:39:58 +01:00
|
|
|
|
|
|
|
test('Correctly sanitize private fields of assignees in the content API', async () => {
|
|
|
|
const assigneeAttribute = 'strapi_assignee';
|
|
|
|
|
|
|
|
const { status, body } = await requests.public.get(`/api/${model.pluralName}`, {
|
|
|
|
qs: { populate: assigneeAttribute },
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
2023-09-11 10:49:21 +01:00
|
|
|
expect(body.data.length).toBeGreaterThan(0);
|
2023-09-08 13:39:58 +01:00
|
|
|
|
|
|
|
const privateUserFields = [
|
|
|
|
'password',
|
|
|
|
'email',
|
|
|
|
'resetPasswordToken',
|
|
|
|
'registrationToken',
|
|
|
|
'isActive',
|
|
|
|
'roles',
|
|
|
|
'blocked',
|
|
|
|
];
|
|
|
|
|
|
|
|
// Assert that every assignee returned is sanitized correctly
|
|
|
|
body.data.forEach((item) => {
|
|
|
|
expect(item.attributes).toHaveProperty(assigneeAttribute);
|
|
|
|
privateUserFields.forEach((field) => {
|
|
|
|
expect(item.attributes[assigneeAttribute]).not.toHaveProperty(field);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-07-19 17:16:34 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('Review Workflow is disabled', () => {
|
|
|
|
beforeAll(async () => {
|
|
|
|
// Unassign Product to workflow so workflow is inactive on this CT
|
|
|
|
await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
|
|
|
{ body: { data: { contentTypes: [] } } }
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('Should not update the entity', async () => {
|
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
const user = requests.admin.getLoggedUser();
|
|
|
|
|
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/assignee`,
|
|
|
|
body: {
|
|
|
|
data: { id: user.id },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(400);
|
2023-07-19 17:16:34 +02:00
|
|
|
expect(response.body.error).toBeDefined();
|
|
|
|
expect(response.body.error.name).toBe('ApplicationError');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-04-24 11:52:05 +02:00
|
|
|
describe('Update a stage on an entity', () => {
|
2023-03-21 14:59:20 +01:00
|
|
|
describe('Review Workflow is enabled', () => {
|
|
|
|
beforeAll(async () => {
|
2023-08-23 14:41:47 +01:00
|
|
|
// Update workflow to assign content type
|
2023-07-03 11:50:03 +02:00
|
|
|
await requests.admin.put(
|
2023-05-16 13:54:27 +02:00
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { contentTypes: [productUID] } } }
|
2023-05-16 13:54:27 +02:00
|
|
|
);
|
2023-03-21 14:59:20 +01:00
|
|
|
});
|
2023-05-16 13:54:27 +02:00
|
|
|
|
2023-03-21 14:59:20 +01:00
|
|
|
test('Should update the accordingly on an entity', async () => {
|
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
|
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
|
|
|
|
body: {
|
|
|
|
data: { id: secondStage.id },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(200);
|
2023-04-12 14:18:40 +01:00
|
|
|
expect(response.body.data[ENTITY_STAGE_ATTRIBUTE]).toEqual(
|
2023-03-21 14:59:20 +01:00
|
|
|
expect.objectContaining({ id: secondStage.id })
|
|
|
|
);
|
|
|
|
});
|
2023-05-16 13:54:27 +02:00
|
|
|
test('Should throw an error if stage does not belong to the workflow', async () => {
|
2023-04-11 17:28:17 +02:00
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
|
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
|
|
|
|
body: {
|
|
|
|
data: { id: 1234 },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(400);
|
2023-04-11 17:28:17 +02:00
|
|
|
expect(response.body.error).toBeDefined();
|
|
|
|
expect(response.body.error.name).toEqual('ApplicationError');
|
2023-05-16 13:54:27 +02:00
|
|
|
expect(response.body.error.message).toEqual('Stage does not belong to workflow "workflow"');
|
2023-04-11 17:28:17 +02:00
|
|
|
});
|
2023-09-11 10:49:21 +01:00
|
|
|
|
|
|
|
test('Should return entity stage information to the content API', async () => {
|
|
|
|
const stageAttribute = 'strapi_stage';
|
|
|
|
|
|
|
|
const { status, body } = await requests.public.get(`/api/${model.pluralName}`, {
|
|
|
|
qs: { populate: stageAttribute },
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
expect(body.data.length).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
body.data.forEach((item) => {
|
|
|
|
expect(item.attributes).toHaveProperty(stageAttribute);
|
|
|
|
expect(item.attributes[stageAttribute]).not.toBeNull();
|
|
|
|
expect(item.attributes[stageAttribute].data.attributes).toHaveProperty('name');
|
|
|
|
});
|
|
|
|
});
|
2023-03-21 14:59:20 +01:00
|
|
|
});
|
2023-09-11 10:49:21 +01:00
|
|
|
|
2023-03-21 14:59:20 +01:00
|
|
|
describe('Review Workflow is disabled', () => {
|
|
|
|
beforeAll(async () => {
|
2023-05-16 13:54:27 +02:00
|
|
|
// Update workflow to unassign content type
|
|
|
|
await requests.admin.put(
|
|
|
|
`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`,
|
2023-05-22 11:09:08 +02:00
|
|
|
{ body: { data: { contentTypes: [] } } }
|
2023-05-16 13:54:27 +02:00
|
|
|
);
|
2023-03-21 14:59:20 +01:00
|
|
|
});
|
|
|
|
test('Should not update the entity', async () => {
|
|
|
|
const entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
|
|
|
|
const response = await requests.admin({
|
|
|
|
method: 'PUT',
|
|
|
|
url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
|
|
|
|
body: {
|
|
|
|
data: { id: secondStage.id },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(response.status).toBe(400);
|
2023-03-21 14:59:20 +01:00
|
|
|
expect(response.body.error).toBeDefined();
|
|
|
|
expect(response.body.error.name).toBe('ApplicationError');
|
|
|
|
});
|
2023-03-08 16:12:53 +00:00
|
|
|
});
|
|
|
|
});
|
2023-03-22 11:55:22 +00:00
|
|
|
|
2023-08-23 14:41:47 +01:00
|
|
|
describe('Listing available stages for transition', () => {
|
|
|
|
const endpoint = (id) => `/admin/content-manager/collection-types/${productUID}/${id}/stages`;
|
|
|
|
|
|
|
|
let utils;
|
|
|
|
let entry;
|
|
|
|
let restrictedRequest;
|
2023-08-31 11:36:25 +01:00
|
|
|
let restrictedUser;
|
2023-08-23 14:41:47 +01:00
|
|
|
let restrictedRole;
|
|
|
|
|
2023-08-31 11:36:25 +01:00
|
|
|
const deleteFixtures = async () => {
|
|
|
|
await utils.deleteUserById(restrictedUser.id);
|
|
|
|
await utils.deleteRolesById([restrictedRole.id]);
|
|
|
|
};
|
|
|
|
|
2023-08-23 14:41:47 +01:00
|
|
|
beforeAll(async () => {
|
|
|
|
// Update workflow to assign content type
|
|
|
|
await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`, {
|
|
|
|
body: { data: { contentTypes: [productUID] } },
|
|
|
|
});
|
|
|
|
|
|
|
|
entry = await createEntry(productUID, { name: 'Product' });
|
|
|
|
|
|
|
|
utils = createUtils(strapi);
|
|
|
|
const role = await utils.createRole({
|
|
|
|
name: 'restricted-role',
|
|
|
|
description: '',
|
|
|
|
});
|
2023-08-31 11:36:25 +01:00
|
|
|
restrictedRole = role;
|
2023-08-23 14:41:47 +01:00
|
|
|
|
2023-08-31 11:36:25 +01:00
|
|
|
const restrictedUserInfo = {
|
|
|
|
email: 'restricted@user.io',
|
|
|
|
password: 'Restricted123',
|
|
|
|
};
|
|
|
|
|
|
|
|
restrictedUser = await utils.createUserIfNotExists({
|
|
|
|
...restrictedUserInfo,
|
2023-08-23 14:41:47 +01:00
|
|
|
roles: [role.id],
|
|
|
|
});
|
|
|
|
|
2023-08-31 11:36:25 +01:00
|
|
|
restrictedRequest = await createAuthRequest({ strapi, userInfo: restrictedUserInfo });
|
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
await deleteFixtures();
|
2023-08-23 14:41:47 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
test("It shouldn't be available for public", async () => {
|
|
|
|
const res = await requests.public.get(endpoint(entry.id));
|
|
|
|
|
|
|
|
if (hasRW) {
|
|
|
|
expect(res.status).toBe(401);
|
|
|
|
} else {
|
|
|
|
expect(res.status).toBe(404);
|
|
|
|
expect(res.body.data).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
test('It should return available stages for an admin user', async () => {
|
|
|
|
const res = await requests.admin.get(endpoint(entry.id));
|
|
|
|
|
|
|
|
expect(res.body.data).toHaveLength(1);
|
|
|
|
expect(res.body.data[0]).toMatchObject(secondStage);
|
|
|
|
});
|
|
|
|
|
2023-09-06 11:23:12 +01:00
|
|
|
test('It should be forbidden when the user cannot read the content type', async () => {
|
2023-08-23 14:41:47 +01:00
|
|
|
const res = await restrictedRequest.get(endpoint(entry.id));
|
|
|
|
|
2023-09-06 11:35:31 +01:00
|
|
|
expect(res.status).toBe(403);
|
2023-08-23 14:41:47 +01:00
|
|
|
});
|
|
|
|
|
2023-08-23 15:01:06 +01:00
|
|
|
test('It should return an empty list when a user does not have the permission to transition the current stage', async () => {
|
2023-08-23 14:41:47 +01:00
|
|
|
const permission = {
|
2023-09-06 11:23:12 +01:00
|
|
|
action: 'plugin::content-manager.explorer.read',
|
|
|
|
subject: productUID,
|
2023-08-23 14:41:47 +01:00
|
|
|
fields: null,
|
|
|
|
conditions: [],
|
|
|
|
};
|
|
|
|
await utils.assignPermissionsToRole(restrictedRole.id, [permission]);
|
|
|
|
|
|
|
|
const res = await restrictedRequest.get(endpoint(entry.id));
|
|
|
|
|
|
|
|
expect(res.body.data).toHaveLength(0);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-05-16 13:54:27 +02:00
|
|
|
describe('Deleting a stage when content already exists', () => {
|
2023-03-22 11:55:22 +00:00
|
|
|
beforeAll(async () => {
|
2023-08-23 14:41:47 +01:00
|
|
|
// Update workflow to assign content type
|
2023-05-16 13:54:27 +02:00
|
|
|
await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`, {
|
2023-05-22 11:09:08 +02:00
|
|
|
body: { data: { contentTypes: [productUID] } },
|
2023-03-22 11:55:22 +00:00
|
|
|
});
|
|
|
|
});
|
2023-03-29 15:33:10 +01:00
|
|
|
|
|
|
|
test('When content exists in a review stage and this stage is deleted, the content should be moved to the nearest available stage', async () => {
|
2023-04-25 10:22:35 +02:00
|
|
|
const products = await findAll(productUID);
|
|
|
|
|
2023-04-25 16:21:13 +02:00
|
|
|
// Move half of the entries to the last stage,
|
|
|
|
// and the other half to the first stage
|
2023-04-25 10:22:35 +02:00
|
|
|
await mapAsync(products.results, async (entity) =>
|
|
|
|
updateEntry(productUID, entity.id, {
|
|
|
|
[ENTITY_STAGE_ATTRIBUTE]: entity.id % 2 ? defaultStage.id : secondStage.id,
|
|
|
|
})
|
2023-04-18 09:26:13 +01:00
|
|
|
);
|
2023-03-29 15:33:10 +01:00
|
|
|
|
2023-04-25 10:22:35 +02:00
|
|
|
// Delete last stage stage of the default workflow
|
2023-05-12 17:39:03 +02:00
|
|
|
await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`, {
|
2023-05-22 11:09:08 +02:00
|
|
|
body: { data: { stages: [defaultStage] } },
|
2023-03-29 15:33:10 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
// Expect the content in these stages to be moved to the nearest available stage
|
|
|
|
const productsAfter = await findAll(productUID);
|
2023-04-25 10:22:35 +02:00
|
|
|
for (const entry of productsAfter.results) {
|
|
|
|
expect(entry[ENTITY_STAGE_ATTRIBUTE].name).toEqual(defaultStage.name);
|
|
|
|
}
|
2023-03-29 15:33:10 +01:00
|
|
|
});
|
|
|
|
});
|
2023-01-25 19:13:48 +01:00
|
|
|
});
|