diff --git a/models/content.js b/models/content.js index 84312362b..833489446 100644 --- a/models/content.js +++ b/models/content.js @@ -488,7 +488,8 @@ async function creditOrDebitTabCoins(oldContent, newContent, options = {}) { // or if it was deleted and is catch by firewall: "deleted" -> "firewall". if ( oldContent && - ((!oldContent.published_at && newContent.status !== 'published') || oldContent.status === 'deleted') + ((!oldContent.published_at && newContent.status !== 'published') || + ['deleted', 'firewall'].includes(oldContent.status)) ) { return; } diff --git a/models/event.js b/models/event.js index d8c2480b5..8f8a6a97f 100644 --- a/models/event.js +++ b/models/event.js @@ -70,25 +70,6 @@ async function findAllRelatedEvents(id) { return results.rows; } -async function findOneById(eventId) { - const query = { - text: ` - SELECT - * - FROM - events - WHERE - id = $1 - LIMIT - 1 - ;`, - values: [eventId], - }; - - const results = await database.query(query); - return results.rows[0]; -} - async function findOneByOriginalEventId(originalEventId, where = {}) { const firstWhereArgumentIndex = 2; const whereClause = buildWhereClause(where, firstWhereArgumentIndex); @@ -136,6 +117,5 @@ function buildWhereClause(where, nextArgumentIndex) { export default Object.freeze({ create, findAllRelatedEvents, - findOneById, findOneByOriginalEventId, }); diff --git a/models/firewall/find.js b/models/firewall/find.js index 6caf1fb44..8cdef14c2 100644 --- a/models/firewall/find.js +++ b/models/firewall/find.js @@ -6,9 +6,10 @@ import user from 'models/user'; import eventTypes from './event-types'; async function findByEventId(eventId) { - const foundOriginalEvent = await event.findOneById(eventId); + const relatedEvents = await event.findAllRelatedEvents(eventId); + const foundRequestedEvent = relatedEvents.find((event) => event.id === eventId); - if (!foundOriginalEvent || !eventTypes.firewall.includes(foundOriginalEvent.type)) { + if (!foundRequestedEvent || !eventTypes.firewall.includes(foundRequestedEvent.type)) { throw new NotFoundError({ message: `O id "${eventId}" não foi encontrado no sistema.`, action: 'Verifique se o "id" está digitado corretamente.', @@ -18,20 +19,11 @@ async function findByEventId(eventId) { }); } - const foundReviewedEvent = await event.findOneByOriginalEventId(eventId, { - type: eventTypes.review, - }); - - const events = [foundOriginalEvent]; - - if (foundReviewedEvent) { - events.push(foundReviewedEvent); - } - const affectedData = await getAffectedData(events.at(-1)); + const affectedData = await getAffectedData(relatedEvents.at(-1)); return { affected: affectedData, - events: events, + events: relatedEvents, }; } diff --git a/models/firewall/review.js b/models/firewall/review.js index 44cd50981..9291bc3df 100644 --- a/models/firewall/review.js +++ b/models/firewall/review.js @@ -17,7 +17,9 @@ const reviewFunctions = { }; async function reviewEvent({ action, eventId, originatorIp, originatorUserId }) { - const firewallEvent = await validateAndGetFirewallEventToReview(eventId); + const relatedEvents = await validateAndGetRelatedEventsToReview(eventId); + + const firewallEvent = relatedEvents.find((e) => e.id === eventId); const metadata = { original_event_id: eventId, @@ -44,7 +46,7 @@ async function reviewEvent({ action, eventId, originatorIp, originatorUserId }) }, ); - const events = [firewallEvent, createdEvent]; + const events = [...relatedEvents, createdEvent]; const affected = await reviewFunctions[eventType](firewallEvent, { transaction, @@ -65,33 +67,33 @@ async function reviewEvent({ action, eventId, originatorIp, originatorUserId }) } } -async function validateAndGetFirewallEventToReview(eventId) { - const reviewingEvent = await event.findOneByOriginalEventId(eventId, { - types: eventTypes.review, - }); +async function validateAndGetRelatedEventsToReview(eventId) { + const relatedEvents = await event.findAllRelatedEvents(eventId); - if (reviewingEvent) { - throw new ValidationError({ - message: 'Você está tentando analisar um evento que já foi analisado.', - action: 'Utilize um "id" que aponte para um evento de firewall que ainda não foi analisado.', + if (!relatedEvents.length) { + throw new NotFoundError({ + message: `O id "${eventId}" não foi encontrado no sistema.`, + action: 'Verifique se o "id" está digitado corretamente.', stack: new Error().stack, - errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:EVENT_ALREADY_REVIEWED', + errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:NOT_FOUND', key: 'id', }); } - const firewallEvent = await event.findOneById(eventId); + const reviewingEvent = relatedEvents.find((e) => eventTypes.review.includes(e.type)); - if (!firewallEvent) { - throw new NotFoundError({ - message: `O id "${eventId}" não foi encontrado no sistema.`, - action: 'Verifique se o "id" está digitado corretamente.', + if (reviewingEvent) { + throw new ValidationError({ + message: 'Você está tentando analisar um evento que já foi analisado.', + action: 'Utilize um "id" que aponte para um evento de firewall que ainda não foi analisado.', stack: new Error().stack, - errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:NOT_FOUND', + errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:EVENT_ALREADY_REVIEWED', key: 'id', }); } + const firewallEvent = relatedEvents.find((e) => e.id === eventId); + if (!eventTypes.firewall.includes(firewallEvent.type)) { throw new ValidationError({ message: 'Você está tentando analisar um evento inválido.', @@ -102,7 +104,7 @@ async function validateAndGetFirewallEventToReview(eventId) { }); } - return firewallEvent; + return relatedEvents; } async function confirmBlockUsers(firewallEvent, options) { @@ -168,7 +170,7 @@ async function unblockUsers(firewallEvent, options) { const activeUsers = []; for (const userData of users) { - if (userData.features.length === 0) { + if (userData.features.length === 0 || (userData.features.length === 1 && userData.features[0] === 'nuked')) { inactiveUsers.push(userData.id); } else { activeUsers.push(userData.id); diff --git a/tests/integration/api/v1/events/firewall/[id]/get.test.js b/tests/integration/api/v1/events/firewall/[id]/get.test.js index 708aacab2..01be08a50 100644 --- a/tests/integration/api/v1/events/firewall/[id]/get.test.js +++ b/tests/integration/api/v1/events/firewall/[id]/get.test.js @@ -249,6 +249,90 @@ describe('GET /api/v1/events/firewall/[id]', () => { }); }); + test('With two consecutive "firewall:block_users" events from the same IP', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`); + await firewallRequestBuilder.buildUser({ with: ['read:firewall'] }); + + // Create users + const { responseBody: user1 } = await usersRequestBuilder.post({ + username: 'firstUser', + email: 'first-user@gmail.com', + password: 'password', + }); + const { responseBody: user2 } = await usersRequestBuilder.post({ + username: 'secondUser', + email: 'second-user@gmail.com', + password: 'password', + }); + const { response: user3Response } = await usersRequestBuilder.post({ + username: 'thirdUser', + email: 'third-user@gmail.com', + password: 'password', + }); + + expect(user3Response.status).toBe(429); + + const firstFirewallEvent = await orchestrator.getLastEvent(); + + const { response: user4Response } = await usersRequestBuilder.post({ + username: 'fourthUser', + email: 'fourth-user@gmail.com', + password: 'password', + }); + + expect(user4Response.status).toBe(429); + + const secondFirewallEvent = await orchestrator.getLastEvent(); + + expect(firstFirewallEvent.id).not.toBe(secondFirewallEvent.id); + + // Get firewall side-effects + const { response: firstFirewallResponse, responseBody: firstFirewallResponseBody } = + await firewallRequestBuilder.get(`/${firstFirewallEvent.id}`); + const { response: secondFirewallResponse, responseBody: secondFirewallResponseBody } = + await firewallRequestBuilder.get(`/${secondFirewallEvent.id}`); + + expect.soft(firstFirewallResponse.status).toBe(200); + expect.soft(secondFirewallResponse.status).toBe(200); + + await usersRequestBuilder.setUser(user1); + const { responseBody: user1AfterFirewall } = await usersRequestBuilder.get(`/${user1.username}`); + + await usersRequestBuilder.setUser(user2); + const { responseBody: user2AfterFirewall } = await usersRequestBuilder.get(`/${user2.username}`); + + expect(firstFirewallResponseBody).toStrictEqual({ + affected: { + users: [user1AfterFirewall, user2AfterFirewall], + }, + events: [ + { + created_at: firstFirewallEvent.created_at.toISOString(), + id: firstFirewallEvent.id, + metadata: { + from_rule: 'create:user', + users: [user1AfterFirewall.id, user2AfterFirewall.id], + }, + originator_user_id: null, + type: 'firewall:block_users', + }, + { + created_at: secondFirewallEvent.created_at.toISOString(), + id: secondFirewallEvent.id, + metadata: { + from_rule: 'create:user', + users: [user1AfterFirewall.id, user2AfterFirewall.id], + }, + originator_user_id: null, + type: 'firewall:block_users', + }, + ], + }); + + expect(firstFirewallResponseBody).toStrictEqual(secondFirewallResponseBody); + }); + test.each([ { action: 'undo', @@ -335,6 +419,116 @@ describe('GET /api/v1/events/firewall/[id]', () => { }); }); + test('With two consecutive "firewall:block_users" events from the same IP reviewed', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`); + const firewallUser = await firewallRequestBuilder.buildUser({ with: ['read:firewall', 'review:firewall'] }); + + // Create users + const { responseBody: user1 } = await usersRequestBuilder.post({ + username: 'firstUser', + email: 'first-user@gmail.com', + password: 'password', + }); + const { responseBody: user2 } = await usersRequestBuilder.post({ + username: 'secondUser', + email: 'second-user@gmail.com', + password: 'password', + }); + const { response: user3Response } = await usersRequestBuilder.post({ + username: 'thirdUser', + email: 'third-user@gmail.com', + password: 'password', + }); + + expect(user3Response.status).toBe(429); + + const firstFirewallEvent = await orchestrator.getLastEvent(); + + const { response: user4Response } = await usersRequestBuilder.post({ + username: 'fourthUser', + email: 'fourth-user@gmail.com', + password: 'password', + }); + + expect(user4Response.status).toBe(429); + + const secondFirewallEvent = await orchestrator.getLastEvent(); + + expect(firstFirewallEvent.id).not.toBe(secondFirewallEvent.id); + + // Check firewall side-effect + expect(firstFirewallEvent.type).toBe('firewall:block_users'); + expect(firstFirewallEvent.metadata.users).toStrictEqual([user1.id, user2.id]); + expect(secondFirewallEvent.type).toBe(firstFirewallEvent.type); + expect(secondFirewallEvent.metadata.users).toStrictEqual(firstFirewallEvent.metadata.users); + + // Review firewall side-effect + const reviewFirewallRequestBuilder = new RequestBuilder( + `/api/v1/moderations/review_firewall/${firstFirewallEvent.id}`, + ); + await reviewFirewallRequestBuilder.setUser(firewallUser); + const { response: reviewResponse } = await reviewFirewallRequestBuilder.post({ action: 'confirm' }); + expect(reviewResponse.status).toBe(200); + + // Get reviewed firewall event from first event id + const reviewEvent = await orchestrator.getLastEvent(); + + const { response: responseFromFirstEvent, responseBody: responseBodyFromFirstEvent } = + await firewallRequestBuilder.get(`/${firstFirewallEvent.id}`); + + expect.soft(responseFromFirstEvent.status).toBe(200); + + const user1AfterReview = await user.findOneById(user1.id, { withBalance: true }); + const user2AfterReview = await user.findOneById(user2.id, { withBalance: true }); + + expect(responseBodyFromFirstEvent).toStrictEqual({ + affected: { + users: [mapUserData(user1AfterReview), mapUserData(user2AfterReview)], + }, + events: [ + { + created_at: firstFirewallEvent.created_at.toISOString(), + id: firstFirewallEvent.id, + metadata: { + from_rule: 'create:user', + users: [user1.id, user2.id], + }, + originator_user_id: null, + type: 'firewall:block_users', + }, + { + created_at: secondFirewallEvent.created_at.toISOString(), + id: secondFirewallEvent.id, + metadata: { + from_rule: 'create:user', + users: [user1.id, user2.id], + }, + originator_user_id: null, + type: 'firewall:block_users', + }, + { + created_at: reviewEvent.created_at.toISOString(), + id: reviewEvent.id, + metadata: { + original_event_id: firstFirewallEvent.id, + users: [user1AfterReview.id, user2AfterReview.id], + }, + originator_user_id: firewallUser.id, + type: 'moderation:block_users', + }, + ], + }); + + // Get reviewed firewall event from second event id + const { response: responseFromSecondEvent, responseBody: responseBodyFromSecondEvent } = + await firewallRequestBuilder.get(`/${secondFirewallEvent.id}`); + + expect(responseFromSecondEvent.status).toBe(200); + + expect(responseBodyFromSecondEvent).toStrictEqual(responseBodyFromFirstEvent); + }); + test('With a "firewall:block_contents:text_root" event', async () => { const usersRequestBuilder = new RequestBuilder(`/api/v1/users`); const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`); diff --git a/tests/integration/api/v1/moderations/review_firewall/[id]/post.test.js b/tests/integration/api/v1/moderations/review_firewall/[id]/post.test.js index 9c73e1036..907457155 100644 --- a/tests/integration/api/v1/moderations/review_firewall/[id]/post.test.js +++ b/tests/integration/api/v1/moderations/review_firewall/[id]/post.test.js @@ -280,6 +280,79 @@ describe('POST /api/v1/moderations/review_firewall/[id]', () => { }); }); + test('Review related events twice', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + + // Create users + const { responseBody: user1 } = await usersRequestBuilder.post({ + username: 'firstUser', + email: 'first-user@gmail.com', + password: 'password', + }); + const { responseBody: user2 } = await usersRequestBuilder.post({ + username: 'secondUser', + email: 'second-user@gmail.com', + password: 'password', + }); + const { response: user3Response } = await usersRequestBuilder.post({ + username: 'thirdUser', + email: 'third-user@gmail.com', + password: 'password', + }); + const firewallEvent1 = await orchestrator.getLastEvent(); + + const { response: user4Response } = await usersRequestBuilder.post({ + username: 'fourthUser', + email: 'fourth-user@gmail.com', + password: 'password', + }); + const firewallEvent2 = await orchestrator.getLastEvent(); + + expect(user3Response.status).toBe(429); + expect(user4Response.status).toBe(429); + + // Check firewall side-effect + expect(firewallEvent1.type).toBe('firewall:block_users'); + expect(firewallEvent2.type).toBe('firewall:block_users'); + + expect(firewallEvent1.metadata.users).toStrictEqual([user1.id, user2.id]); + + // Review firewall + const reviewFirstEventRequestBuilder = new RequestBuilder( + `/api/v1/moderations/review_firewall/${firewallEvent1.id}`, + ); + const firewallUser = await reviewFirstEventRequestBuilder.buildUser({ with: ['review:firewall'] }); + + const { response } = await reviewFirstEventRequestBuilder.post({ + action: 'confirm', + }); + + expect(response.status).toBe(200); + + // Review related event + const reviewSecondEventRequestBuilder = new RequestBuilder( + `/api/v1/moderations/review_firewall/${firewallEvent2.id}`, + ); + await reviewSecondEventRequestBuilder.setUser(firewallUser); + + const { response: responseAgain, responseBody: responseAgainBody } = await reviewSecondEventRequestBuilder.post({ + action: 'undo', + }); + + expect.soft(responseAgain.status).toBe(400); + + expect(responseAgainBody).toStrictEqual({ + name: 'ValidationError', + message: 'Você está tentando analisar um evento que já foi analisado.', + action: 'Utilize um "id" que aponte para um evento de firewall que ainda não foi analisado.', + status_code: 400, + error_id: responseAgainBody.error_id, + request_id: responseAgainBody.request_id, + error_location_code: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:EVENT_ALREADY_REVIEWED', + key: 'id', + }); + }); + describe('With action = "undo"', () => { test('With a "firewall:block_users" event', async () => { const usersRequestBuilder = new RequestBuilder('/api/v1/users'); @@ -796,6 +869,183 @@ describe('POST /api/v1/moderations/review_firewall/[id]', () => { expect(user2AfterUndo).toStrictEqual(user2AfterRate); }); + test('With three "firewall:block_contents:text_root" events for the same contents', async () => { + // Create user and contents + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const defaultUser = await contentsRequestBuilder.buildUser(); + + const { responseBody: content1 } = await createContentViaApi(contentsRequestBuilder); + const { responseBody: content2 } = await createContentViaApi(contentsRequestBuilder); + const { response: responseContent3 } = await createContentViaApi(contentsRequestBuilder); + const firewallEvent1 = await orchestrator.getLastEvent(); + const { response: responseContent4 } = await createContentViaApi(contentsRequestBuilder); + const firewallEvent2 = await orchestrator.getLastEvent(); + const { response: responseContent5 } = await createContentViaApi(contentsRequestBuilder); + const firewallEvent3 = await orchestrator.getLastEvent(); + + expect(responseContent3.status).toBe(429); + expect(responseContent4.status).toBe(429); + expect(responseContent5.status).toBe(429); + + // Check firewall side-effect + const content1AfterSideEffect = await content.findOne({ where: { id: content1.id } }); + const content2AfterSideEffect = await content.findOne({ where: { id: content2.id } }); + + expect(content1AfterSideEffect.status).toBe('firewall'); + expect(content2AfterSideEffect.status).toBe('firewall'); + + expect(firewallEvent1.type).toBe('firewall:block_contents:text_root'); + expect(firewallEvent2.type).toBe('firewall:block_contents:text_root'); + expect(firewallEvent3.type).toBe('firewall:block_contents:text_root'); + + expect(firewallEvent1.metadata.contents).toStrictEqual([content1.id, content2.id]); + + expect(firewallEvent2).toStrictEqual({ + ...firewallEvent1, + id: firewallEvent2.id, + created_at: firewallEvent2.created_at, + }); + + expect(firewallEvent3).toStrictEqual({ + ...firewallEvent1, + id: firewallEvent3.id, + created_at: firewallEvent3.created_at, + }); + + expect(firewallEvent1.id).not.toBe(firewallEvent2.id); + expect(firewallEvent1.id).not.toBe(firewallEvent3.id); + expect(firewallEvent2.id).not.toBe(firewallEvent3.id); + + // Undo firewall side-effect from second event + const reviewFirewallRequestBuilder = new RequestBuilder( + `/api/v1/moderations/review_firewall/${firewallEvent2.id}`, + ); + const firewallUser = await reviewFirewallRequestBuilder.buildUser({ + with: ['read:firewall', 'review:firewall'], + }); + + const { response, responseBody } = await reviewFirewallRequestBuilder.post({ + action: 'undo', + }); + + expect.soft(response.status).toBe(200); + + expect(responseBody).toStrictEqual({ + affected: { + contents: [ + { + body: content1.body, + created_at: content1.created_at, + deleted_at: content1.deleted_at, + id: content1.id, + owner_id: content1.owner_id, + parent_id: content1.parent_id, + published_at: content1.published_at, + slug: content1.slug, + source_url: content1.source_url, + status: content1.status, + title: content1.title, + type: content1.type, + updated_at: content1.updated_at, + }, + { + body: content2.body, + created_at: content2.created_at, + deleted_at: content2.deleted_at, + id: content2.id, + owner_id: content2.owner_id, + parent_id: content2.parent_id, + published_at: content2.published_at, + slug: content2.slug, + source_url: content2.source_url, + status: content2.status, + title: content2.title, + type: content2.type, + updated_at: content2.updated_at, + }, + ], + users: [ + { + created_at: defaultUser.created_at.toISOString(), + description: defaultUser.description, + features: defaultUser.features, + id: defaultUser.id, + tabcash: 0, + tabcoins: 0, + updated_at: defaultUser.updated_at.toISOString(), + username: defaultUser.username, + }, + ], + }, + events: [ + { + created_at: firewallEvent1.created_at.toISOString(), + id: firewallEvent1.id, + metadata: firewallEvent1.metadata, + originator_user_id: firewallEvent1.originator_user_id, + type: firewallEvent1.type, + }, + { + created_at: firewallEvent2.created_at.toISOString(), + id: firewallEvent2.id, + metadata: firewallEvent2.metadata, + originator_user_id: firewallEvent2.originator_user_id, + type: firewallEvent2.type, + }, + { + created_at: firewallEvent3.created_at.toISOString(), + id: firewallEvent3.id, + metadata: firewallEvent3.metadata, + originator_user_id: firewallEvent3.originator_user_id, + type: firewallEvent3.type, + }, + { + created_at: responseBody.events[3].created_at, + id: responseBody.events[3].id, + metadata: { + original_event_id: firewallEvent2.id, + contents: firewallEvent2.metadata.contents, + }, + originator_user_id: firewallUser.id, + type: 'moderation:unblock_contents:text_root', + }, + ], + }); + + const createdEventResponse = responseBody.events[1]; + expect(Date.parse(createdEventResponse.created_at)).not.toBeNaN(); + + // Check "undo" event + const undoEvent = await orchestrator.getLastEvent(); + expect(undoEvent).toStrictEqual({ + created_at: expect.any(Date), + id: undoEvent.id, + metadata: { + original_event_id: firewallEvent2.id, + contents: firewallEvent2.metadata.contents, + }, + originator_ip: '127.0.0.1', + originator_user_id: firewallUser.id, + type: 'moderation:unblock_contents:text_root', + }); + + expect(uuidVersion(undoEvent.id)).toBe(4); + + // Check contents + const content1AfterUndo = await content.findOne({ where: { id: content1.id } }); + const content2AfterUndo = await content.findOne({ where: { id: content2.id } }); + + expect(content1AfterUndo).toStrictEqual({ + ...content1AfterSideEffect, + status: 'published', + }); + + expect(content2AfterUndo).toStrictEqual({ + ...content2AfterSideEffect, + status: 'published', + }); + }); + test('With a "firewall:block_contents:text_child" event', async () => { // Create user and contents const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); @@ -1705,5 +1955,460 @@ describe('POST /api/v1/moderations/review_firewall/[id]', () => { expect(content2AfterConfirm.status).toBe('deleted'); }); }); + + describe('Different firewall events containing an element in common', () => { + test('With two "firewall:block_users" events, "confirm" and "undo"', async () => { + const usersRequestBuilder = new RequestBuilder('/api/v1/users'); + const { response: response1, responseBody: user1 } = await usersRequestBuilder.post({ + username: 'request1', + email: 'request1@gmail.com', + password: 'validpassword', + }); + + const firstCreateUserEvent = await orchestrator.getLastEvent(); + + await orchestrator.updateEventCreatedAt(firstCreateUserEvent.id, new Date(Date.now() - 4 * 1000)); + + const { response: response2, responseBody: user2 } = await usersRequestBuilder.post({ + username: 'request2', + email: 'request2@gmail.com', + password: 'validpassword', + }); + + const { response: response3 } = await usersRequestBuilder.post({ + username: 'request3', + email: 'request3@gmail.com', + password: 'validpassword', + }); + + const firstFirewallEvent = await orchestrator.getLastEvent(); + + await orchestrator.updateEventCreatedAt(firstCreateUserEvent.id, new Date(Date.now() - 30 * 60 * 1000)); + + const { response: response4, responseBody: user4 } = await usersRequestBuilder.post({ + username: 'request4', + email: 'request4@gmail.com', + password: 'validpassword', + }); + + const { response: response5 } = await usersRequestBuilder.post({ + username: 'request5', + email: 'request5@gmail.com', + password: 'validpassword', + }); + + const secondFirewallEvent = await orchestrator.getLastEvent(); + + expect.soft(response1.status).toBe(201); + expect.soft(response2.status).toBe(201); + expect.soft(response3.status).toBe(429); + expect.soft(response4.status).toBe(201); + expect.soft(response5.status).toBe(429); + + expect(firstFirewallEvent).toStrictEqual({ + id: firstFirewallEvent.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: firstFirewallEvent.created_at, + }); + expect(uuidVersion(firstFirewallEvent.id)).toBe(4); + expect(Date.parse(firstFirewallEvent.created_at)).not.toBe(NaN); + + expect(secondFirewallEvent).toStrictEqual({ + id: secondFirewallEvent.id, + type: 'firewall:block_users', + originator_user_id: null, + originator_ip: '127.0.0.1', + metadata: { + from_rule: 'create:user', + users: [user2.id, user4.id], + }, + created_at: secondFirewallEvent.created_at, + }); + expect(uuidVersion(secondFirewallEvent.id)).toBe(4); + expect(Date.parse(secondFirewallEvent.created_at)).not.toBe(NaN); + + // Confirm first event + const reviewFirewallRequestBuilder = new RequestBuilder(`/api/v1/moderations/review_firewall`); + const firewallUser = await reviewFirewallRequestBuilder.buildUser({ + with: ['read:firewall', 'review:firewall'], + }); + + const { response: responseConfirm, responseBody: responseBodyConfirm } = + await reviewFirewallRequestBuilder.post(`/${firstFirewallEvent.id}`, { + action: 'confirm', + }); + + expect(responseConfirm.status).toEqual(200); + + expect(responseBodyConfirm).toStrictEqual({ + affected: { + users: [ + { + created_at: user1.created_at, + description: user1.description, + features: ['nuked'], + id: user1.id, + tabcash: user1.tabcash, + tabcoins: user1.tabcoins, + updated_at: user1.updated_at, + username: user1.username, + }, + { + created_at: user2.created_at, + description: user2.description, + features: ['nuked'], + id: user2.id, + tabcash: user2.tabcash, + tabcoins: user2.tabcoins, + updated_at: user2.updated_at, + username: user2.username, + }, + ], + }, + events: [ + { + created_at: firstFirewallEvent.created_at.toISOString(), + id: firstFirewallEvent.id, + metadata: firstFirewallEvent.metadata, + originator_user_id: firstFirewallEvent.originator_user_id, + type: firstFirewallEvent.type, + }, + { + created_at: responseBodyConfirm.events[1].created_at, + id: responseBodyConfirm.events[1].id, + metadata: { + original_event_id: firstFirewallEvent.id, + users: firstFirewallEvent.metadata.users, + }, + originator_user_id: firewallUser.id, + type: 'moderation:block_users', + }, + ], + }); + + const createdConfirmationEventResponse = responseBodyConfirm.events[1]; + expect(Date.parse(createdConfirmationEventResponse.created_at)).not.toEqual(NaN); + + // Check "confirm" event + const confirmationEvent = await orchestrator.getLastEvent(); + expect(confirmationEvent).toStrictEqual({ + created_at: expect.any(Date), + id: confirmationEvent.id, + metadata: { + original_event_id: firstFirewallEvent.id, + users: firstFirewallEvent.metadata.users, + }, + originator_ip: '127.0.0.1', + originator_user_id: firewallUser.id, + type: 'moderation:block_users', + }); + + expect(uuidVersion(confirmationEvent.id)).toEqual(4); + + // Undo second event + const { response: responseUndo, responseBody: responseBodyUndo } = await reviewFirewallRequestBuilder.post( + `/${secondFirewallEvent.id}`, + { action: 'undo' }, + ); + + expect(responseUndo.status).toEqual(200); + + expect(responseBodyUndo).toStrictEqual({ + affected: { + users: [ + { + created_at: user4.created_at, + description: user4.description, + features: responseBodyUndo.affected.users[0].features, + id: user4.id, + tabcash: user4.tabcash, + tabcoins: user4.tabcoins, + updated_at: user4.updated_at, + username: user4.username, + }, + { + created_at: user2.created_at, + description: user2.description, + features: responseBodyUndo.affected.users[1].features, + id: user2.id, + tabcash: user2.tabcash, + tabcoins: user2.tabcoins, + updated_at: user2.updated_at, + username: user2.username, + }, + ], + }, + events: [ + { + created_at: secondFirewallEvent.created_at.toISOString(), + id: secondFirewallEvent.id, + metadata: secondFirewallEvent.metadata, + originator_user_id: secondFirewallEvent.originator_user_id, + type: secondFirewallEvent.type, + }, + { + created_at: responseBodyUndo.events[1].created_at, + id: responseBodyUndo.events[1].id, + metadata: { + original_event_id: secondFirewallEvent.id, + users: secondFirewallEvent.metadata.users, + }, + originator_user_id: firewallUser.id, + type: 'moderation:unblock_users', + }, + ], + }); + + const createdEventResponse = responseBodyUndo.events[1]; + expect(Date.parse(createdEventResponse.created_at)).not.toEqual(NaN); + + const user4AfterUndoResponse = responseBodyUndo.affected.users[0]; + const user2AfterUndoResponse = responseBodyUndo.affected.users[1]; + + // User 2 is nuked because of the previous confirmed firewall event. + expect(user2AfterUndoResponse.features.sort()).toStrictEqual(['nuked', 'read:activation_token']); + expect(user4AfterUndoResponse.features.sort()).toStrictEqual(user4.features.sort()); + + // Check "undo" event + const undoEvent = await orchestrator.getLastEvent(); + expect(undoEvent).toStrictEqual({ + created_at: expect.any(Date), + id: undoEvent.id, + metadata: { + original_event_id: secondFirewallEvent.id, + users: secondFirewallEvent.metadata.users, + }, + originator_ip: '127.0.0.1', + originator_user_id: firewallUser.id, + type: 'moderation:unblock_users', + }); + + expect(uuidVersion(undoEvent.id)).toEqual(4); + }); + + test('With two "firewall:block_contents:text_root" events, "confirm" and "undo"', async () => { + const contentsRequestBuilder = new RequestBuilder('/api/v1/contents'); + const user1 = await contentsRequestBuilder.buildUser(); + + const { response: response1, responseBody: content1 } = await createContentViaApi(contentsRequestBuilder); + const firstCreateContentEvent = await orchestrator.getLastEvent(); + + const { response: response2, responseBody: content2 } = await createContentViaApi(contentsRequestBuilder); + await orchestrator.createRate(content2, 10); + + const { response: response3 } = await createContentViaApi(contentsRequestBuilder); + + const firstFirewallEvent = await orchestrator.getLastEvent(); + + await orchestrator.updateEventCreatedAt(firstCreateContentEvent.id, new Date(Date.now() - 30 * 60 * 1000)); + + const { response: response4, responseBody: content4 } = await createContentViaApi(contentsRequestBuilder); + const { response: response5 } = await createContentViaApi(contentsRequestBuilder); + + const secondFirewallEvent = await orchestrator.getLastEvent(); + + expect.soft(response1.status).toBe(201); + expect.soft(response2.status).toBe(201); + expect.soft(response3.status).toBe(429); + expect.soft(response4.status).toBe(201); + expect.soft(response5.status).toBe(429); + + // Confirm first event + const reviewFirewallRequestBuilder = new RequestBuilder(`/api/v1/moderations/review_firewall`); + const firewallUser = await reviewFirewallRequestBuilder.buildUser({ + with: ['read:firewall', 'review:firewall'], + }); + + const { response: responseConfirm, responseBody: responseBodyConfirm } = + await reviewFirewallRequestBuilder.post(`/${firstFirewallEvent.id}`, { action: 'confirm' }); + + expect(responseConfirm.status).toEqual(200); + + expect(responseBodyConfirm).toStrictEqual({ + affected: { + contents: [ + { + body: content1.body, + created_at: content1.created_at, + deleted_at: responseBodyConfirm.affected.contents[0].deleted_at, + id: content1.id, + owner_id: content1.owner_id, + parent_id: content1.parent_id, + published_at: content1.published_at, + slug: content1.slug, + source_url: content1.source_url, + status: 'deleted', + title: content1.title, + type: content1.type, + updated_at: content1.updated_at, + }, + { + body: content2.body, + created_at: content2.created_at, + deleted_at: responseBodyConfirm.affected.contents[1].deleted_at, + id: content2.id, + owner_id: content2.owner_id, + parent_id: content2.parent_id, + published_at: content2.published_at, + slug: content2.slug, + source_url: content2.source_url, + status: 'deleted', + title: content2.title, + type: content2.type, + updated_at: content2.updated_at, + }, + ], + users: [ + { + created_at: user1.created_at.toISOString(), + description: user1.description, + features: user1.features, + id: user1.id, + tabcash: 0, + tabcoins: 0, + updated_at: user1.updated_at.toISOString(), + username: user1.username, + }, + ], + }, + events: [ + { + created_at: firstFirewallEvent.created_at.toISOString(), + id: firstFirewallEvent.id, + metadata: firstFirewallEvent.metadata, + originator_user_id: firstFirewallEvent.originator_user_id, + type: firstFirewallEvent.type, + }, + { + created_at: responseBodyConfirm.events[1].created_at, + id: responseBodyConfirm.events[1].id, + metadata: { + original_event_id: firstFirewallEvent.id, + contents: firstFirewallEvent.metadata.contents, + }, + originator_user_id: firewallUser.id, + type: 'moderation:block_contents:text_root', + }, + ], + }); + + // Check "confirm" event + const confirmationEvent = await orchestrator.getLastEvent(); + expect(confirmationEvent).toStrictEqual({ + created_at: expect.any(Date), + id: confirmationEvent.id, + metadata: { + original_event_id: firstFirewallEvent.id, + contents: firstFirewallEvent.metadata.contents, + }, + originator_ip: '127.0.0.1', + originator_user_id: firewallUser.id, + type: 'moderation:block_contents:text_root', + }); + + expect(uuidVersion(confirmationEvent.id)).toEqual(4); + + // Undo second event + const { response: responseUndo, responseBody: responseBodyUndo } = await reviewFirewallRequestBuilder.post( + `/${secondFirewallEvent.id}`, + { action: 'undo' }, + ); + + expect(responseUndo.status).toEqual(200); + + expect(responseBodyUndo).toStrictEqual({ + affected: { + contents: [ + { + body: content4.body, + created_at: content4.created_at, + deleted_at: content4.deleted_at, + id: content4.id, + owner_id: content4.owner_id, + parent_id: content4.parent_id, + published_at: content4.published_at, + slug: content4.slug, + source_url: content4.source_url, + status: content4.status, + title: content4.title, + type: content4.type, + updated_at: content4.updated_at, + }, + { + body: content2.body, + created_at: content2.created_at, + deleted_at: responseBodyUndo.affected.contents[1].deleted_at, + id: content2.id, + owner_id: content2.owner_id, + parent_id: content2.parent_id, + published_at: content2.published_at, + slug: content2.slug, + source_url: content2.source_url, + status: 'deleted', + title: content2.title, + type: content2.type, + updated_at: content2.updated_at, + }, + ], + users: [ + { + created_at: user1.created_at.toISOString(), + description: user1.description, + features: user1.features, + id: user1.id, + tabcash: 0, + tabcoins: 0, + updated_at: user1.updated_at.toISOString(), + username: user1.username, + }, + ], + }, + events: [ + { + created_at: secondFirewallEvent.created_at.toISOString(), + id: secondFirewallEvent.id, + metadata: secondFirewallEvent.metadata, + originator_user_id: secondFirewallEvent.originator_user_id, + type: secondFirewallEvent.type, + }, + { + created_at: responseBodyUndo.events[1].created_at, + id: responseBodyUndo.events[1].id, + metadata: { + original_event_id: secondFirewallEvent.id, + contents: secondFirewallEvent.metadata.contents, + }, + originator_user_id: firewallUser.id, + type: 'moderation:unblock_contents:text_root', + }, + ], + }); + + const createdEventResponse = responseBodyUndo.events[1]; + expect(Date.parse(createdEventResponse.created_at)).not.toEqual(NaN); + + // Check "undo" event + const undoEvent = await orchestrator.getLastEvent(); + expect(undoEvent).toStrictEqual({ + created_at: expect.any(Date), + id: undoEvent.id, + metadata: { + original_event_id: secondFirewallEvent.id, + contents: secondFirewallEvent.metadata.contents, + }, + originator_ip: '127.0.0.1', + originator_user_id: firewallUser.id, + type: 'moderation:unblock_contents:text_root', + }); + + expect(uuidVersion(undoEvent.id)).toEqual(4); + }); + }); }); }); diff --git a/tests/orchestrator.js b/tests/orchestrator.js index d1c62eea5..19cf5065b 100644 --- a/tests/orchestrator.js +++ b/tests/orchestrator.js @@ -420,6 +420,15 @@ async function getLastEvent() { return results.rows[0]; } +async function updateEventCreatedAt(id, createdAt) { + const query = { + text: 'UPDATE events SET created_at = $1 WHERE id = $2;', + values: [createdAt, id], + }; + const results = await database.query(query); + return results.rows[0]; +} + function parseSetCookies(response) { const setCookieHeaderValues = response.headers.get('set-cookie'); const parsedCookies = setCookieParser.parse(setCookieHeaderValues, { map: true }); @@ -447,6 +456,7 @@ const orchestrator = { removeFeaturesFromUser, runPendingMigrations, updateContent, + updateEventCreatedAt, updateRewardedAt, waitForAllServices, webserverUrl,