diff --git a/models/event.js b/models/event.js index 4ec6885f3..c3c501ebd 100644 --- a/models/event.js +++ b/models/event.js @@ -43,13 +43,7 @@ function validateObject(object) { return cleanObject; } -async function findAll() { - const results = await database.query('SELECT * FROM events;'); - return results.rows; -} - export default Object.freeze({ create, updateMetadata, - findAll, }); diff --git a/pages/api/v1/contents/[username]/[slug]/index.public.js b/pages/api/v1/contents/[username]/[slug]/index.public.js index 5d219c8d2..c3efde0d6 100644 --- a/pages/api/v1/contents/[username]/[slug]/index.public.js +++ b/pages/api/v1/contents/[username]/[slug]/index.public.js @@ -115,7 +115,7 @@ async function patchHandler(request, response) { }); } - if (!unfilteredBodyValues.parent_id) { + if (!contentToBeUpdated.parent_id) { if (!authorization.can(userTryingToPatch, 'create:content:text_root')) { throw new ForbiddenError({ message: 'Você não possui permissão para editar conteúdos na raiz do site.', @@ -147,7 +147,7 @@ async function patchHandler(request, response) { const currentEvent = await event.create( { - type: filteredBodyValues.parent_id ? 'update:content:text_child' : 'update:content:text_root', + type: contentToBeUpdated.parent_id ? 'update:content:text_child' : 'update:content:text_root', originatorUserId: request.context.user.id, originatorIp: request.context.clientIp, }, diff --git a/pages/api/v1/users/[username]/index.public.js b/pages/api/v1/users/[username]/index.public.js index 30b94e708..5df431a11 100644 --- a/pages/api/v1/users/[username]/index.public.js +++ b/pages/api/v1/users/[username]/index.public.js @@ -163,6 +163,13 @@ async function patchHandler(request, response) { for (const field of updatableFields) { if (originalUser[field] !== updatedUser[field]) { metadata.updatedFields.push(field); + + if (field === 'username') { + metadata.username = { + old: originalUser.username, + new: updatedUser.username, + }; + } } } diff --git a/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js b/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js index efb4c14cc..9ecfffad4 100644 --- a/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js +++ b/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js @@ -87,6 +87,174 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { }); }); + describe('User without "create:content:text_root" feature', () => { + test('"root" content with valid data', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const userWithoutFeature = await contentsRequestBuilder.buildUser({ without: ['create:content:text_root'] }); + + const rootContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + title: 'Root content title', + body: 'Root content body', + }); + + const { response, responseBody } = await contentsRequestBuilder.patch( + `/${userWithoutFeature.username}/${rootContent.slug}`, + { + title: 'Valid user trying to update "root" content.', + body: "He shouldn't be able to do it because he lacks the 'create:content:text_root' feature.", + }, + ); + + expect(response.status).toEqual(403); + expect(responseBody.status_code).toEqual(403); + expect(responseBody.name).toEqual('ForbiddenError'); + expect(responseBody.message).toEqual('Você não possui permissão para editar conteúdos na raiz do site.'); + expect(responseBody.action).toEqual('Verifique se você possui a feature "create:content:text_root".'); + expect(uuidVersion(responseBody.error_id)).toEqual(4); + expect(uuidVersion(responseBody.request_id)).toEqual(4); + expect(responseBody.error_location_code).toEqual( + 'CONTROLLER:CONTENT:PATCH_HANDLER:CREATE:CONTENT:TEXT_ROOT:FEATURE_NOT_FOUND', + ); + }); + + test('"child" content with valid data', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const userWithoutFeature = await contentsRequestBuilder.buildUser({ without: ['create:content:text_root'] }); + + const rootContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + title: 'Root content title', + body: 'Root content body', + }); + + const childContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + body: 'Child content with original body', + parent_id: rootContent.id, + status: 'published', + }); + + const { response, responseBody } = await contentsRequestBuilder.patch( + `/${userWithoutFeature.username}/${childContent.slug}`, + { + body: 'Updated body, even without "create:content:text_root" feature.', + }, + ); + + expect(response.status).toEqual(200); + + expect(responseBody).toStrictEqual({ + id: responseBody.id, + owner_id: userWithoutFeature.id, + parent_id: rootContent.id, + slug: childContent.slug, + title: null, + body: 'Updated body, even without "create:content:text_root" feature.', + status: 'published', + source_url: null, + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + published_at: responseBody.published_at, + deleted_at: null, + tabcoins: 0, + tabcoins_credit: 0, + tabcoins_debit: 0, + owner_username: userWithoutFeature.username, + }); + + expect(uuidVersion(responseBody.id)).toEqual(4); + expect(Date.parse(responseBody.created_at)).not.toEqual(NaN); + expect(Date.parse(responseBody.published_at)).not.toEqual(NaN); + expect(Date.parse(responseBody.updated_at)).not.toEqual(NaN); + expect(responseBody.updated_at > childContent.updated_at.toISOString()).toEqual(true); + }); + }); + + describe('User without "create:content:text_child" feature', () => { + test('"root" content with valid data', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const userWithoutFeature = await contentsRequestBuilder.buildUser({ without: ['create:content:text_child'] }); + + const rootContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + title: 'Valid user trying to update "root" content.', + body: 'It should be possible, even without the "create:content:text_child" feature.', + status: 'published', + }); + + const { response, responseBody } = await contentsRequestBuilder.patch( + `/${userWithoutFeature.username}/${rootContent.slug}`, + { source_url: 'http://www.tabnews.com.br/' }, + ); + + expect(response.status).toEqual(200); + + expect(responseBody).toStrictEqual({ + id: responseBody.id, + owner_id: userWithoutFeature.id, + parent_id: null, + slug: 'valid-user-trying-to-update-root-content', + title: 'Valid user trying to update "root" content.', + body: 'It should be possible, even without the "create:content:text_child" feature.', + status: 'published', + source_url: 'http://www.tabnews.com.br/', + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + published_at: responseBody.published_at, + deleted_at: null, + tabcoins: 1, + tabcoins_credit: 0, + tabcoins_debit: 0, + owner_username: userWithoutFeature.username, + }); + + expect(uuidVersion(responseBody.id)).toEqual(4); + expect(Date.parse(responseBody.created_at)).not.toEqual(NaN); + expect(Date.parse(responseBody.published_at)).not.toEqual(NaN); + expect(Date.parse(responseBody.updated_at)).not.toEqual(NaN); + expect(responseBody.updated_at > rootContent.updated_at.toISOString()).toEqual(true); + }); + + test('"child" content with valid data', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const userWithoutFeature = await contentsRequestBuilder.buildUser({ without: ['create:content:text_child'] }); + + const rootContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + title: 'Root content title', + body: 'Root content body', + }); + + const childContent = await orchestrator.createContent({ + owner_id: userWithoutFeature.id, + body: 'Child content body', + parent_id: rootContent.id, + }); + + const { response, responseBody } = await contentsRequestBuilder.patch( + `/${userWithoutFeature.username}/${childContent.slug}`, + { + title: 'Valid user, trying to update "child" content.', + body: "He shouldn't be able to do it because he lacks the 'create:content:text_child' feature.", + }, + ); + + expect(response.status).toEqual(403); + expect(responseBody.status_code).toEqual(403); + expect(responseBody.name).toEqual('ForbiddenError'); + expect(responseBody.message).toEqual( + 'Você não possui permissão para editar conteúdos dentro de outros conteúdos.', + ); + expect(responseBody.action).toEqual('Verifique se você possui a feature "create:content:text_child".'); + expect(uuidVersion(responseBody.error_id)).toEqual(4); + expect(uuidVersion(responseBody.request_id)).toEqual(4); + expect(responseBody.error_location_code).toEqual( + 'CONTROLLER:CONTENT:PATCH_HANDLER:CREATE:CONTENT:TEXT_CHILD:FEATURE_NOT_FOUND', + ); + }); + }); + describe('Default user', () => { test('Content without PATCH Body and "Content-Type"', async () => { const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); diff --git a/tests/integration/api/v1/contents/firewall.post.test.js b/tests/integration/api/v1/contents/firewall.post.test.js index 6b1f028af..6d35419ad 100644 --- a/tests/integration/api/v1/contents/firewall.post.test.js +++ b/tests/integration/api/v1/contents/firewall.post.test.js @@ -1,7 +1,6 @@ import { version as uuidVersion } from 'uuid'; import content from 'models/content.js'; -import event from 'models/event.js'; import orchestrator from 'tests/orchestrator.js'; beforeEach(async () => { @@ -61,9 +60,9 @@ describe('POST /api/v1/contents [FIREWALL]', () => { const request2Body = await request2.json(); const request3Body = await request3.json(); - expect(request1.status).toBe(201); - expect(request2.status).toBe(201); - expect(request3.status).toBe(429); + expect.soft(request1.status).toBe(201); + expect.soft(request2.status).toBe(201); + expect.soft(request3.status).toBe(429); expect(request3Body).toStrictEqual({ name: 'TooManyRequestsError', @@ -96,18 +95,22 @@ describe('POST /api/v1/contents [FIREWALL]', () => { expect(content2.status).toStrictEqual('draft'); expect(content3).toStrictEqual(undefined); - const events = await event.findAll(); - expect(events.length).toEqual(3); + const lastEvent = await orchestrator.getLastEvent(); - expect(uuidVersion(events[2].id)).toEqual(4); - expect(events[2].type).toEqual('firewall:block_contents:text_root'); - expect(events[2].originator_user_id).toEqual(defaultUser.id); - expect(events[2].originator_ip).toEqual('127.0.0.1'); - expect(events[2].metadata).toEqual({ - from_rule: 'create:content:text_root', - contents: [content1.id, content2.id], + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_contents:text_root', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:content:text_root', + contents: [content1.id, content2.id], + }, + created_at: lastEvent.created_at, }); - expect(Date.parse(events[2].created_at)).not.toEqual(NaN); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); }); test('Spamming valid "child" contents', async () => { @@ -176,9 +179,9 @@ describe('POST /api/v1/contents [FIREWALL]', () => { const request2Body = await request2.json(); const request3Body = await request3.json(); - expect(request1.status).toBe(201); - expect(request2.status).toBe(201); - expect(request3.status).toBe(429); + expect.soft(request1.status).toBe(201); + expect.soft(request2.status).toBe(201); + expect.soft(request3.status).toBe(429); expect(request3Body).toStrictEqual({ name: 'TooManyRequestsError', @@ -211,18 +214,22 @@ describe('POST /api/v1/contents [FIREWALL]', () => { expect(content2.status).toStrictEqual('draft'); expect(content3).toStrictEqual(undefined); - const events = await event.findAll(); - expect(events.length).toEqual(4); + const lastEvent = await orchestrator.getLastEvent(); - expect(uuidVersion(events[3].id)).toEqual(4); - expect(events[3].type).toEqual('firewall:block_contents:text_child'); - expect(events[3].originator_user_id).toEqual(defaultUser.id); - expect(events[3].originator_ip).toEqual('127.0.0.1'); - expect(events[3].metadata).toEqual({ - from_rule: 'create:content:text_child', - contents: [content1.id, content2.id], + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_contents:text_child', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:content:text_child', + contents: [content1.id, content2.id], + }, + created_at: lastEvent.created_at, }); - expect(Date.parse(events[3].created_at)).not.toEqual(NaN); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); }); }); }); diff --git a/tests/integration/api/v1/events/get.test.js b/tests/integration/api/v1/events/get.test.js deleted file mode 100644 index 2c281e7df..000000000 --- a/tests/integration/api/v1/events/get.test.js +++ /dev/null @@ -1,95 +0,0 @@ -import { version as uuidVersion } from 'uuid'; - -import event from 'models/event.js'; -import orchestrator from 'tests/orchestrator.js'; - -beforeAll(async () => { - await orchestrator.waitForAllServices(); - await orchestrator.dropAllTables(); - await orchestrator.runPendingMigrations(); -}); - -describe('GET /api/v1/events [NOT YET IMPLEMENTED]', () => { - describe('Anonymous user', () => { - test('Returning all types of events', async () => { - const createUserResponse = await fetch(`${orchestrator.webserverUrl}/api/v1/users`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: 'validusername', - email: 'valid@email.com', - password: 'validpassword', - }), - }); - - const createUserResponseBody = await createUserResponse.json(); - - await orchestrator.activateUser(createUserResponseBody); - const sessionObject = await orchestrator.createSession(createUserResponseBody); - - const createContentRootResponse = await fetch(`${orchestrator.webserverUrl}/api/v1/contents`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - cookie: `session_id=${sessionObject.token}`, - }, - body: JSON.stringify({ - title: 'Root', - body: 'Root', - status: 'published', - }), - }); - - const createContentRootResponseBody = await createContentRootResponse.json(); - - const createContentChildRootResponse = await fetch(`${orchestrator.webserverUrl}/api/v1/contents`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - cookie: `session_id=${sessionObject.token}`, - }, - body: JSON.stringify({ - title: 'Child', - body: 'Child', - status: 'published', - parent_id: createContentRootResponseBody.id, - }), - }); - - const createContentChildResponseBody = await createContentChildRootResponse.json(); - - const events = await event.findAll(); - - expect(events.length).toEqual(3); - - expect(uuidVersion(events[0].id)).toEqual(4); - expect(events[0].type).toEqual('create:user'); - expect(events[0].originator_user_id).toEqual(createUserResponseBody.id); - expect(events[0].originator_ip).toEqual('127.0.0.1'); - expect(events[0].metadata).toEqual({ - id: createUserResponseBody.id, - }); - expect(Date.parse(events[0].created_at)).not.toEqual(NaN); - - expect(uuidVersion(events[1].id)).toEqual(4); - expect(events[1].type).toEqual('create:content:text_root'); - expect(events[1].originator_user_id).toEqual(createUserResponseBody.id); - expect(events[1].originator_ip).toEqual('127.0.0.1'); - expect(events[1].metadata).toEqual({ - id: createContentRootResponseBody.id, - }); - expect(Date.parse(events[1].created_at)).not.toEqual(NaN); - - expect(uuidVersion(events[2].id)).toEqual(4); - expect(events[2].type).toEqual('create:content:text_child'); - expect(events[2].originator_user_id).toEqual(createUserResponseBody.id); - expect(events[2].originator_ip).toEqual('127.0.0.1'); - expect(events[2].metadata).toEqual({ - id: createContentChildResponseBody.id, - }); - expect(Date.parse(events[2].created_at)).not.toEqual(NaN); - }); - }); -}); diff --git a/tests/integration/api/v1/users/firewall.post.test.js b/tests/integration/api/v1/users/firewall.post.test.js index 6c8a4ae2e..88ce2040a 100644 --- a/tests/integration/api/v1/users/firewall.post.test.js +++ b/tests/integration/api/v1/users/firewall.post.test.js @@ -1,6 +1,5 @@ import { version as uuidVersion } from 'uuid'; -import event from 'models/event.js'; import user from 'models/user.js'; import orchestrator from 'tests/orchestrator.js'; @@ -54,9 +53,9 @@ describe('POST /api/v1/users [FIREWALL]', () => { const request2Body = await request2.json(); const request3Body = await request3.json(); - expect(request1.status).toBe(201); - expect(request2.status).toBe(201); - expect(request3.status).toBe(429); + expect.soft(request1.status).toBe(201); + expect.soft(request2.status).toBe(201); + expect.soft(request3.status).toBe(429); expect(request3Body).toStrictEqual({ name: 'TooManyRequestsError', @@ -76,18 +75,22 @@ describe('POST /api/v1/users [FIREWALL]', () => { expect(user1.features).toStrictEqual([]); expect(user2.features).toStrictEqual([]); - const events = await event.findAll(); - expect(events.length).toEqual(3); + const lastEvent = await orchestrator.getLastEvent(); - expect(uuidVersion(events[2].id)).toEqual(4); - expect(events[2].type).toEqual('firewall:block_users'); - expect(events[2].originator_user_id).toEqual(null); - expect(events[2].originator_ip).toEqual('127.0.0.1'); - expect(events[2].metadata).toEqual({ - from_rule: 'create:user', - users: [user1.id, user2.id], + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_users', + originator_user_id: null, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:user', + users: [user1.id, user2.id], + }, + created_at: lastEvent.created_at, }); - expect(Date.parse(events[2].created_at)).not.toEqual(NaN); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); }); }); }); diff --git a/tests/integration/models/event.test.js b/tests/integration/models/event.test.js new file mode 100644 index 000000000..e10b05353 --- /dev/null +++ b/tests/integration/models/event.test.js @@ -0,0 +1,587 @@ +import { version as uuidVersion } from 'uuid'; + +import orchestrator from 'tests/orchestrator'; +import RequestBuilder from 'tests/request-builder'; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); + await orchestrator.dropAllTables(); + await orchestrator.runPendingMigrations(); +}); + +describe('models/event', () => { + describe('Anonymous user', () => { + test('Create "create:user" event', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const { responseBody: createUserResponseBody } = await usersRequestBuilder.post({ + username: 'validusername', + email: 'valid@email.com', + password: 'validpassword', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'create:user', + originator_user_id: createUserResponseBody.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createUserResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + }); + + describe('Default user', () => { + test('Create "update:user" event', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const defaultUser = await usersRequestBuilder.buildUser(); + + const { responseBody } = await usersRequestBuilder.patch(`/${defaultUser.username}`, { + username: 'newusername', + description: 'new description', + email: 'new@email.com', + notifications: false, + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:user', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: defaultUser.id, + updatedFields: ['description', 'notifications', 'username'], + username: { + old: defaultUser.username, + new: responseBody.username, + }, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "create:content:text_root" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'create:content:text_root', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentRootResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "create:content:text_child" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + const { responseBody: createContentChildResponseBody } = await contentsRequestBuilder.post({ + title: 'Child', + body: 'Child', + status: 'published', + parent_id: createContentRootResponseBody.id, + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'create:content:text_child', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentChildResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:text_root" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + await contentsRequestBuilder.patch(`/${defaultUser.username}/${createContentRootResponseBody.slug}`, { + title: 'Root Updated', + body: 'Root Updated', + status: 'deleted', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:text_root', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentRootResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:text_child" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + const { responseBody: createContentChildResponseBody } = await contentsRequestBuilder.post({ + body: 'Child', + status: 'published', + parent_id: createContentRootResponseBody.id, + }); + + await contentsRequestBuilder.patch(`/${defaultUser.username}/${createContentChildResponseBody.slug}`, { + body: 'Child Updated', + status: 'deleted', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:text_child', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentChildResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:tabcoins" with "transaction_type" set to "credit"', async () => { + const firstUser = await orchestrator.createUser(); + const firstUserContent = await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Root', + body: 'Body', + status: 'published', + }); + + const tabcoinsRequestBuilder = new RequestBuilder( + `/api/v1/contents/${firstUser.username}/${firstUserContent.slug}/tabcoins`, + ); + const secondUser = await tabcoinsRequestBuilder.buildUser(); + + await orchestrator.createBalance({ + balanceType: 'user:tabcoin', + recipientId: secondUser.id, + amount: 2, + }); + + await tabcoinsRequestBuilder.post({ + transaction_type: 'credit', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:tabcoins', + originator_user_id: secondUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + amount: 2, + content_id: firstUserContent.id, + from_user_id: secondUser.id, + content_owner_id: firstUser.id, + transaction_type: 'credit', + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:tabcoins" with "transaction_type" set to "debit"', async () => { + const firstUser = await orchestrator.createUser(); + const firstUserContent = await orchestrator.createContent({ + owner_id: firstUser.id, + title: 'Root', + body: 'Body', + status: 'published', + }); + + const tabcoinsRequestBuilder = new RequestBuilder( + `/api/v1/contents/${firstUser.username}/${firstUserContent.slug}/tabcoins`, + ); + const secondUser = await tabcoinsRequestBuilder.buildUser(); + + await orchestrator.createBalance({ + balanceType: 'user:tabcoin', + recipientId: secondUser.id, + amount: 2, + }); + + await tabcoinsRequestBuilder.post({ + transaction_type: 'debit', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:tabcoins', + originator_user_id: secondUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + amount: 2, + content_id: firstUserContent.id, + from_user_id: secondUser.id, + content_owner_id: firstUser.id, + transaction_type: 'debit', + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "reward:user:tabcoins" event', async () => { + const userRequestBuilder = new RequestBuilder('/api/v1/user'); + const defaultUser = await userRequestBuilder.buildUser(); + await orchestrator.createPrestige(defaultUser.id); + + await orchestrator.updateRewardedAt( + defaultUser.id, + new Date(Date.now() - 1000 - 1000 * 60 * 60 * 24), // 1 day and 1 second ago + ); + + await userRequestBuilder.get(); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'reward:user:tabcoins', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + amount: 2, + reward_type: 'daily', + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + expect(Date.parse(lastEvent.created_at)).toBeGreaterThan(Date.now() - 1000); + }); + }); + + describe('Privileged user', () => { + test('Create "update:user" event', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const userToBeUpdated = await usersRequestBuilder.buildUser(); + const privilegedUser = await usersRequestBuilder.buildUser({ with: ['update:user:others'] }); + + await usersRequestBuilder.patch(`/${userToBeUpdated.username}`, { + username: 'newusername', + description: 'new description', + email: 'new@email.com', + notifications: false, + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:user', + originator_user_id: privilegedUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: userToBeUpdated.id, + updatedFields: ['description'], + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "ban:user" event', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const userToBan = await usersRequestBuilder.buildUser(); + const privilegedUser = await usersRequestBuilder.buildUser({ with: ['ban:user'] }); + + await usersRequestBuilder.delete(`/${userToBan.username}`, { + ban_type: 'nuke', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'ban:user', + originator_user_id: privilegedUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + ban_type: 'nuke', + user_id: userToBan.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:text_root" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + const privilegedUser = await contentsRequestBuilder.buildUser({ with: ['update:content:others'] }); + + await contentsRequestBuilder.patch(`/${defaultUser.username}/${createContentRootResponseBody.slug}`, { + title: 'Root Updated', + body: 'Root Updated', + status: 'deleted', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:text_root', + originator_user_id: privilegedUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentRootResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "update:content:text_child" event', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: createContentRootResponseBody } = await contentsRequestBuilder.post({ + title: 'Root', + body: 'Root', + status: 'published', + }); + + const { responseBody: createContentChildResponseBody } = await contentsRequestBuilder.post({ + body: 'Child', + status: 'published', + parent_id: createContentRootResponseBody.id, + }); + + const privilegedUser = await contentsRequestBuilder.buildUser({ with: ['update:content:others'] }); + + await contentsRequestBuilder.patch(`/${defaultUser.username}/${createContentChildResponseBody.slug}`, { + body: 'Child Updated', + status: 'deleted', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'update:content:text_child', + originator_user_id: privilegedUser.id, + originator_ip: '127.0.0.1', + created_at: lastEvent.created_at, + metadata: { + id: createContentChildResponseBody.id, + }, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + }); + + describe('Firewall', () => { + beforeEach(async () => { + await orchestrator.dropAllTables(); + await orchestrator.runPendingMigrations(); + await orchestrator.createFirewallTestFunctions(); + }); + + test('Create "firewall:block_users" event', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + + const { responseBody: user1 } = await usersRequestBuilder.post({ + username: 'request1', + email: 'request1@gmail.com', + password: 'validpassword', + }); + + const { responseBody: user2 } = await usersRequestBuilder.post({ + username: 'request2', + email: 'request2@gmail.com', + password: 'validpassword', + }); + + await usersRequestBuilder.post({ + username: 'request3', + email: 'request3@gmail.com', + password: 'validpassword', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_users', + originator_user_id: null, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:user', + users: [user1.id, user2.id], + }, + created_at: lastEvent.created_at, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "firewall:block_contents:text_root" event', async () => { + const contentRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentRequestBuilder.buildUser(); + + const { responseBody: content1 } = await contentRequestBuilder.post({ + title: 'Título 1', + body: 'Corpo', + }); + + const { responseBody: content2 } = await contentRequestBuilder.post({ + title: 'Título 2', + body: 'Corpo', + }); + + await contentRequestBuilder.post({ + title: 'Título 3', + body: 'Corpo', + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_contents:text_root', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:content:text_root', + contents: [content1.id, content2.id], + }, + created_at: lastEvent.created_at, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + + test('Create "firewall:block_contents:text_child" event', async () => { + const contentRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentRequestBuilder.buildUser(); + + const { responseBody: rootContentBody } = await contentRequestBuilder.post({ + title: 'Root Content', + body: 'Corpo', + }); + + const { responseBody: content1 } = await contentRequestBuilder.post({ + body: 'Corpo', + parent_id: rootContentBody.id, + }); + + const { responseBody: content2 } = await contentRequestBuilder.post({ + body: 'Corpo', + parent_id: rootContentBody.id, + }); + + await contentRequestBuilder.post({ + body: 'Corpo', + parent_id: rootContentBody.id, + }); + + const lastEvent = await orchestrator.getLastEvent(); + + expect(lastEvent).toStrictEqual({ + id: lastEvent.id, + type: 'firewall:block_contents:text_child', + originator_user_id: defaultUser.id, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:content:text_child', + contents: [content1.id, content2.id], + }, + created_at: lastEvent.created_at, + }); + + expect(uuidVersion(lastEvent.id)).toBe(4); + expect(Date.parse(lastEvent.created_at)).not.toBe(NaN); + }); + }); +}); diff --git a/tests/orchestrator.js b/tests/orchestrator.js index 02f17560b..c2cb950c4 100644 --- a/tests/orchestrator.js +++ b/tests/orchestrator.js @@ -394,6 +394,11 @@ async function updateRewardedAt(userId, rewardedAt) { return await database.query(query); } +async function getLastEvent() { + const results = await database.query('SELECT * FROM events ORDER BY created_at DESC LIMIT 1;'); + return results.rows[0]; +} + function parseSetCookies(response) { const setCookieHeaderValues = response.headers.get('set-cookie'); const parsedCookies = setCookieParser.parse(setCookieHeaderValues, { map: true }); @@ -401,27 +406,28 @@ function parseSetCookies(response) { } const orchestrator = { - waitForAllServices, - dropAllTables, - runPendingMigrations, - webserverUrl, - deleteAllEmails, - getLastEmail, - createUser, activateUser, - createSession, - findSessionByToken, addFeaturesToUser, - removeFeaturesFromUser, + createBalance, createContent, - updateContent, - createRecoveryToken, createFirewallTestFunctions, - createBalance, createPrestige, createRate, - updateRewardedAt, + createRecoveryToken, + createSession, + createUser, + deleteAllEmails, + dropAllTables, + findSessionByToken, + getLastEmail, + getLastEvent, parseSetCookies, + removeFeaturesFromUser, + runPendingMigrations, + updateContent, + updateRewardedAt, + waitForAllServices, + webserverUrl, }; export default orchestrator; diff --git a/tests/request-builder.js b/tests/request-builder.js index 01069271c..aa95cc894 100644 --- a/tests/request-builder.js +++ b/tests/request-builder.js @@ -107,6 +107,29 @@ export default class RequestBuilder { return { response, responseBody }; } + async delete(routeOrRequestBody, inputRequestBody) { + const { route, requestBody } = this.getRouteAndRequestBody(routeOrRequestBody, inputRequestBody); + + if (!this.headers) { + this.buildHeaders(); + } + + const fetchData = { + method: 'DELETE', + headers: this.headers, + }; + + if (requestBody) { + fetchData.body = typeof requestBody === 'object' ? JSON.stringify(requestBody) : requestBody; + } + + const response = await fetch(`${this.baseUrl}${route}`, fetchData); + + const responseBody = await response.json(); + + return { response, responseBody }; + } + getRouteAndRequestBody(routeOrRequestBody = '', inputRequestBody) { let route = routeOrRequestBody; let requestBody = inputRequestBody;