From 78d593c012d3a6961ff22ba6696c9618dfbb1018 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Fri, 24 Jan 2025 16:22:08 -0300 Subject: [PATCH 01/26] feat: Migrations and initial db queries --- src/adapters/db.ts | 53 ++++++++++++- src/migrations/1737745803297_add-blocks.ts | 35 +++++++++ src/types.ts | 10 +++ test/mocks/components/db.ts | 5 ++ test/unit/adapters/db.spec.ts | 87 ++++++++++++++++++++++ 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/migrations/1737745803297_add-blocks.ts diff --git a/src/adapters/db.ts b/src/adapters/db.ts index e2569f1..96e7aa2 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -1,7 +1,15 @@ import SQL, { SQLStatement } from 'sql-template-strings' import { randomUUID } from 'node:crypto' import { PoolClient } from 'pg' -import { AppComponents, Friendship, FriendshipAction, FriendshipRequest, IDatabaseComponent, Friend } from '../types' +import { + AppComponents, + Friendship, + FriendshipAction, + FriendshipRequest, + IDatabaseComponent, + Friend, + BlockedUser +} from '../types' import { FRIENDSHIPS_PER_PAGE } from './rpc-server/constants' import { normalizeAddress } from '../utils/address' @@ -331,6 +339,49 @@ export function createDBComponent(components: Pick const results = await pg.query(query) return results.rows }, + async blockUser(blockerAddress, blockedAddress) { + const query = SQL` + INSERT INTO blocks (id, blocker_address, blocked_address) + VALUES (${randomUUID()}, ${normalizeAddress(blockerAddress)}, ${normalizeAddress(blockedAddress)}) + ON CONFLICT DO NOTHING` + await pg.query(query) + }, + async unblockUser(blockerAddress, blockedAddress) { + const query = SQL` + DELETE FROM blocks + WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} + AND LOWER(blocked_address) = ${normalizeAddress(blockedAddress)} + ` + await pg.query(query) + }, + async blockUsers(blockerAddress, blockedAddresses) { + const query = SQL`INSERT INTO blocks (id, blocker_address, blocked_address) VALUES ` + + blockedAddresses.forEach((blockedAddress, index) => { + query.append(SQL`(${randomUUID()}, ${normalizeAddress(blockerAddress)}, ${normalizeAddress(blockedAddress)})`) + if (index < blockedAddresses.length - 1) { + query.append(SQL`, `) + } + }) + + query.append(SQL` ON CONFLICT DO NOTHING`) + await pg.query(query) + }, + async unblockUsers(blockerAddress, blockedAddresses) { + const query = SQL` + DELETE FROM blocks + WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} + AND LOWER(blocked_address) IN (${blockedAddresses.map(normalizeAddress)}) + ` + await pg.query(query) + }, + async getBlockedUsers(blockerAddress) { + const query = SQL` + SELECT blocked_address as address FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} + ` + const results = await pg.query(query) + return results.rows.map((row) => row.address) + }, async executeTx(cb: (client: PoolClient) => Promise): Promise { const pool = pg.getPool() const client = await pool.connect() diff --git a/src/migrations/1737745803297_add-blocks.ts b/src/migrations/1737745803297_add-blocks.ts new file mode 100644 index 0000000..bb604c7 --- /dev/null +++ b/src/migrations/1737745803297_add-blocks.ts @@ -0,0 +1,35 @@ +import { MigrationBuilder, ColumnDefinitions, PgType } from 'node-pg-migrate' + +export const shorthands: ColumnDefinitions | undefined = undefined + +export async function up(pgm: MigrationBuilder): Promise { + pgm.createTable('blocks', { + id: { + type: PgType.UUID, + primaryKey: true + }, + blocker_address: { + type: PgType.VARCHAR, + notNull: true + }, + blocked_address: { + type: PgType.VARCHAR, + notNull: true + }, + blocked_at: { + type: PgType.TIMESTAMP, + default: pgm.func('now()') + } + }) + + pgm.createIndex('blocks', ['blocker_address']) + pgm.createIndex('blocks', ['blocked_address']) + pgm.createIndex('blocks', ['blocker_address', 'blocked_address'], { unique: true }) +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropTable('blocks') + pgm.dropIndex('blocks', ['blocker_address']) + pgm.dropIndex('blocks', ['blocked_address']) + pgm.dropIndex('blocks', ['blocker_address', 'blocked_address']) +} diff --git a/src/types.ts b/src/types.ts index 74a48c9..d6d0318 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,11 @@ export interface IDatabaseComponent { getReceivedFriendshipRequests(userAddress: string, pagination?: Pagination): Promise getSentFriendshipRequests(userAddress: string, pagination?: Pagination): Promise getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise + blockUser(blockerAddress: string, blockedAddress: string): Promise + unblockUser(blockerAddress: string, blockedAddress: string): Promise + blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise + unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise + getBlockedUsers(blockerAddress: string): Promise executeTx(cb: (client: PoolClient) => Promise): Promise } export interface IRedisComponent extends IBaseComponent { @@ -225,6 +230,11 @@ export type Friend = { address: string } +export type BlockedUser = { + address: string + blocked_at: string +} + export enum Action { REQUEST = 'request', // request a friendship CANCEL = 'cancel', // cancel a friendship request diff --git a/test/mocks/components/db.ts b/test/mocks/components/db.ts index 10abe83..5d5a0fc 100644 --- a/test/mocks/components/db.ts +++ b/test/mocks/components/db.ts @@ -14,5 +14,10 @@ export const mockDb: jest.Mocked = { recordFriendshipAction: jest.fn(), getReceivedFriendshipRequests: jest.fn(), getSentFriendshipRequests: jest.fn(), + getBlockedUsers: jest.fn(), + blockUser: jest.fn(), + unblockUser: jest.fn(), + blockUsers: jest.fn(), + unblockUsers: jest.fn(), executeTx: jest.fn() } diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 7802d21..ded7f70 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -381,6 +381,93 @@ describe('db', () => { }) }) + describe('blockUser', () => { + it('should block a user', async () => { + await dbComponent.blockUser('0x123', '0x456') + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('INSERT INTO blocks (id, blocker_address, blocked_address)'), + values: expect.arrayContaining([expect.any(String), normalizeAddress('0x123'), normalizeAddress('0x456')]) + }) + ) + + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('ON CONFLICT DO NOTHING') + }) + ) + }) + }) + + describe('unblockUser', () => { + it('should unblock a user', async () => { + await dbComponent.unblockUser('0x123', '0x456') + const expectedQuery = SQL` + DELETE FROM blocks + WHERE LOWER(blocker_address) = ${normalizeAddress('0x123')} + AND LOWER(blocked_address) = ${normalizeAddress('0x456')}` + + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining(expectedQuery.text), + values: expectedQuery.values + }) + ) + }) + }) + + describe('blockUsers', () => { + it('should block multiple users', async () => { + await dbComponent.blockUsers('0x123', ['0x456', '0x789']) + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('INSERT INTO blocks (id, blocker_address, blocked_address)'), + values: expect.arrayContaining([ + expect.any(String), + normalizeAddress('0x123'), + normalizeAddress('0x456'), + expect.any(String), + normalizeAddress('0x123'), + normalizeAddress('0x789') + ]) + }) + ) + }) + }) + + describe('unblockUsers', () => { + it('should unblock multiple users', async () => { + await dbComponent.unblockUsers('0x123', ['0x456', '0x789']) + const expectedQuery = SQL` + DELETE FROM blocks + WHERE LOWER(blocker_address) = ${normalizeAddress('0x123')} + AND LOWER(blocked_address) IN (${['0x456', '0x789'].map(normalizeAddress)})` + + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining(expectedQuery.text), + values: expectedQuery.values + }) + ) + }) + }) + + describe('getBlockedUsers', () => { + it('should retrieve blocked users', async () => { + const mockBlockedUsers = [{ address: '0x456' }, { address: '0x789' }] + mockPg.query.mockResolvedValueOnce({ rows: mockBlockedUsers, rowCount: mockBlockedUsers.length }) + + const result = await dbComponent.getBlockedUsers('0x123') + expect(result).toEqual(mockBlockedUsers.map((user) => user.address)) + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('LOWER(blocker_address) ='), + values: expect.arrayContaining([normalizeAddress('0x123')]) + }) + ) + }) + }) + describe('executeTx', () => { it('should execute a transaction successfully', async () => { const result = await dbComponent.executeTx(async (client) => { From 5b94adab4235b021a220e64a5395f6bf8cef46b6 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 25 Feb 2025 15:31:36 +0000 Subject: [PATCH 02/26] feat: New queries abstractions --- src/adapters/db.ts | 237 ++++------------------------------ src/logic/queries.ts | 190 +++++++++++++++++++++++++++ test/unit/adapters/db.spec.ts | 134 ++++++++++++++++--- 3 files changed, 335 insertions(+), 226 deletions(-) create mode 100644 src/logic/queries.ts diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 4954f65..a795b3a 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -9,220 +9,55 @@ import { IDatabaseComponent, Friend, BlockedUser, - Pagination, - Action + Pagination } from '../types' import { FRIENDSHIPS_PER_PAGE } from './rpc-server/constants' import { normalizeAddress } from '../utils/address' +import { getFriendsBaseQuery, getFriendshipRequestsBaseQuery, getMutualFriendsBaseQuery } from '../logic/queries' + +type FriendshipRequestType = 'sent' | 'received' export function createDBComponent(components: Pick): IDatabaseComponent { const { pg, logs } = components const logger = logs.getLogger('db-component') - // TODO: abstract common statements in a util file - function getFriendsBaseQuery(userAddress: string) { - const normalizedUserAddress = normalizeAddress(userAddress) - return SQL` - SELECT DISTINCT - CASE - WHEN LOWER(address_requester) = ${normalizedUserAddress} THEN address_requested - ELSE address_requester - END as address, - created_at - FROM friendships - WHERE (LOWER(address_requester) = ${normalizedUserAddress} OR LOWER(address_requested) = ${normalizedUserAddress})` + async function getCount(query: SQLStatement) { + const result = await pg.query<{ count: number }>(query) + return result.rows[0].count } - function getFriendshipRequestsBaseQuery( - userAddress: string, - type: 'sent' | 'received', - { onlyCount, pagination }: { onlyCount?: boolean; pagination?: Pagination } = { onlyCount: false } - ): SQLStatement { - const { limit, offset } = pagination || {} - const normalizedUserAddress = normalizeAddress(userAddress) - - const columnMapping = { - sent: SQL` - CASE - WHEN LOWER(f.address_requester) = lr.acting_user THEN LOWER(f.address_requested) - ELSE LOWER(f.address_requester) - END`, - received: SQL` LOWER(lr.acting_user)` - } - - const filterMapping = { - sent: SQL` LOWER(lr.acting_user) = ${normalizedUserAddress}`, - received: SQL` LOWER(lr.acting_user) <> ${normalizedUserAddress} AND (LOWER(f.address_requester) = ${normalizedUserAddress} OR LOWER(f.address_requested) = ${normalizedUserAddress})` - } - - const baseQuery = SQL`WITH latest_requests AS ( - SELECT DISTINCT ON (friendship_id) * - FROM friendship_actions - ORDER BY friendship_id, timestamp DESC - ) SELECT` - - if (onlyCount) { - baseQuery.append(SQL` DISTINCT COUNT(1) as count`) - } else { - baseQuery.append(SQL` lr.id,`) - baseQuery.append(columnMapping[type]) - baseQuery.append(SQL` as address, lr.timestamp, lr.metadata`) + function getFriendshipRequests(type: FriendshipRequestType) { + return async (userAddress: string, pagination?: Pagination) => { + const query = getFriendshipRequestsBaseQuery(userAddress, type, { pagination }) + const result = await pg.query(query) + return result.rows } + } - baseQuery.append(SQL` FROM friendships f`) - baseQuery.append(SQL` INNER JOIN latest_requests lr ON f.id = lr.friendship_id`) - baseQuery.append(SQL` WHERE`) - baseQuery.append(filterMapping[type]) - baseQuery.append(SQL` AND action = ${Action.REQUEST}`) - - baseQuery.append(SQL` AND f.is_active IS FALSE`) - - if (!onlyCount) { - baseQuery.append(SQL` ORDER BY lr.timestamp DESC`) - - if (limit) { - baseQuery.append(SQL` LIMIT ${limit}`) - } - - if (offset) { - baseQuery.append(SQL` OFFSET ${offset}`) - } + function getFriendshipRequestsCount(type: FriendshipRequestType) { + return async (userAddress: string) => { + const query = getFriendshipRequestsBaseQuery(userAddress, type, { onlyCount: true }) + return getCount(query) } - - return baseQuery } return { - async getFriends(userAddress, { onlyActive = true, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 } } = {}) { - const { limit, offset } = pagination - - const query: SQLStatement = getFriendsBaseQuery(userAddress) - - if (onlyActive) { - query.append(SQL` AND is_active = true`) - } - - query.append(SQL` ORDER BY created_at DESC OFFSET ${offset} LIMIT ${limit}`) - + async getFriends(userAddress, { onlyActive, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 } } = {}) { + const query: SQLStatement = getFriendsBaseQuery(userAddress, { onlyActive, pagination }) const result = await pg.query(query) return result.rows }, async getFriendsCount(userAddress, { onlyActive } = { onlyActive: true }) { - const normalizedUserAddress = normalizeAddress(userAddress) - const query: SQLStatement = SQL` - SELECT COUNT(*) - FROM friendships - WHERE (LOWER(address_requester) = ${normalizedUserAddress} OR LOWER(address_requested) = ${normalizedUserAddress})` - - if (onlyActive) { - query.append(SQL` AND is_active = true`) - } - - const result = await pg.query<{ count: number }>(query) - return result.rows[0].count + const query: SQLStatement = getFriendsBaseQuery(userAddress, { onlyActive, onlyCount: true }) + return getCount(query) }, async getMutualFriends(userAddress1, userAddress2, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 }) { - const { limit, offset } = pagination - - const normalizedUserAddress1 = normalizeAddress(userAddress1) - const normalizedUserAddress2 = normalizeAddress(userAddress2) - - const result = await pg.query( - SQL`WITH friendsA as ( - SELECT - CASE - WHEN LOWER(address_requester) = ${normalizedUserAddress1} then address_requested - else address_requester - end as address - FROM - ( - SELECT f_a.* - FROM friendships f_a - WHERE - ( - LOWER(f_a.address_requester) = ${normalizedUserAddress1} or LOWER(f_a.address_requested) = ${normalizedUserAddress1} - ) AND f_a.is_active = true - ) as friends_a - ) - SELECT - f_b.address - FROM - friendsA f_b - WHERE - f_b.address IN ( - SELECT - CASE - WHEN LOWER(address_requester) = ${normalizedUserAddress2} then address_requested - else address_requester - end as address_a - FROM - ( - SELECT - f_b.* - FROM - friendships f_b - WHERE - ( - LOWER(f_b.address_requester) = ${normalizedUserAddress2} - or LOWER(f_b.address_requested) = ${normalizedUserAddress2} - ) AND f_b.is_active = true - ) as friends_b - ) - ORDER BY f_b.address - LIMIT ${limit} - OFFSET ${offset}` - ) - + const result = await pg.query(getMutualFriendsBaseQuery(userAddress1, userAddress2, { pagination })) return result.rows }, async getMutualFriendsCount(userAddress1, userAddress2) { - const normalizedUserAddress1 = normalizeAddress(userAddress1) - const normalizedUserAddress2 = normalizeAddress(userAddress2) - - const result = await pg.query<{ count: number }>( - SQL`WITH friendsA as ( - SELECT - CASE - WHEN LOWER(address_requester) = ${normalizedUserAddress1} THEN address_requested - ELSE address_requester - END as address - FROM - ( - SELECT f_a.* - FROM friendships f_a - WHERE - ( - LOWER(f_a.address_requester) = ${normalizedUserAddress1} - OR LOWER(f_a.address_requested) = ${normalizedUserAddress1} - ) AND f_a.is_active = true - ) as friends_a - ) - SELECT - COUNT(address) - FROM - friendsA f_b - WHERE - address IN ( - SELECT - CASE - WHEN address_requester = ${normalizedUserAddress2} THEN address_requested - ELSE address_requester - END as address_a - FROM - ( - SELECT f_b.* - FROM friendships f_b - WHERE - ( - f_b.address_requester = ${normalizedUserAddress2} - OR f_b.address_requested = ${normalizedUserAddress2} - ) AND f_b.is_active = true - ) as friends_b - )` - ) - - return result.rows[0].count + return getCount(getMutualFriendsBaseQuery(userAddress1, userAddress2, { onlyCount: true })) }, async getFriendship(users) { const [userAddress1, userAddress2] = users.map(normalizeAddress) @@ -307,26 +142,10 @@ export function createDBComponent(components: Pick return uuid }, - async getReceivedFriendshipRequests(userAddress, pagination) { - const query = getFriendshipRequestsBaseQuery(userAddress, 'received', { pagination }) - const results = await pg.query(query) - return results.rows - }, - async getReceivedFriendshipRequestsCount(userAddress) { - const query = getFriendshipRequestsBaseQuery(userAddress, 'received', { onlyCount: true }) - const results = await pg.query<{ count: number }>(query) - return results.rows[0].count - }, - async getSentFriendshipRequests(userAddress, pagination) { - const query = getFriendshipRequestsBaseQuery(userAddress, 'sent', { pagination }) - const results = await pg.query(query) - return results.rows - }, - async getSentFriendshipRequestsCount(userAddress) { - const query = getFriendshipRequestsBaseQuery(userAddress, 'sent', { onlyCount: true }) - const results = await pg.query<{ count: number }>(query) - return results.rows[0].count - }, + getReceivedFriendshipRequests: getFriendshipRequests('received'), + getReceivedFriendshipRequestsCount: getFriendshipRequestsCount('received'), + getSentFriendshipRequests: getFriendshipRequests('sent'), + getSentFriendshipRequestsCount: getFriendshipRequestsCount('sent'), async getOnlineFriends(userAddress: string, onlinePotentialFriends: string[]) { if (onlinePotentialFriends.length === 0) return [] @@ -382,7 +201,7 @@ export function createDBComponent(components: Pick const query = SQL` DELETE FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} - AND LOWER(blocked_address) IN (${blockedAddresses.map(normalizeAddress)}) + AND LOWER(blocked_address) = ANY(${blockedAddresses.map(normalizeAddress)}) ` await pg.query(query) }, diff --git a/src/logic/queries.ts b/src/logic/queries.ts new file mode 100644 index 0000000..b7dc012 --- /dev/null +++ b/src/logic/queries.ts @@ -0,0 +1,190 @@ +import SQL, { SQLStatement } from 'sql-template-strings' +import { normalizeAddress } from '../utils/address' +import { Action, Pagination } from '../types' + +function withAlias(tableAlias?: string): string { + return tableAlias ? `${tableAlias}.` : `` +} + +export function getFriendAddressCase(userAddress: string, tableAlias?: string): SQLStatement { + const normalizedUserAddress = normalizeAddress(userAddress) + return SQL`CASE + WHEN LOWER(` + .append(withAlias(tableAlias)) + .append(SQL`address_requester) = ${normalizedUserAddress} THEN `) + .append(withAlias(tableAlias)) + .append( + SQL`address_requested + ELSE ` + ) + .append(withAlias(tableAlias)).append(SQL`address_requester + END`) +} + +export function getFriendshipCondition(userAddress: string, tableAlias: string): SQLStatement { + const normalizedUserAddress = normalizeAddress(userAddress) + return SQL`(LOWER(` + .append(tableAlias) + .append(SQL`.address_requester) = ${normalizedUserAddress} OR LOWER(`) + .append(tableAlias) + .append(SQL`.address_requested) = ${normalizedUserAddress})`) +} + +export function getBlockingCondition( + userAddress: string, + friendAddressCase: SQLStatement = getFriendAddressCase(userAddress, 'f') +): SQLStatement { + const normalizedUserAddress = normalizeAddress(userAddress) + const query = SQL`NOT EXISTS (` + .append(SQL`SELECT 1 FROM blocks b`) + .append(SQL` WHERE (b.blocker_address = ${normalizedUserAddress} AND b.blocked_address = `) + .append(friendAddressCase) + .append(SQL`) OR (b.blocked_address = ${normalizedUserAddress} AND b.blocker_address = `) + .append(friendAddressCase) + .append(SQL`))`) + return query +} + +export function getFriendsBaseQuery( + userAddress: string, + options: { onlyActive?: boolean; pagination?: Pagination; onlyCount?: boolean } = { + onlyActive: true, + onlyCount: false + } +) { + const friendAddressCase = getFriendAddressCase(userAddress, 'f') + + const { onlyActive, onlyCount, pagination } = options + + const query: SQLStatement = SQL`SELECT DISTINCT ` + + if (onlyCount) { + query.append(SQL`COUNT(*)`) + } else { + query.append(friendAddressCase).append(SQL` as address, created_at`) + } + + query.append(SQL` FROM friendships f`) + query.append(SQL` WHERE `).append(getFriendshipCondition(userAddress, 'f')) + + if (onlyActive) { + query.append(SQL` AND f.is_active = true`) + } + + query.append(SQL` AND `).append(getBlockingCondition(userAddress, friendAddressCase)) + + if (!onlyCount && pagination) { + const { offset, limit } = pagination + query.append(SQL` ORDER BY f.created_at DESC OFFSET ${offset} LIMIT ${limit}`) + } + + return query +} + +export function getFriendshipRequestsBaseQuery( + userAddress: string, + type: 'sent' | 'received', + { onlyCount, pagination }: { onlyCount?: boolean; pagination?: Pagination } = { onlyCount: false } +): SQLStatement { + const { limit, offset } = pagination || {} + const normalizedUserAddress = normalizeAddress(userAddress) + + const columnMapping = { + sent: SQL` + CASE + WHEN LOWER(f.address_requester) = lr.acting_user THEN LOWER(f.address_requested) + ELSE LOWER(f.address_requester) + END`, + received: SQL` LOWER(lr.acting_user)` + } + + const filterMapping = { + sent: SQL` LOWER(lr.acting_user) = ${normalizedUserAddress}`, + received: SQL` LOWER(lr.acting_user) <> ${normalizedUserAddress} AND (LOWER(f.address_requester) = ${normalizedUserAddress} OR LOWER(f.address_requested) = ${normalizedUserAddress})` + } + + const baseQuery = SQL`WITH latest_requests AS ( + SELECT DISTINCT ON (friendship_id) * + FROM friendship_actions + ORDER BY friendship_id, timestamp DESC + ) SELECT` + + if (onlyCount) { + baseQuery.append(SQL` DISTINCT COUNT(1) as count`) + } else { + baseQuery.append(SQL` lr.id,`) + baseQuery.append(columnMapping[type]) + baseQuery.append(SQL` as address, lr.timestamp, lr.metadata`) + } + + baseQuery.append(SQL` FROM friendships f`) + baseQuery.append(SQL` INNER JOIN latest_requests lr ON f.id = lr.friendship_id`) + baseQuery.append(SQL` WHERE`) + baseQuery.append(filterMapping[type]) + baseQuery.append(SQL` AND action = ${Action.REQUEST}`) + + baseQuery.append(SQL` AND f.is_active IS FALSE`) + + if (!onlyCount) { + baseQuery.append(SQL` ORDER BY lr.timestamp DESC`) + + if (limit) { + baseQuery.append(SQL` LIMIT ${limit}`) + } + + if (offset) { + baseQuery.append(SQL` OFFSET ${offset}`) + } + } + + return baseQuery +} + +export function getMutualFriendsBaseQuery( + userAddress1: string, + userAddress2: string, + options: { pagination?: Pagination; onlyCount?: boolean } = { onlyCount: false } +): SQLStatement { + const normalizedUserAddress1 = normalizeAddress(userAddress1) + const normalizedUserAddress2 = normalizeAddress(userAddress2) + const { pagination, onlyCount } = options + + const friendsSubquery = (address: string, tableAlias: string) => + SQL` + SELECT ` + .append(getFriendAddressCase(address)) + .append( + SQL` as address + FROM friendships ` + ) + .append(tableAlias) + .append(SQL` WHERE `) + .append(getFriendshipCondition(address, tableAlias)) + .append(SQL` AND `) + .append(tableAlias).append(SQL`.is_active = true + `) + + const query = SQL`WITH friendsA as (`.append(friendsSubquery(normalizedUserAddress1, 'f_a')).append(SQL`) SELECT `) + + if (onlyCount) { + query.append(SQL`COUNT(address)`) + } else { + query.append(SQL`f_b.address`) + } + + query + .append(SQL` FROM friendsA f_b WHERE f_b.address IN (`) + .append(friendsSubquery(normalizedUserAddress2, 'f_b')) + .append(SQL`)`) + + if (!onlyCount) { + query.append(SQL` ORDER BY f_b.address`) + + if (pagination) { + const { limit, offset } = pagination + query.append(SQL` LIMIT ${limit} OFFSET ${offset}`) + } + } + + return query +} diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 5bfe5db..090aa75 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -28,23 +28,23 @@ describe('db', () => { const expectedFragmentsOfTheQuery = [ { - text: 'WHEN LOWER(address_requester) =', + text: 'WHEN LOWER(f.address_requester) =', values: ['0x123'] }, { - text: 'WHERE (LOWER(address_requester) =', + text: 'WHERE (LOWER(f.address_requester) =', values: ['0x123'] }, { - text: 'OR LOWER(address_requested) =', + text: 'OR LOWER(f.address_requested) =', values: ['0x123'] }, { - text: 'AND is_active = true', + text: 'AND f.is_active = true', values: [] }, { - text: 'ORDER BY created_at DESC', + text: 'ORDER BY f.created_at DESC', values: [] }, { @@ -93,12 +93,12 @@ describe('db', () => { const result = await dbComponent.getFriendsCount('0x123', { onlyActive: true }) - const expectedQuery = SQL`WHERE (LOWER(address_requester) = ${'0x123'} OR LOWER(address_requested) = ${'0x123'}) AND is_active = true` + const expectedQuery = SQL`WHERE (LOWER(f.address_requester) = ${'0x123'} OR LOWER(f.address_requested) = ${'0x123'}) AND f.is_active = true` expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining(expectedQuery.text), - values: expectedQuery.values + values: expect.arrayContaining(expectedQuery.values) }) ) expect(result).toBe(mockCount) @@ -110,12 +110,12 @@ describe('db', () => { const result = await dbComponent.getFriendsCount('0x123', { onlyActive: false }) - const expectedQuery = SQL`WHERE (LOWER(address_requester) = ${'0x123'} OR LOWER(address_requested) = ${'0x123'})` + const expectedQuery = SQL`WHERE (LOWER(f.address_requester) = ${'0x123'} OR LOWER(f.address_requested) = ${'0x123'})` expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining(expectedQuery.text), - values: expectedQuery.values + values: expect.arrayContaining(expectedQuery.values) }) ) expect(result).toBe(mockCount) @@ -123,28 +123,128 @@ describe('db', () => { }) describe('getMutualFriends', () => { - // TODO improve this test - it('should return mutual friends', async () => { + it('should return mutual friends with proper query structure', async () => { const mockMutualFriends = [{ address: '0x789' }] mockPg.query.mockResolvedValueOnce({ rows: mockMutualFriends, rowCount: mockMutualFriends.length }) - const result = await dbComponent.getMutualFriends('0x123', '0x456') + const result = await dbComponent.getMutualFriends('0x123', '0x456', { limit: 10, offset: 5 }) expect(result).toEqual(mockMutualFriends) - expect(mockPg.query).toHaveBeenCalled() + + const expectedQueryFragments = [ + { + text: 'WHEN LOWER(address_requester) =', + values: ['0x123'] + }, + { + text: 'LOWER(f_a.address_requester) =', + values: ['0x123'] + }, + { + text: 'LOWER(f_a.address_requested) =', + values: ['0x123'] + }, + { + text: 'LOWER(f_b.address_requester) =', + values: ['0x456'] + }, + { + text: 'LOWER(f_b.address_requested) =', + values: ['0x456'] + }, + { + text: 'is_active = true', + values: [] + }, + { + text: 'ORDER BY f_b.address', + values: [] + }, + { + text: 'LIMIT', + values: [expect.any(Number)] + }, + { + text: 'OFFSET', + values: [expect.any(Number)] + } + ] + + expectedQueryFragments.forEach(({ text, values }) => { + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining(text), + values: expect.arrayContaining(values) + }) + ) + }) + }) + + it('should handle empty results', async () => { + mockPg.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }) + + const result = await dbComponent.getMutualFriends('0x123', '0x456') + + expect(result).toEqual([]) }) }) describe('getMutualFriendsCount', () => { - // TODO improve this test - it('should return the count of mutual friends', async () => { + it('should return the count of mutual friends with proper query structure', async () => { const mockCount = 3 mockPg.query.mockResolvedValueOnce({ rows: [{ count: mockCount }], rowCount: 1 }) const result = await dbComponent.getMutualFriendsCount('0x123', '0x456') expect(result).toBe(mockCount) - expect(mockPg.query).toHaveBeenCalled() + + const expectedQueryFragments = [ + { + text: 'WITH friendsA as', + values: [] + }, + { + text: 'COUNT(address)', + values: [] + }, + { + text: 'WHEN LOWER(address_requester) =', + values: ['0x123'] + }, + { + text: 'is_active = true', + values: [] + } + ] + + expectedQueryFragments.forEach(({ text, values }) => { + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining(text), + values: expect.arrayContaining(values) + }) + ) + }) + }) + + it('should handle zero mutual friends', async () => { + mockPg.query.mockResolvedValueOnce({ rows: [{ count: 0 }], rowCount: 1 }) + + const result = await dbComponent.getMutualFriendsCount('0x123', '0x456') + + expect(result).toBe(0) + }) + + it('should normalize addresses in the query', async () => { + mockPg.query.mockResolvedValueOnce({ rows: [{ count: 1 }], rowCount: 1 }) + + await dbComponent.getMutualFriendsCount('0x123ABC', '0x456DEF') + + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + values: expect.arrayContaining(['0x123abc', '0x456def']) + }) + ) }) }) @@ -500,7 +600,7 @@ describe('db', () => { const expectedQuery = SQL` DELETE FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress('0x123')} - AND LOWER(blocked_address) IN (${['0x456', '0x789'].map(normalizeAddress)})` + AND LOWER(blocked_address) = ANY(${['0x456', '0x789'].map(normalizeAddress)})` expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ From 8c3c926eeb24bf18cb7140579ab163ca1c2bce57 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 25 Feb 2025 15:38:38 +0000 Subject: [PATCH 03/26] feat: Initial approach to block, unblock, and get blocked users --- .../rpc-server/services/block-user.ts | 66 +++++++++++++++ .../rpc-server/services/get-blocked-users.ts | 44 ++++++++++ .../rpc-server/services/unblock-user.ts | 61 ++++++++++++++ .../rpc-server/services/block-user.spec.ts | 83 +++++++++++++++++++ .../services/get-blocked-users.spec.ts | 75 +++++++++++++++++ .../rpc-server/services/unblock-user.spec.ts | 83 +++++++++++++++++++ 6 files changed, 412 insertions(+) create mode 100644 src/adapters/rpc-server/services/block-user.ts create mode 100644 src/adapters/rpc-server/services/get-blocked-users.ts create mode 100644 src/adapters/rpc-server/services/unblock-user.ts create mode 100644 test/unit/adapters/rpc-server/services/block-user.spec.ts create mode 100644 test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts create mode 100644 test/unit/adapters/rpc-server/services/unblock-user.spec.ts diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts new file mode 100644 index 0000000..308e18c --- /dev/null +++ b/src/adapters/rpc-server/services/block-user.ts @@ -0,0 +1,66 @@ +import { parseProfileToFriend } from '../../../logic/friends' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { + BlockUserPayload, + BlockUserResponse +} from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' + +export function blockUserService({ + components: { logs, db, catalystClient } +}: RPCServiceContext<'logs' | 'db' | 'catalystClient'>) { + const logger = logs.getLogger('block-user-service') + + return async function (request: BlockUserPayload, context: RpcServerContext): Promise { + try { + const { address: blockerAddress } = context + const blockedAddress = request.user?.address + + if (!blockedAddress) { + return { + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'User address is missing in the request payload' } + } + } + } + + const profile = await catalystClient.getProfile(blockedAddress) + + if (!profile) { + return { + response: { + $case: 'invalidRequest', + invalidRequest: { + message: 'Profile not found' + } + } + } + } + + await db.blockUser(blockerAddress, blockedAddress) + + return { + response: { + $case: 'blocked', + blocked: { + profile: parseProfileToFriend(profile) + } + } + } + } catch (error: any) { + logger.error(`Error blocking user: ${error.message}`, { + error: error.message, + stack: error.stack + }) + + return { + response: { + $case: 'internalServerError', + internalServerError: { + message: error.message + } + } + } + } + } +} diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts new file mode 100644 index 0000000..accb81b --- /dev/null +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -0,0 +1,44 @@ +import { parseProfilesToFriends } from '../../../logic/friends' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { getPage } from '../../../utils/pagination' +import { FRIENDSHIPS_PER_PAGE } from '../constants' +import { + GetBlockedUsersPayload, + GetBlockedUsersResponse +} from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' + +export function getBlockedUsersService({ + components: { logs, db, catalystClient } +}: RPCServiceContext<'logs' | 'db' | 'catalystClient'>) { + const logger = logs.getLogger('get-blocked-users-service') + + return async function (request: GetBlockedUsersPayload, context: RpcServerContext): Promise { + const { pagination } = request + const { address: loggedUserAddress } = context + + try { + const blockedAddresses = await db.getBlockedUsers(loggedUserAddress) + const profiles = await catalystClient.getProfiles(blockedAddresses) + + return { + profiles: parseProfilesToFriends(profiles), + paginationData: { + total: blockedAddresses.length, + page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) + } + } + } catch (error: any) { + logger.error(`Error getting blocked users: ${error.message}`, { + error: error.message, + stack: error.stack + }) + return { + friends: [], + paginationData: { + total: 0, + page: 1 + } + } + } + } +} diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts new file mode 100644 index 0000000..49b3c98 --- /dev/null +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -0,0 +1,61 @@ +import { parseProfileToFriend } from '../../../logic/friends' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { + UnblockUserPayload, + UnblockUserResponse +} from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' + +export function unblockUserService({ + components: { logs, db, catalystClient } +}: RPCServiceContext<'logs' | 'db' | 'catalystClient'>) { + const logger = logs.getLogger('unblock-user-service') + + return async function (request: UnblockUserPayload, context: RpcServerContext): Promise { + try { + const { address: blockerAddress } = context + const blockedAddress = request.user?.address + + if (!blockedAddress) { + return { + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'User address is missing in the request payload' } + } + } + } + + const profile = await catalystClient.getProfile(blockedAddress) + + if (!profile) { + return { + response: { $case: 'invalidRequest', invalidRequest: { message: 'Profile not found' } } + } + } + + await db.unblockUser(blockerAddress, blockedAddress) + + return { + response: { + $case: 'unblocked', + unblocked: { + profile: parseProfileToFriend(profile) + } + } + } + } catch (error: any) { + logger.error(`Error unblocking user: ${error.message}`, { + error: error.message, + stack: error.stack + }) + + return { + response: { + $case: 'internalServerError', + internalServerError: { + message: error.message + } + } + } + } + } +} diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts new file mode 100644 index 0000000..f9bd10c --- /dev/null +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -0,0 +1,83 @@ +import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components' +import { blockUserService } from '../../../../../src/adapters/rpc-server/services/block-user' +import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { RpcServerContext } from '../../../../../src/types' +import { createMockProfile } from '../../../../mocks/profile' + +describe('blockUserService', () => { + let blockUser: ReturnType + + const rpcContext: RpcServerContext = { + address: '0x123', + subscribersContext: undefined + } + + beforeEach(() => { + blockUser = blockUserService({ + components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } + }) + }) + + it('should block a user successfully', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + + const response = await blockUser(request, rpcContext) + + expect(response.response.$case).toBe('blocked') + expect(response.response.blocked.profile).toEqual({ + address: mockProfile.address, + name: mockProfile.name, + avatarUrl: mockProfile.avatarUrl + }) + expect(mockDb.blockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) + }) + + it('should return invalidRequest when user address is missing', async () => { + const request: BlockUserPayload = { + user: { address: '' } + } + + const response = await blockUser(request, rpcContext) + + expect(response.response.$case).toBe('invalidRequest') + expect(response.response.invalidRequest.message).toBe('User address is missing in the request payload') + expect(mockDb.blockUser).not.toHaveBeenCalled() + }) + + it('should return invalidRequest when profile is not found', async () => { + const blockedAddress = '0x456' + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(null) + + const response = await blockUser(request, rpcContext) + + expect(response.response.$case).toBe('invalidRequest') + expect(response.response.invalidRequest.message).toBe('Profile not found') + expect(mockDb.blockUser).not.toHaveBeenCalled() + }) + + it('should handle internal server errors', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.blockUser.mockRejectedValueOnce(new Error('Database error')) + + const response = await blockUser(request, rpcContext) + + expect(response.response.$case).toBe('internalServerError') + expect(response.response.internalServerError.message).toBe('Database error') + }) +}) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts new file mode 100644 index 0000000..6d58daa --- /dev/null +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -0,0 +1,75 @@ +import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components' +import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/services/get-blocked-users' +import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { RpcServerContext } from '../../../../../src/types' +import { createMockProfile } from '../../../../mocks/profile' + +describe('getBlockedUsersService', () => { + let getBlockedUsers: ReturnType + + const rpcContext: RpcServerContext = { + address: '0x123', + subscribersContext: undefined + } + + const request: GetBlockedUsersPayload = { + pagination: { limit: 10, offset: 0 } + } + + beforeEach(() => { + getBlockedUsers = getBlockedUsersService({ + components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } + }) + }) + + it('should return blocked users with their profiles', async () => { + const blockedAddresses = ['0x456', '0x789'] + const mockProfiles = blockedAddresses.map(createMockProfile) + + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) + mockCatalystClient.getProfiles.mockResolvedValueOnce(mockProfiles) + + const response = await getBlockedUsers(request, rpcContext) + + expect(response).toEqual({ + profiles: mockProfiles.map((profile) => ({ + address: profile.address, + name: profile.name, + avatarUrl: profile.avatarUrl + })), + paginationData: { + total: blockedAddresses.length, + page: 1 + } + }) + }) + + it('should handle empty blocked users list', async () => { + mockDb.getBlockedUsers.mockResolvedValueOnce([]) + + const response = await getBlockedUsers(request, rpcContext) + + expect(response).toEqual({ + profiles: [], + paginationData: { + total: 0, + page: 1 + } + }) + expect(mockCatalystClient.getProfiles).not.toHaveBeenCalled() + }) + + it('should handle errors gracefully', async () => { + mockDb.getBlockedUsers.mockRejectedValueOnce(new Error('Database error')) + + const response = await getBlockedUsers(request, rpcContext) + + expect(response).toEqual({ + profiles: [], + paginationData: { + total: 0, + page: 1 + } + }) + }) +}) diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts new file mode 100644 index 0000000..d5c8f1a --- /dev/null +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -0,0 +1,83 @@ +import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components' +import { unblockUserService } from '../../../../../src/adapters/rpc-server/services/unblock-user' +import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { RpcServerContext } from '../../../../../src/types' +import { createMockProfile } from '../../../../mocks/profile' + +describe('unblockUserService', () => { + let unblockUser: ReturnType + + const rpcContext: RpcServerContext = { + address: '0x123', + subscribersContext: undefined + } + + beforeEach(() => { + unblockUser = unblockUserService({ + components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } + }) + }) + + it('should unblock a user successfully', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + + const response = await unblockUser(request, rpcContext) + + expect(response.response.$case).toBe('unblocked') + expect(response.response.unblocked.profile).toEqual({ + address: mockProfile.address, + name: mockProfile.name, + avatarUrl: mockProfile.avatarUrl + }) + expect(mockDb.unblockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) + }) + + it('should return invalidRequest when user address is missing', async () => { + const request: UnblockUserPayload = { + user: { address: '' } + } + + const response = await unblockUser(request, rpcContext) + + expect(response.response.$case).toBe('invalidRequest') + expect(response.response.invalidRequest.message).toBe('User address is missing in the request payload') + expect(mockDb.unblockUser).not.toHaveBeenCalled() + }) + + it('should return invalidRequest when profile is not found', async () => { + const blockedAddress = '0x456' + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(null) + + const response = await unblockUser(request, rpcContext) + + expect(response.response.$case).toBe('invalidRequest') + expect(response.response.invalidRequest.message).toBe('Profile not found') + expect(mockDb.unblockUser).not.toHaveBeenCalled() + }) + + it('should handle internal server errors', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.unblockUser.mockRejectedValueOnce(new Error('Database error')) + + const response = await unblockUser(request, rpcContext) + + expect(response.response.$case).toBe('internalServerError') + expect(response.response.internalServerError.message).toBe('Database error') + }) +}) From 092a473ffff8ffae43c7c43dda5b774217809a0e Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 25 Feb 2025 16:15:35 +0000 Subject: [PATCH 04/26] fix: Bump protocol and fix tests --- package.json | 2 +- src/adapters/rpc-server/rpc-server.ts | 10 ++++ .../rpc-server/services/block-user.ts | 12 ++--- .../rpc-server/services/get-blocked-users.ts | 2 +- .../rpc-server/services/unblock-user.ts | 13 +++-- .../rpc-server/services/block-user.spec.ts | 48 +++++++++++++------ .../services/get-blocked-users.spec.ts | 43 +++++++++-------- .../rpc-server/services/unblock-user.spec.ts | 48 +++++++++++++------ test/unit/adapters/ws-pool.spec.ts | 2 - .../controllers/handlers/ws-handler.spec.ts | 2 - test/unit/peer-tracking.spec.ts | 1 - test/unit/utils/UWebSocketTransport.spec.ts | 2 - yarn.lock | 12 +++-- 13 files changed, 123 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index b9e5af9..ae4e12c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13021146556.commit-7327e37.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/rpc-server/rpc-server.ts b/src/adapters/rpc-server/rpc-server.ts index 249e6ec..145bb33 100644 --- a/src/adapters/rpc-server/rpc-server.ts +++ b/src/adapters/rpc-server/rpc-server.ts @@ -16,6 +16,9 @@ import { friendConnectivityUpdateHandler, friendshipAcceptedUpdateHandler } from '../../logic/updates' +import { blockUserService } from './services/block-user' +import { getBlockedUsersService } from './services/get-blocked-users' +import { unblockUserService } from './services/unblock-user' export async function createRpcServerComponent({ logs, @@ -58,6 +61,10 @@ export async function createRpcServerComponent({ components: { logs, db, archipelagoStats, catalystClient } }) + const blockUser = blockUserService({ components: { logs, db, catalystClient } }) + const unblockUser = unblockUserService({ components: { logs, db, catalystClient } }) + const getBlockedUsers = getBlockedUsersService({ components: { logs, db, catalystClient } }) + rpcServer.setHandler(async function handler(port) { registerService(port, SocialServiceDefinition, async () => ({ getFriends, @@ -66,6 +73,9 @@ export async function createRpcServerComponent({ getSentFriendshipRequests, getFriendshipStatus, upsertFriendship, + blockUser, + unblockUser, + getBlockedUsers, subscribeToFriendshipUpdates, subscribeToFriendConnectivityUpdates })) diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 308e18c..1616e02 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -18,8 +18,8 @@ export function blockUserService({ if (!blockedAddress) { return { response: { - $case: 'invalidRequest', - invalidRequest: { message: 'User address is missing in the request payload' } + $case: 'internalServerError', + internalServerError: { message: 'User address is missing in the request payload' } } } } @@ -29,8 +29,8 @@ export function blockUserService({ if (!profile) { return { response: { - $case: 'invalidRequest', - invalidRequest: { + $case: 'internalServerError', + internalServerError: { message: 'Profile not found' } } @@ -41,8 +41,8 @@ export function blockUserService({ return { response: { - $case: 'blocked', - blocked: { + $case: 'ok', + ok: { profile: parseProfileToFriend(profile) } } diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts index accb81b..a5fc92a 100644 --- a/src/adapters/rpc-server/services/get-blocked-users.ts +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -33,7 +33,7 @@ export function getBlockedUsersService({ stack: error.stack }) return { - friends: [], + profiles: [], paginationData: { total: 0, page: 1 diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index 49b3c98..d6bd4bf 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -18,8 +18,8 @@ export function unblockUserService({ if (!blockedAddress) { return { response: { - $case: 'invalidRequest', - invalidRequest: { message: 'User address is missing in the request payload' } + $case: 'internalServerError', + internalServerError: { message: 'User address is missing in the request payload' } } } } @@ -28,7 +28,10 @@ export function unblockUserService({ if (!profile) { return { - response: { $case: 'invalidRequest', invalidRequest: { message: 'Profile not found' } } + response: { + $case: 'internalServerError', + internalServerError: { message: 'Profile not found' } + } } } @@ -36,8 +39,8 @@ export function unblockUserService({ return { response: { - $case: 'unblocked', - unblocked: { + $case: 'ok', + ok: { profile: parseProfileToFriend(profile) } } diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index f9bd10c..e9db428 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -3,6 +3,7 @@ import { blockUserService } from '../../../../../src/adapters/rpc-server/service import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' +import { parseProfileToFriend } from '../../../../../src/logic/friends' describe('blockUserService', () => { let blockUser: ReturnType @@ -29,28 +30,35 @@ describe('blockUserService', () => { const response = await blockUser(request, rpcContext) - expect(response.response.$case).toBe('blocked') - expect(response.response.blocked.profile).toEqual({ - address: mockProfile.address, - name: mockProfile.name, - avatarUrl: mockProfile.avatarUrl + expect(response).toEqual({ + response: { + $case: 'ok', + ok: { + profile: parseProfileToFriend(mockProfile) + } + } }) expect(mockDb.blockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) + expect(mockLogs.getLogger('block-user-service')).toBeDefined() }) - it('should return invalidRequest when user address is missing', async () => { + it('should return internalServerError when user address is missing', async () => { const request: BlockUserPayload = { user: { address: '' } } const response = await blockUser(request, rpcContext) - expect(response.response.$case).toBe('invalidRequest') - expect(response.response.invalidRequest.message).toBe('User address is missing in the request payload') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: 'User address is missing in the request payload' } + } + }) expect(mockDb.blockUser).not.toHaveBeenCalled() }) - it('should return invalidRequest when profile is not found', async () => { + it('should return internalServerError when profile is not found', async () => { const blockedAddress = '0x456' const request: BlockUserPayload = { user: { address: blockedAddress } @@ -60,24 +68,34 @@ describe('blockUserService', () => { const response = await blockUser(request, rpcContext) - expect(response.response.$case).toBe('invalidRequest') - expect(response.response.invalidRequest.message).toBe('Profile not found') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: 'Profile not found' } + } + }) expect(mockDb.blockUser).not.toHaveBeenCalled() }) - it('should handle internal server errors', async () => { + it('should handle database errors', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } + const error = new Error('Database error') mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) - mockDb.blockUser.mockRejectedValueOnce(new Error('Database error')) + mockDb.blockUser.mockRejectedValueOnce(error) const response = await blockUser(request, rpcContext) - expect(response.response.$case).toBe('internalServerError') - expect(response.response.internalServerError.message).toBe('Database error') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: error.message } + } + }) + expect(mockLogs.getLogger('block-user-service')).toBeDefined() }) }) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index 6d58daa..e868f96 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -3,6 +3,8 @@ import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/s import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' +import { parseProfilesToFriends } from '../../../../../src/logic/friends' +import { FRIENDSHIPS_PER_PAGE } from '../../../../../src/adapters/rpc-server/constants' describe('getBlockedUsersService', () => { let getBlockedUsers: ReturnType @@ -12,19 +14,18 @@ describe('getBlockedUsersService', () => { subscribersContext: undefined } - const request: GetBlockedUsersPayload = { - pagination: { limit: 10, offset: 0 } - } - beforeEach(() => { getBlockedUsers = getBlockedUsersService({ components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } }) }) - it('should return blocked users with their profiles', async () => { + it('should return blocked users with profiles and pagination', async () => { const blockedAddresses = ['0x456', '0x789'] const mockProfiles = blockedAddresses.map(createMockProfile) + const request: GetBlockedUsersPayload = { + pagination: { limit: 10, offset: 0 } + } mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) mockCatalystClient.getProfiles.mockResolvedValueOnce(mockProfiles) @@ -32,35 +33,34 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response).toEqual({ - profiles: mockProfiles.map((profile) => ({ - address: profile.address, - name: profile.name, - avatarUrl: profile.avatarUrl - })), + profiles: parseProfilesToFriends(mockProfiles), paginationData: { total: blockedAddresses.length, page: 1 } }) + expect(mockLogs.getLogger('get-blocked-users-service')).toBeDefined() }) - it('should handle empty blocked users list', async () => { - mockDb.getBlockedUsers.mockResolvedValueOnce([]) + it('should use default pagination when not provided', async () => { + const blockedAddresses = ['0x456'] + const mockProfiles = blockedAddresses.map(createMockProfile) + const request: GetBlockedUsersPayload = {} + + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) + mockCatalystClient.getProfiles.mockResolvedValueOnce(mockProfiles) const response = await getBlockedUsers(request, rpcContext) - expect(response).toEqual({ - profiles: [], - paginationData: { - total: 0, - page: 1 - } - }) - expect(mockCatalystClient.getProfiles).not.toHaveBeenCalled() + expect(response.paginationData.page).toBe(1) + expect(response.profiles).toEqual(parseProfilesToFriends(mockProfiles)) }) it('should handle errors gracefully', async () => { - mockDb.getBlockedUsers.mockRejectedValueOnce(new Error('Database error')) + const error = new Error('Database error') + const request: GetBlockedUsersPayload = {} + + mockDb.getBlockedUsers.mockRejectedValueOnce(error) const response = await getBlockedUsers(request, rpcContext) @@ -71,5 +71,6 @@ describe('getBlockedUsersService', () => { page: 1 } }) + expect(mockLogs.getLogger('get-blocked-users-service')).toBeDefined() }) }) diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index d5c8f1a..bb0a717 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -3,6 +3,7 @@ import { unblockUserService } from '../../../../../src/adapters/rpc-server/servi import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' +import { parseProfileToFriend } from '../../../../../src/logic/friends' describe('unblockUserService', () => { let unblockUser: ReturnType @@ -29,28 +30,35 @@ describe('unblockUserService', () => { const response = await unblockUser(request, rpcContext) - expect(response.response.$case).toBe('unblocked') - expect(response.response.unblocked.profile).toEqual({ - address: mockProfile.address, - name: mockProfile.name, - avatarUrl: mockProfile.avatarUrl + expect(response).toEqual({ + response: { + $case: 'ok', + ok: { + profile: parseProfileToFriend(mockProfile) + } + } }) expect(mockDb.unblockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) + expect(mockLogs.getLogger('unblock-user-service')).toBeDefined() }) - it('should return invalidRequest when user address is missing', async () => { + it('should return internalServerError when user address is missing', async () => { const request: UnblockUserPayload = { user: { address: '' } } const response = await unblockUser(request, rpcContext) - expect(response.response.$case).toBe('invalidRequest') - expect(response.response.invalidRequest.message).toBe('User address is missing in the request payload') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: 'User address is missing in the request payload' } + } + }) expect(mockDb.unblockUser).not.toHaveBeenCalled() }) - it('should return invalidRequest when profile is not found', async () => { + it('should return internalServerError when profile is not found', async () => { const blockedAddress = '0x456' const request: UnblockUserPayload = { user: { address: blockedAddress } @@ -60,24 +68,34 @@ describe('unblockUserService', () => { const response = await unblockUser(request, rpcContext) - expect(response.response.$case).toBe('invalidRequest') - expect(response.response.invalidRequest.message).toBe('Profile not found') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: 'Profile not found' } + } + }) expect(mockDb.unblockUser).not.toHaveBeenCalled() }) - it('should handle internal server errors', async () => { + it('should handle database errors', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } + const error = new Error('Database error') mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) - mockDb.unblockUser.mockRejectedValueOnce(new Error('Database error')) + mockDb.unblockUser.mockRejectedValueOnce(error) const response = await unblockUser(request, rpcContext) - expect(response.response.$case).toBe('internalServerError') - expect(response.response.internalServerError.message).toBe('Database error') + expect(response).toEqual({ + response: { + $case: 'internalServerError', + internalServerError: { message: error.message } + } + }) + expect(mockLogs.getLogger('unblock-user-service')).toBeDefined() }) }) diff --git a/test/unit/adapters/ws-pool.spec.ts b/test/unit/adapters/ws-pool.spec.ts index 60989b8..d3be644 100644 --- a/test/unit/adapters/ws-pool.spec.ts +++ b/test/unit/adapters/ws-pool.spec.ts @@ -10,8 +10,6 @@ describe('ws-pool-component', () => { let mockClearInterval: jest.Mock beforeEach(async () => { - jest.clearAllMocks() - // Mock setInterval/clearInterval originalSetInterval = global.setInterval mockSetInterval = jest.fn() diff --git a/test/unit/controllers/handlers/ws-handler.spec.ts b/test/unit/controllers/handlers/ws-handler.spec.ts index 6956d02..def9894 100644 --- a/test/unit/controllers/handlers/ws-handler.spec.ts +++ b/test/unit/controllers/handlers/ws-handler.spec.ts @@ -23,8 +23,6 @@ describe('ws-handler', () => { let mockContext: any beforeEach(async () => { - jest.clearAllMocks() - mockData = { isConnected: false, auth: false, diff --git a/test/unit/peer-tracking.spec.ts b/test/unit/peer-tracking.spec.ts index dc77db0..ba45e38 100644 --- a/test/unit/peer-tracking.spec.ts +++ b/test/unit/peer-tracking.spec.ts @@ -7,7 +7,6 @@ describe('PeerTrackingComponent', () => { let peerTracking: IPeerTrackingComponent beforeEach(async () => { - jest.clearAllMocks() peerTracking = await createPeerTrackingComponent({ logs: mockLogs, nats: mockNats, diff --git a/test/unit/utils/UWebSocketTransport.spec.ts b/test/unit/utils/UWebSocketTransport.spec.ts index 0ed3845..e9282b6 100644 --- a/test/unit/utils/UWebSocketTransport.spec.ts +++ b/test/unit/utils/UWebSocketTransport.spec.ts @@ -10,8 +10,6 @@ describe('UWebSocketTransport', () => { let errorListener: jest.Mock beforeEach(async () => { - jest.clearAllMocks() - mockConfig.getNumber.mockImplementation(async (key) => { const defaults: Record = { WS_TRANSPORT_MAX_QUEUE_SIZE: 1000, diff --git a/yarn.lock b/yarn.lock index b01d716..0926047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,9 +756,15 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13021146556.commit-7327e37.tgz": - version "1.0.0-13021146556.commit-7327e37" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13021146556.commit-7327e37.tgz#9f0e1ee50633bba35736beb90cb6c7c7300d8e58" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13524950006.commit-c63ab51.tgz": + version "1.0.0-13524950006.commit-c63ab51" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13524950006.commit-c63ab51.tgz#7f5abf55286fa6cc2661e35775942f35d92bbec2" + dependencies: + "@dcl/ts-proto" "1.154.0" + +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz": + version "1.0.0-13525612589.commit-f273ffa" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz#c1423467b62944d864b0141a90dfd866e47cae60" dependencies: "@dcl/ts-proto" "1.154.0" From 4faffeb5a7d6f1accfcf61618138d0ddacd4358d Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 25 Feb 2025 16:27:27 +0000 Subject: [PATCH 05/26] test: improve tests --- test/unit/adapters/db.spec.ts | 91 +++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 090aa75..7eec0ce 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -16,7 +16,7 @@ describe('db', () => { }) describe('getFriends', () => { - it('should return active friendships', async () => { + it('should return active friendships excluding blocked users', async () => { const mockFriends = [ { id: 'friendship-1', address_requester: '0x123', address_requested: '0x456', is_active: true } ] @@ -36,31 +36,27 @@ describe('db', () => { values: ['0x123'] }, { - text: 'OR LOWER(f.address_requested) =', - values: ['0x123'] - }, - { - text: 'AND f.is_active = true', + text: 'AND NOT EXISTS (SELECT 1 FROM blocks b WHERE', values: [] }, { - text: 'ORDER BY f.created_at DESC', + text: 'AND b.blocked_address = CASE', values: [] }, { - text: 'OFFSET', - values: [expect.any(Number)] + text: 'WHEN LOWER(f.address_requester) =', + values: ['0x123'] }, { - text: 'LIMIT', - values: [expect.any(Number)] + text: 'ORDER BY f.created_at DESC', + values: [] } ] expectedFragmentsOfTheQuery.forEach(({ text, values }) => { expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(text), + strings: expect.arrayContaining([expect.stringContaining(text)]), values: expect.arrayContaining(values) }) ) @@ -69,7 +65,7 @@ describe('db', () => { expect(result).toEqual(mockFriends) }) - it('should return all friendships including inactive', async () => { + it('should return all friendships including inactive but excluding blocked users', async () => { const mockFriends = [ { id: 'friendship-1', address_requester: '0x123', address_requested: '0x456', is_active: false } ] @@ -78,8 +74,11 @@ describe('db', () => { const result = await dbComponent.getFriends('0x123', { onlyActive: false }) expect(mockPg.query).toHaveBeenCalledWith( - expect.not.objectContaining({ - text: expect.stringContaining('AND is_active = true') + expect.objectContaining({ + strings: expect.arrayContaining([ + expect.not.stringContaining('AND f.is_active = true'), + expect.stringContaining('AND NOT EXISTS (SELECT 1 FROM blocks b WHERE') + ]) }) ) expect(result).toEqual(mockFriends) @@ -87,37 +86,67 @@ describe('db', () => { }) describe('getFriendsCount', () => { - it('should return the count of active friendships', async () => { + it('should return the count of active friendships excluding blocked users', async () => { const mockCount = 5 mockPg.query.mockResolvedValueOnce({ rows: [{ count: mockCount }], rowCount: 1 }) const result = await dbComponent.getFriendsCount('0x123', { onlyActive: true }) - const expectedQuery = SQL`WHERE (LOWER(f.address_requester) = ${'0x123'} OR LOWER(f.address_requested) = ${'0x123'}) AND f.is_active = true` + const expectedQueryFragments = [ + { + text: 'SELECT DISTINCT COUNT(*) FROM friendships f', + values: [] + }, + { + text: 'WHERE (LOWER(f.address_requester) =', + values: ['0x123'] + }, + { + text: 'AND NOT EXISTS (SELECT 1 FROM blocks b WHERE', + values: [] + } + ] - expect(mockPg.query).toHaveBeenCalledWith( - expect.objectContaining({ - text: expect.stringContaining(expectedQuery.text), - values: expect.arrayContaining(expectedQuery.values) - }) - ) + expectedQueryFragments.forEach(({ text, values }) => { + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + strings: expect.arrayContaining([expect.stringContaining(text)]), + values: expect.arrayContaining(values) + }) + ) + }) expect(result).toBe(mockCount) }) - it('should return the count of all friendships', async () => { + it('should return the count of all friendships excluding blocked users', async () => { const mockCount = 10 mockPg.query.mockResolvedValueOnce({ rows: [{ count: mockCount }], rowCount: 1 }) const result = await dbComponent.getFriendsCount('0x123', { onlyActive: false }) - const expectedQuery = SQL`WHERE (LOWER(f.address_requester) = ${'0x123'} OR LOWER(f.address_requested) = ${'0x123'})` + const expectedQueryFragments = [ + { + text: 'SELECT DISTINCT COUNT(*) FROM friendships f', + values: [] + }, + { + text: 'WHERE (LOWER(f.address_requester) =', + values: ['0x123'] + }, + { + text: 'AND NOT EXISTS (SELECT 1 FROM blocks b WHERE', + values: [] + } + ] - expect(mockPg.query).toHaveBeenCalledWith( - expect.objectContaining({ - text: expect.stringContaining(expectedQuery.text), - values: expect.arrayContaining(expectedQuery.values) - }) - ) + expectedQueryFragments.forEach(({ text, values }) => { + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + strings: expect.arrayContaining([expect.stringContaining(text)]), + values: expect.arrayContaining(values) + }) + ) + }) expect(result).toBe(mockCount) }) }) From e01097f80e97aaf2b7c5200ee04fbddd6902fead Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 25 Feb 2025 16:32:37 +0000 Subject: [PATCH 06/26] fix: Use new type Profile and rename parsers --- src/adapters/rpc-server/services/block-user.ts | 4 ++-- .../rpc-server/services/get-blocked-users.ts | 4 ++-- src/adapters/rpc-server/services/get-friends.ts | 4 ++-- .../rpc-server/services/get-mutual-friends.ts | 4 ++-- .../subscribe-to-friend-connectivity-updates.ts | 4 ++-- src/adapters/rpc-server/services/unblock-user.ts | 4 ++-- src/logic/friends.ts | 10 +++++----- src/logic/friendships.ts | 8 ++++---- .../rpc-server/services/block-user.spec.ts | 4 ++-- .../rpc-server/services/get-blocked-users.spec.ts | 6 +++--- ...ubscribe-to-friend-connectivity-updates.spec.ts | 12 ++++++------ .../subscribe-to-friendship-updates.spec.ts | 10 +++++----- .../rpc-server/services/unblock-user.spec.ts | 4 ++-- test/unit/logic/friends.spec.ts | 6 +++--- test/unit/logic/friendships.spec.ts | 14 +++++++------- 15 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 1616e02..41531c2 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -1,4 +1,4 @@ -import { parseProfileToFriend } from '../../../logic/friends' +import { parseCatalystProfileToProfile } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { BlockUserPayload, @@ -43,7 +43,7 @@ export function blockUserService({ response: { $case: 'ok', ok: { - profile: parseProfileToFriend(profile) + profile: parseCatalystProfileToProfile(profile) } } } diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts index a5fc92a..2589c80 100644 --- a/src/adapters/rpc-server/services/get-blocked-users.ts +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -1,4 +1,4 @@ -import { parseProfilesToFriends } from '../../../logic/friends' +import { parseCatalystProfilesToProfiles } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -21,7 +21,7 @@ export function getBlockedUsersService({ const profiles = await catalystClient.getProfiles(blockedAddresses) return { - profiles: parseProfilesToFriends(profiles), + profiles: parseCatalystProfilesToProfiles(profiles), paginationData: { total: blockedAddresses.length, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts index a0227b2..852a8ed 100644 --- a/src/adapters/rpc-server/services/get-friends.ts +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -1,4 +1,4 @@ -import { parseProfilesToFriends } from '../../../logic/friends' +import { parseCatalystProfilesToProfiles } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -29,7 +29,7 @@ export function getFriendsService({ const profiles = await catalystClient.getProfiles(friends.map((friend) => friend.address)) return { - friends: parseProfilesToFriends(profiles), + friends: parseCatalystProfilesToProfiles(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-mutual-friends.ts b/src/adapters/rpc-server/services/get-mutual-friends.ts index 9c05775..94df142 100644 --- a/src/adapters/rpc-server/services/get-mutual-friends.ts +++ b/src/adapters/rpc-server/services/get-mutual-friends.ts @@ -6,7 +6,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { normalizeAddress } from '../../../utils/address' import { getPage } from '../../../utils/pagination' -import { parseProfilesToFriends } from '../../../logic/friends' +import { parseCatalystProfilesToProfiles } from '../../../logic/friends' export function getMutualFriendsService({ components: { logs, db, catalystClient } @@ -30,7 +30,7 @@ export function getMutualFriendsService({ const profiles = await catalystClient.getProfiles(mutualFriends.map((friend) => friend.address)) return { - friends: parseProfilesToFriends(profiles), + friends: parseCatalystProfilesToProfiles(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts index e9f2d35..11ff0d5 100644 --- a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts +++ b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts @@ -5,7 +5,7 @@ import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { parseEmittedUpdateToFriendConnectivityUpdate } from '../../../logic/friendships' -import { parseProfilesToFriends } from '../../../logic/friends' +import { parseCatalystProfilesToProfiles } from '../../../logic/friends' import { handleSubscriptionUpdates } from '../../../logic/updates' export function subscribeToFriendConnectivityUpdatesService({ @@ -21,7 +21,7 @@ export function subscribeToFriendConnectivityUpdatesService({ const onlineFriends = await db.getOnlineFriends(context.address, onlinePeers) const profiles = await catalystClient.getProfiles(onlineFriends.map((friend) => friend.address)) - const parsedProfiles = parseProfilesToFriends(profiles).map((friend) => ({ + const parsedProfiles = parseCatalystProfilesToProfiles(profiles).map((friend) => ({ friend, status: ConnectivityStatus.ONLINE })) diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index d6bd4bf..c5558f9 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -1,4 +1,4 @@ -import { parseProfileToFriend } from '../../../logic/friends' +import { parseCatalystProfileToProfile } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { UnblockUserPayload, @@ -41,7 +41,7 @@ export function unblockUserService({ response: { $case: 'ok', ok: { - profile: parseProfileToFriend(profile) + profile: parseCatalystProfileToProfile(profile) } } } diff --git a/src/logic/friends.ts b/src/logic/friends.ts index 9697bcb..b210bac 100644 --- a/src/logic/friends.ts +++ b/src/logic/friends.ts @@ -1,8 +1,8 @@ -import { FriendProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { Profile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { getProfileName, getProfileHasClaimedName, getProfileUserId, getProfilePictureUrl } from './profiles' -import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' +import { Profile as LambdasProfile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -export function parseProfileToFriend(profile: Profile): FriendProfile { +export function parseCatalystProfileToProfile(profile: LambdasProfile): Profile { const name = getProfileName(profile) const userId = getProfileUserId(profile) const hasClaimedName = getProfileHasClaimedName(profile) @@ -16,6 +16,6 @@ export function parseProfileToFriend(profile: Profile): FriendProfile { } } -export function parseProfilesToFriends(profiles: Profile[]): FriendProfile[] { - return profiles.map((profile) => parseProfileToFriend(profile)) +export function parseCatalystProfilesToProfiles(profiles: LambdasProfile[]): Profile[] { + return profiles.map((profile) => parseCatalystProfileToProfile(profile)) } diff --git a/src/logic/friendships.ts b/src/logic/friendships.ts index f836ca6..004c174 100644 --- a/src/logic/friendships.ts +++ b/src/logic/friendships.ts @@ -7,7 +7,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, FriendshipAction, FriendshipRequest, FriendshipStatus, SubscriptionEventsEmitter } from '../types' import { normalizeAddress } from '../utils/address' -import { parseProfileToFriend } from './friends' +import { parseCatalystProfileToProfile } from './friends' import { getProfileUserId } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' @@ -142,7 +142,7 @@ export function parseEmittedUpdateToFriendshipUpdate( request: { id: update.id, createdAt: update.timestamp, - friend: parseProfileToFriend(profile), + friend: parseCatalystProfileToProfile(profile), message: update.metadata?.message } } @@ -202,7 +202,7 @@ export function parseEmittedUpdateToFriendConnectivityUpdate( ): FriendConnectivityUpdate | null { const { status } = update return { - friend: parseProfileToFriend(profile), + friend: parseCatalystProfileToProfile(profile), status } } @@ -224,7 +224,7 @@ export function parseFriendshipRequestToFriendshipRequestResponse( ): FriendshipRequestResponse { return { id: request.id, - friend: parseProfileToFriend(profile), + friend: parseCatalystProfileToProfile(profile), createdAt: new Date(request.timestamp).getTime(), message: request.metadata?.message || '' } diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index e9db428..308f1ea 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -3,7 +3,7 @@ import { blockUserService } from '../../../../../src/adapters/rpc-server/service import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToFriend } from '../../../../../src/logic/friends' +import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' describe('blockUserService', () => { let blockUser: ReturnType @@ -34,7 +34,7 @@ describe('blockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(mockProfile) + profile: parseCatalystProfileToProfile(mockProfile) } } }) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index e868f96..a4e9071 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -3,7 +3,7 @@ import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/s import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfilesToFriends } from '../../../../../src/logic/friends' +import { parseCatalystProfilesToProfiles } from '../../../../../src/logic/friends' import { FRIENDSHIPS_PER_PAGE } from '../../../../../src/adapters/rpc-server/constants' describe('getBlockedUsersService', () => { @@ -33,7 +33,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response).toEqual({ - profiles: parseProfilesToFriends(mockProfiles), + profiles: parseCatalystProfilesToProfiles(mockProfiles), paginationData: { total: blockedAddresses.length, page: 1 @@ -53,7 +53,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response.paginationData.page).toBe(1) - expect(response.profiles).toEqual(parseProfilesToFriends(mockProfiles)) + expect(response.profiles).toEqual(parseCatalystProfilesToProfiles(mockProfiles)) }) it('should handle errors gracefully', async () => { diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts index 7aba99d..14b2198 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts @@ -4,7 +4,7 @@ import { mockLogs, mockArchipelagoStats, mockDb, mockConfig, mockCatalystClient import { subscribeToFriendConnectivityUpdatesService } from '../../../../../src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates' import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToFriend } from '../../../../../src/logic/friends' +import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' @@ -42,7 +42,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockArchipelagoStats.getPeers.mockResolvedValue(['0x456', '0x789']) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -52,7 +52,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { expect(mockArchipelagoStats.getPeersFromCache).toHaveBeenCalled() expect(result.value).toEqual({ - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE }) @@ -65,7 +65,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -107,7 +107,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -125,7 +125,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts index 75ad448..1c24ac8 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts @@ -4,7 +4,7 @@ import { Action, RpcServerContext } from '../../../../../src/types' import { mockCatalystClient, mockLogs } from '../../../../mocks/components' import { createMockProfile } from '../../../../mocks/profile' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' -import { parseProfileToFriend } from '../../../../../src/logic/friends' +import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' jest.mock('../../../../../src/logic/updates') @@ -41,7 +41,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should handle subscription updates', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -51,7 +51,7 @@ describe('subscribeToFriendshipUpdatesService', () => { const result = await generator.next() expect(result.value).toEqual({ - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp }) @@ -84,7 +84,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should get the proper address from the update', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -100,7 +100,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should filter updates based on address conditions', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToFriend(mockFriendProfile), + friend: parseCatalystProfileToProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index bb0a717..32cb3ac 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -3,7 +3,7 @@ import { unblockUserService } from '../../../../../src/adapters/rpc-server/servi import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToFriend } from '../../../../../src/logic/friends' +import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' describe('unblockUserService', () => { let unblockUser: ReturnType @@ -34,7 +34,7 @@ describe('unblockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(mockProfile) + profile: parseCatalystProfileToProfile(mockProfile) } } }) diff --git a/test/unit/logic/friends.spec.ts b/test/unit/logic/friends.spec.ts index 0f0277b..e772aad 100644 --- a/test/unit/logic/friends.spec.ts +++ b/test/unit/logic/friends.spec.ts @@ -1,9 +1,9 @@ -import { parseProfilesToFriends, parseProfileToFriend } from '../../../src/logic/friends' +import { parseCatalystProfilesToProfiles, parseCatalystProfileToProfile } from '../../../src/logic/friends' import { mockProfile } from '../../mocks/profile' describe('parseProfileToFriend', () => { it('should parse profile to friend', () => { - const friend = parseProfileToFriend(mockProfile) + const friend = parseCatalystProfileToProfile(mockProfile) expect(friend).toEqual({ address: mockProfile.avatars[0].userId, name: mockProfile.avatars[0].name, @@ -28,7 +28,7 @@ describe('parseProfilesToFriends', () => { } const profiles = [mockProfile, anotherProfile] - const result = parseProfilesToFriends(profiles) + const result = parseCatalystProfilesToProfiles(profiles) expect(result).toEqual([ { diff --git a/test/unit/logic/friendships.spec.ts b/test/unit/logic/friendships.spec.ts index 38ac8b1..750931c 100644 --- a/test/unit/logic/friendships.spec.ts +++ b/test/unit/logic/friendships.spec.ts @@ -17,7 +17,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockExpectedFriendshipRequest, createMockFriendshipRequest } from '../../mocks/friendship-request' import { createMockProfile, mockProfile } from '../../mocks/profile' -import { parseProfileToFriend } from '../../../src/logic/friends' +import { parseCatalystProfileToProfile } from '../../../src/logic/friends' describe('isFriendshipActionValid()', () => { test('it should be valid if from is null and to is REQUEST ', () => { @@ -399,7 +399,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseProfileToFriend(mockProfile), + friend: parseCatalystProfileToProfile(mockProfile), message: undefined } } @@ -425,7 +425,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseProfileToFriend(mockProfile), + friend: parseCatalystProfileToProfile(mockProfile), message: 'Hi!' } } @@ -493,7 +493,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { from: '0xA', to: '0xB' }, - mockProfile, + mockProfile ) ).toEqual({ update: { @@ -518,7 +518,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { from: '0xA', to: '0xB' }, - mockProfile, + mockProfile ) ).toEqual({ update: { @@ -543,7 +543,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { from: '0xA', to: '0xB' }, - mockProfile, + mockProfile ) ).toBe(null) }) @@ -589,7 +589,7 @@ describe('parseEmittedUpdateToFriendConnectivityUpdate()', () => { ])('it should parse status %s update properly', (status) => { const update = { address: '0x123', status } expect(parseEmittedUpdateToFriendConnectivityUpdate(update, mockProfile)).toEqual({ - friend: parseProfileToFriend(mockProfile), + friend: parseCatalystProfileToProfile(mockProfile), status }) }) From fccda6dd02f4ca11aa490bfdb8df36e973d7b2d7 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 26 Feb 2025 10:26:13 +0000 Subject: [PATCH 07/26] feat: Blocking filter in mutual friends and friendship requests --- .../services/get-friendship-status.ts | 1 + src/logic/queries.ts | 7 ++++-- test/unit/adapters/db.spec.ts | 22 +++++-------------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/adapters/rpc-server/services/get-friendship-status.ts b/src/adapters/rpc-server/services/get-friendship-status.ts index 8ab7146..ead7fd3 100644 --- a/src/adapters/rpc-server/services/get-friendship-status.ts +++ b/src/adapters/rpc-server/services/get-friendship-status.ts @@ -5,6 +5,7 @@ import { GetFriendshipStatusResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +// TODO: What should happen if one of the users is blocked? export function getFriendshipStatusService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { const logger = logs.getLogger('get-sent-friendship-requests-service') diff --git a/src/logic/queries.ts b/src/logic/queries.ts index b7dc012..aba2346 100644 --- a/src/logic/queries.ts +++ b/src/logic/queries.ts @@ -124,6 +124,7 @@ export function getFriendshipRequestsBaseQuery( baseQuery.append(SQL` AND action = ${Action.REQUEST}`) baseQuery.append(SQL` AND f.is_active IS FALSE`) + baseQuery.append(SQL` AND `).append(getBlockingCondition(userAddress)) if (!onlyCount) { baseQuery.append(SQL` ORDER BY lr.timestamp DESC`) @@ -161,8 +162,10 @@ export function getMutualFriendsBaseQuery( .append(SQL` WHERE `) .append(getFriendshipCondition(address, tableAlias)) .append(SQL` AND `) - .append(tableAlias).append(SQL`.is_active = true - `) + .append(tableAlias) + .append(SQL`.is_active = true`) + .append(SQL` AND `) + .append(getBlockingCondition(address)) const query = SQL`WITH friendsA as (`.append(friendsSubquery(normalizedUserAddress1, 'f_a')).append(SQL`) SELECT `) diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 7eec0ce..ac98351 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -152,7 +152,7 @@ describe('db', () => { }) describe('getMutualFriends', () => { - it('should return mutual friends with proper query structure', async () => { + it('should return mutual friends excluding blocked users', async () => { const mockMutualFriends = [{ address: '0x789' }] mockPg.query.mockResolvedValueOnce({ rows: mockMutualFriends, rowCount: mockMutualFriends.length }) @@ -166,23 +166,11 @@ describe('db', () => { values: ['0x123'] }, { - text: 'LOWER(f_a.address_requester) =', - values: ['0x123'] - }, - { - text: 'LOWER(f_a.address_requested) =', - values: ['0x123'] - }, - { - text: 'LOWER(f_b.address_requester) =', - values: ['0x456'] - }, - { - text: 'LOWER(f_b.address_requested) =', - values: ['0x456'] + text: 'AND NOT EXISTS (SELECT 1 FROM blocks b WHERE', + values: [] }, { - text: 'is_active = true', + text: 'AND b.blocked_address = CASE', values: [] }, { @@ -202,7 +190,7 @@ describe('db', () => { expectedQueryFragments.forEach(({ text, values }) => { expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining(text), + strings: expect.arrayContaining([expect.stringContaining(text)]), values: expect.arrayContaining(values) }) ) From c738e4d36baf362da03f2e6423ffbe9eca8d3143 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 26 Feb 2025 10:32:20 +0000 Subject: [PATCH 08/26] chore: Remove unused function --- src/adapters/db.ts | 8 -------- .../rpc-server/services/get-friends.ts | 1 - .../services/get-friendship-status.ts | 2 +- .../rpc-server/services/upsert-friendship.ts | 1 + src/types.ts | 1 - test/mocks/components/db.ts | 1 - test/unit/adapters/db.spec.ts | 18 ------------------ 7 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index a795b3a..862c373 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -70,14 +70,6 @@ export function createDBComponent(components: Pick return results.rows[0] }, - async getLastFriendshipAction(friendshipId) { - const query = SQL` - SELECT * FROM friendship_actions where friendship_id = ${friendshipId} ORDER BY timestamp DESC LIMIT 1 - ` - const results = await pg.query(query) - - return results.rows[0] - }, async getLastFriendshipActionByUsers(loggedUser: string, friendUser: string) { const normalizedLoggedUser = normalizeAddress(loggedUser) const normalizedFriendUser = normalizeAddress(friendUser) diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts index 852a8ed..06e4ddf 100644 --- a/src/adapters/rpc-server/services/get-friends.ts +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -20,7 +20,6 @@ export function getFriendsService({ const { address: loggedUserAddress } = context try { - // TODO: can use the getPeersFromCache to get the online friends and sort online friends first const [friends, total] = await Promise.all([ db.getFriends(loggedUserAddress, { pagination }), db.getFriendsCount(loggedUserAddress) diff --git a/src/adapters/rpc-server/services/get-friendship-status.ts b/src/adapters/rpc-server/services/get-friendship-status.ts index ead7fd3..5817571 100644 --- a/src/adapters/rpc-server/services/get-friendship-status.ts +++ b/src/adapters/rpc-server/services/get-friendship-status.ts @@ -5,7 +5,7 @@ import { GetFriendshipStatusResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -// TODO: What should happen if one of the users is blocked? +// TODO(feat/blocks): What should happen if one of the users is blocked? export function getFriendshipStatusService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { const logger = logs.getLogger('get-sent-friendship-requests-service') diff --git a/src/adapters/rpc-server/services/upsert-friendship.ts b/src/adapters/rpc-server/services/upsert-friendship.ts index 18b23d9..3c84794 100644 --- a/src/adapters/rpc-server/services/upsert-friendship.ts +++ b/src/adapters/rpc-server/services/upsert-friendship.ts @@ -46,6 +46,7 @@ export function upsertFriendshipService({ } try { + // TODO(feat/blocks): check if someone is blocked by the other user first const lastAction = await db.getLastFriendshipActionByUsers(context.address, parsedRequest.user!) if (!validateNewFriendshipAction(context.address, parsedRequest, lastAction)) { diff --git a/src/types.ts b/src/types.ts index 14bef28..d1c24af 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,7 +94,6 @@ export interface IDatabaseComponent { getMutualFriends(userAddress1: string, userAddress2: string, pagination?: Pagination): Promise getMutualFriendsCount(userAddress1: string, userAddress2: string): Promise getFriendship(userAddresses: [string, string]): Promise - getLastFriendshipAction(friendshipId: string): Promise getLastFriendshipActionByUsers(loggedUser: string, friendUser: string): Promise recordFriendshipAction( friendshipId: string, diff --git a/test/mocks/components/db.ts b/test/mocks/components/db.ts index 203cc6b..e299e1d 100644 --- a/test/mocks/components/db.ts +++ b/test/mocks/components/db.ts @@ -9,7 +9,6 @@ export const mockDb: jest.Mocked = { getMutualFriends: jest.fn(), getMutualFriendsCount: jest.fn(), getFriendship: jest.fn(), - getLastFriendshipAction: jest.fn(), getLastFriendshipActionByUsers: jest.fn(), recordFriendshipAction: jest.fn(), getReceivedFriendshipRequests: jest.fn(), diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index ac98351..a908c55 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -350,24 +350,6 @@ describe('db', () => { }) }) - describe('getLastFriendshipAction', () => { - it('should return the most recent friendship action', async () => { - const mockAction = { - id: 'action-1', - friendship_id: 'friendship-1', - action: Action.REQUEST, - acting_user: '0x123', - metadata: null, - timestamp: '2025-01-01T00:00:00.000Z' - } - mockPg.query.mockResolvedValueOnce({ rows: [mockAction], rowCount: 1 }) - - const result = await dbComponent.getLastFriendshipAction('friendship-1') - - expect(result).toEqual(mockAction) - }) - }) - describe('getReceivedFriendshipRequests', () => { it('should retrieve received friendship requests', async () => { const mockRequests = [ From 486a165d6742d00735aabd06c66ca47301631768 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 26 Feb 2025 10:49:30 +0000 Subject: [PATCH 09/26] feat: Filter blocked users from online friends query --- src/adapters/db.ts | 6 ++++++ test/unit/adapters/db.spec.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 862c373..92f14ad 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -156,6 +156,12 @@ export function createDBComponent(components: Pick OR (LOWER(address_requested) = ${normalizedUserAddress} AND LOWER(address_requester) = ANY(${normalizedOnlinePotentialFriends})) ) + AND NOT EXISTS ( + SELECT 1 FROM blocks + WHERE (LOWER(blocker_address) = ${normalizedUserAddress} AND LOWER(blocked_address) = ANY(${normalizedOnlinePotentialFriends})) + OR + (LOWER(blocker_address) = ANY(${normalizedOnlinePotentialFriends}) AND LOWER(blocked_address) = ${normalizedUserAddress}) + ) AND is_active = true` const results = await pg.query(query) diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index a908c55..21db6c2 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -509,7 +509,7 @@ describe('db', () => { expect(mockPg.query).not.toHaveBeenCalled() }) - it('should query friendships for potential friends', async () => { + it.only('should query friendships for potential friends', async () => { const mockResult = { rows: [{ address: '0x456' }, { address: '0x789' }], rowCount: 2 @@ -525,7 +525,13 @@ describe('db', () => { SQL`WHEN LOWER(address_requester) = ${userAddress} THEN LOWER(address_requested)`, SQL`ELSE LOWER(address_requester)`, SQL`(LOWER(address_requester) = ${userAddress} AND LOWER(address_requested) = ANY(${normalizedPotentialFriends}))`, - SQL`(LOWER(address_requested) = ${userAddress} AND LOWER(address_requester) = ANY(${normalizedPotentialFriends}))` + SQL`(LOWER(address_requested) = ${userAddress} AND LOWER(address_requester) = ANY(${normalizedPotentialFriends}))`, + SQL`AND NOT EXISTS ( + SELECT 1 FROM blocks + WHERE (LOWER(blocker_address) = ${userAddress} AND LOWER(blocked_address) = ANY(${normalizedPotentialFriends})) + OR + (LOWER(blocker_address) = ANY(${normalizedPotentialFriends}) AND LOWER(blocked_address) = ${userAddress}) + )` ] queryExpectations.forEach((query) => { From 2e5f168a7e6d3fb806a7f81110ee915fdbbadfde Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 26 Feb 2025 12:09:20 +0000 Subject: [PATCH 10/26] feat: Add validation in upsert for blocked friendships --- src/adapters/db.ts | 14 +++++++++ .../rpc-server/services/upsert-friendship.ts | 12 +++++++- src/types.ts | 1 + test/mocks/components/db.ts | 1 + test/unit/adapters/db.spec.ts | 29 ++++++++++++++++++- .../services/upsert-friendship.spec.ts | 17 ++++++++++- test/unit/logic/friends.spec.ts | 4 +-- 7 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 92f14ad..50067f7 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -210,6 +210,20 @@ export function createDBComponent(components: Pick const results = await pg.query(query) return results.rows.map((row) => row.address) }, + async isFriendshipBlocked(loggedUserAddress, anotherUserAddress) { + const normalizedLoggedUserAddress = normalizeAddress(loggedUserAddress) + const normalizedAnotherUserAddress = normalizeAddress(anotherUserAddress) + + const query = SQL` + SELECT EXISTS ( + SELECT 1 FROM blocks + WHERE (LOWER(blocker_address), LOWER(blocked_address)) IN ((${normalizedLoggedUserAddress}, ${normalizedAnotherUserAddress}), (${normalizedAnotherUserAddress}, ${normalizedLoggedUserAddress})) + ) + ` + const results = await pg.query<{ exists: boolean }>(query) + return results.rows[0].exists + }, + async executeTx(cb: (client: PoolClient) => Promise): Promise { const pool = pg.getPool() const client = await pool.connect() diff --git a/src/adapters/rpc-server/services/upsert-friendship.ts b/src/adapters/rpc-server/services/upsert-friendship.ts index 3c84794..8c4917b 100644 --- a/src/adapters/rpc-server/services/upsert-friendship.ts +++ b/src/adapters/rpc-server/services/upsert-friendship.ts @@ -46,7 +46,17 @@ export function upsertFriendshipService({ } try { - // TODO(feat/blocks): check if someone is blocked by the other user first + const isBlocked = await db.isFriendshipBlocked(context.address, parsedRequest.user!) + + if (isBlocked) { + return { + response: { + $case: 'invalidFriendshipAction', + invalidFriendshipAction: {} + } + } + } + const lastAction = await db.getLastFriendshipActionByUsers(context.address, parsedRequest.user!) if (!validateNewFriendshipAction(context.address, parsedRequest, lastAction)) { diff --git a/src/types.ts b/src/types.ts index d1c24af..5d62a47 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,6 +112,7 @@ export interface IDatabaseComponent { blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise getBlockedUsers(blockerAddress: string): Promise + isFriendshipBlocked(blockerAddress: string, blockedAddress: string): Promise executeTx(cb: (client: PoolClient) => Promise): Promise } export interface IRedisComponent extends IBaseComponent { diff --git a/test/mocks/components/db.ts b/test/mocks/components/db.ts index e299e1d..71c5b62 100644 --- a/test/mocks/components/db.ts +++ b/test/mocks/components/db.ts @@ -20,5 +20,6 @@ export const mockDb: jest.Mocked = { blockUsers: jest.fn(), unblockUsers: jest.fn(), getSentFriendshipRequestsCount: jest.fn(), + isFriendshipBlocked: jest.fn(), executeTx: jest.fn() } diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 21db6c2..392b75b 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -509,7 +509,7 @@ describe('db', () => { expect(mockPg.query).not.toHaveBeenCalled() }) - it.only('should query friendships for potential friends', async () => { + it('should query friendships for potential friends', async () => { const mockResult = { rows: [{ address: '0x456' }, { address: '0x789' }], rowCount: 2 @@ -632,6 +632,33 @@ describe('db', () => { }) }) + describe('isFriendshipBlocked', () => { + it('should check if exists a blocked friendship', async () => { + mockPg.query.mockResolvedValueOnce({ rows: [{ exists: true }], rowCount: 1 }) + await dbComponent.isFriendshipBlocked('0x123', '0x456') + + const expectedQuery = SQL` + SELECT EXISTS ( + SELECT 1 FROM blocks + WHERE (LOWER(blocker_address), LOWER(blocked_address)) IN ((${'0x123'}, ${'0x456'}), (${'0x456'}, ${'0x123'})) + ) + ` + + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining(expectedQuery.text), + values: expectedQuery.values + }) + ) + }) + + it.each([false, true])('should return %s if the friendship is %s', async (isBlocked: boolean) => { + mockPg.query.mockResolvedValueOnce({ rows: [{ exists: isBlocked }], rowCount: 1 }) + const result = await dbComponent.isFriendshipBlocked('0x123', '0x456') + expect(result).toBe(isBlocked) + }) + }) + describe('executeTx', () => { it('should execute a transaction successfully', async () => { const result = await dbComponent.executeTx(async (client) => { diff --git a/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts b/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts index d600a09..7ccf19c 100644 --- a/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts +++ b/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts @@ -121,6 +121,20 @@ describe('upsertFriendshipService', () => { }) }) + it('should return invalidFriendshipAction for a blocked friendship', async () => { + jest.spyOn(FriendshipsLogic, 'parseUpsertFriendshipRequest').mockReturnValueOnce(mockParsedRequest) + mockDb.isFriendshipBlocked.mockResolvedValueOnce(true) + + const result: UpsertFriendshipResponse = await upsertFriendship(mockRequest, rpcContext) + + expect(result).toEqual({ + response: { + $case: 'invalidFriendshipAction', + invalidFriendshipAction: {} + } + }) + }) + it('should return invalidFriendshipAction for an invalid action', async () => { jest.spyOn(FriendshipsLogic, 'parseUpsertFriendshipRequest').mockReturnValueOnce(mockParsedRequest) jest.spyOn(FriendshipsLogic, 'validateNewFriendshipAction').mockReturnValueOnce(false) @@ -177,12 +191,13 @@ describe('upsertFriendshipService', () => { jest.spyOn(FriendshipsLogic, 'validateNewFriendshipAction').mockReturnValueOnce(true) jest.spyOn(FriendshipsLogic, 'getNewFriendshipStatus').mockReturnValueOnce(FriendshipStatus.Requested) + mockDb.getLastFriendshipActionByUsers.mockResolvedValueOnce(null) mockDb.getFriendship.mockResolvedValueOnce(null) + mockCatalystClient.getProfiles.mockResolvedValueOnce([mockSenderProfile, mockReceiverProfile]) mockDb.createFriendship.mockResolvedValueOnce({ id: 'new-friendship-id', created_at: new Date() }) - mockCatalystClient.getProfiles.mockResolvedValueOnce([mockSenderProfile, mockReceiverProfile]) const result: UpsertFriendshipResponse = await upsertFriendship(mockRequest, rpcContext) diff --git a/test/unit/logic/friends.spec.ts b/test/unit/logic/friends.spec.ts index e772aad..0dce668 100644 --- a/test/unit/logic/friends.spec.ts +++ b/test/unit/logic/friends.spec.ts @@ -1,7 +1,7 @@ import { parseCatalystProfilesToProfiles, parseCatalystProfileToProfile } from '../../../src/logic/friends' import { mockProfile } from '../../mocks/profile' -describe('parseProfileToFriend', () => { +describe('parseCatalystProfileToProfile', () => { it('should parse profile to friend', () => { const friend = parseCatalystProfileToProfile(mockProfile) expect(friend).toEqual({ @@ -13,7 +13,7 @@ describe('parseProfileToFriend', () => { }) }) -describe('parseProfilesToFriends', () => { +describe('parseCatalystProfilesToProfiles', () => { it('should convert profiles to friend users', () => { const anotherProfile = { ...mockProfile, From 2b7b469b498c33a325a6cfeca5ab8319e6927740 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Mon, 3 Mar 2025 11:43:55 +0100 Subject: [PATCH 11/26] feat: Add implementation for GetBlockingStatus --- package.json | 2 +- src/adapters/db.ts | 23 ++++--- src/adapters/rpc-server/rpc-server.ts | 4 +- .../services/get-blocking-status.ts | 32 ++++++++++ src/logic/friends.ts | 8 +-- src/types.ts | 14 ++--- test/mocks/components/db.ts | 1 + test/mocks/friend.ts | 11 +++- test/unit/adapters/db.spec.ts | 18 +++++- .../services/get-blocked-users.spec.ts | 1 - .../services/get-blocking-status.spec.ts | 61 +++++++++++++++++++ yarn.lock | 12 ++-- 12 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 src/adapters/rpc-server/services/get-blocking-status.ts create mode 100644 test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts diff --git a/package.json b/package.json index ae4e12c..9b6ad71 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 50067f7..9c10471 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -7,8 +7,7 @@ import { FriendshipAction, FriendshipRequest, IDatabaseComponent, - Friend, - BlockedUser, + User, Pagination } from '../types' import { FRIENDSHIPS_PER_PAGE } from './rpc-server/constants' @@ -42,10 +41,15 @@ export function createDBComponent(components: Pick } } + async function getAddressesFromQuery(query: SQLStatement): Promise { + const result = await pg.query(query) + return result.rows.map((row) => row.address) + } + return { async getFriends(userAddress, { onlyActive, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 } } = {}) { const query: SQLStatement = getFriendsBaseQuery(userAddress, { onlyActive, pagination }) - const result = await pg.query(query) + const result = await pg.query(query) return result.rows }, async getFriendsCount(userAddress, { onlyActive } = { onlyActive: true }) { @@ -53,7 +57,7 @@ export function createDBComponent(components: Pick return getCount(query) }, async getMutualFriends(userAddress1, userAddress2, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 }) { - const result = await pg.query(getMutualFriendsBaseQuery(userAddress1, userAddress2, { pagination })) + const result = await pg.query(getMutualFriendsBaseQuery(userAddress1, userAddress2, { pagination })) return result.rows }, async getMutualFriendsCount(userAddress1, userAddress2) { @@ -164,7 +168,7 @@ export function createDBComponent(components: Pick ) AND is_active = true` - const results = await pg.query(query) + const results = await pg.query(query) return results.rows }, async blockUser(blockerAddress, blockedAddress) { @@ -207,8 +211,13 @@ export function createDBComponent(components: Pick const query = SQL` SELECT blocked_address as address FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} ` - const results = await pg.query(query) - return results.rows.map((row) => row.address) + return getAddressesFromQuery(query) + }, + async getBlockedByUsers(blockedAddress) { + const query = SQL` + SELECT blocker_address as address FROM blocks WHERE LOWER(blocked_address) = ${normalizeAddress(blockedAddress)} + ` + return getAddressesFromQuery(query) }, async isFriendshipBlocked(loggedUserAddress, anotherUserAddress) { const normalizedLoggedUserAddress = normalizeAddress(loggedUserAddress) diff --git a/src/adapters/rpc-server/rpc-server.ts b/src/adapters/rpc-server/rpc-server.ts index 145bb33..571b335 100644 --- a/src/adapters/rpc-server/rpc-server.ts +++ b/src/adapters/rpc-server/rpc-server.ts @@ -19,7 +19,7 @@ import { import { blockUserService } from './services/block-user' import { getBlockedUsersService } from './services/get-blocked-users' import { unblockUserService } from './services/unblock-user' - +import { getBlockingStatusService } from './services/get-blocking-status' export async function createRpcServerComponent({ logs, db, @@ -64,6 +64,7 @@ export async function createRpcServerComponent({ const blockUser = blockUserService({ components: { logs, db, catalystClient } }) const unblockUser = unblockUserService({ components: { logs, db, catalystClient } }) const getBlockedUsers = getBlockedUsersService({ components: { logs, db, catalystClient } }) + const getBlockingStatus = getBlockingStatusService({ components: { logs, db } }) rpcServer.setHandler(async function handler(port) { registerService(port, SocialServiceDefinition, async () => ({ @@ -76,6 +77,7 @@ export async function createRpcServerComponent({ blockUser, unblockUser, getBlockedUsers, + getBlockingStatus, subscribeToFriendshipUpdates, subscribeToFriendConnectivityUpdates })) diff --git a/src/adapters/rpc-server/services/get-blocking-status.ts b/src/adapters/rpc-server/services/get-blocking-status.ts new file mode 100644 index 0000000..563dad3 --- /dev/null +++ b/src/adapters/rpc-server/services/get-blocking-status.ts @@ -0,0 +1,32 @@ +import { Empty } from '@dcl/protocol/out-js/google/protobuf/empty.gen' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { GetBlockingStatusResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' + +export function getBlockingStatusService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { + const logger = logs.getLogger('get-blocking-status-service') + + return async function (_request: Empty, context: RpcServerContext): Promise { + const { address } = context + + try { + const [blockedAddresses, blockedByAddresses] = await Promise.all([ + db.getBlockedUsers(address), + db.getBlockedByUsers(address) + ]) + + return { + blockedUsers: blockedAddresses, + blockedByUsers: blockedByAddresses + } + } catch (error: any) { + logger.error(`Error getting blocking status: ${error.message}`, { + error: error.message, + stack: error.stack + }) + return { + blockedUsers: [], + blockedByUsers: [] + } + } + } +} diff --git a/src/logic/friends.ts b/src/logic/friends.ts index b210bac..55f8202 100644 --- a/src/logic/friends.ts +++ b/src/logic/friends.ts @@ -1,8 +1,8 @@ -import { Profile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { UserProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { getProfileName, getProfileHasClaimedName, getProfileUserId, getProfilePictureUrl } from './profiles' -import { Profile as LambdasProfile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' +import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -export function parseCatalystProfileToProfile(profile: LambdasProfile): Profile { +export function parseCatalystProfileToProfile(profile: Profile): UserProfile { const name = getProfileName(profile) const userId = getProfileUserId(profile) const hasClaimedName = getProfileHasClaimedName(profile) @@ -16,6 +16,6 @@ export function parseCatalystProfileToProfile(profile: LambdasProfile): Profile } } -export function parseCatalystProfilesToProfiles(profiles: LambdasProfile[]): Profile[] { +export function parseCatalystProfilesToProfiles(profiles: Profile[]): UserProfile[] { return profiles.map((profile) => parseCatalystProfileToProfile(profile)) } diff --git a/src/types.ts b/src/types.ts index 5d62a47..e5243f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,14 +84,14 @@ export interface IDatabaseComponent { pagination?: Pagination onlyActive?: boolean } - ): Promise + ): Promise getFriendsCount( userAddress: string, options?: { onlyActive?: boolean } ): Promise - getMutualFriends(userAddress1: string, userAddress2: string, pagination?: Pagination): Promise + getMutualFriends(userAddress1: string, userAddress2: string, pagination?: Pagination): Promise getMutualFriendsCount(userAddress1: string, userAddress2: string): Promise getFriendship(userAddresses: [string, string]): Promise getLastFriendshipActionByUsers(loggedUser: string, friendUser: string): Promise @@ -106,12 +106,13 @@ export interface IDatabaseComponent { getReceivedFriendshipRequestsCount(userAddress: string): Promise getSentFriendshipRequests(userAddress: string, pagination?: Pagination): Promise getSentFriendshipRequestsCount(userAddress: string): Promise - getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise + getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise blockUser(blockerAddress: string, blockedAddress: string): Promise unblockUser(blockerAddress: string, blockedAddress: string): Promise blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise getBlockedUsers(blockerAddress: string): Promise + getBlockedByUsers(blockedAddress: string): Promise isFriendshipBlocked(blockerAddress: string, blockedAddress: string): Promise executeTx(cb: (client: PoolClient) => Promise): Promise } @@ -258,15 +259,10 @@ export type Friendship = { updated_at: string } -export type Friend = { +export type User = { address: string } -export type BlockedUser = { - address: string - blocked_at: string -} - export enum Action { REQUEST = 'request', // request a friendship CANCEL = 'cancel', // cancel a friendship request diff --git a/test/mocks/components/db.ts b/test/mocks/components/db.ts index 71c5b62..3b6d3e3 100644 --- a/test/mocks/components/db.ts +++ b/test/mocks/components/db.ts @@ -15,6 +15,7 @@ export const mockDb: jest.Mocked = { getReceivedFriendshipRequestsCount: jest.fn(), getSentFriendshipRequests: jest.fn(), getBlockedUsers: jest.fn(), + getBlockedByUsers: jest.fn(), blockUser: jest.fn(), unblockUser: jest.fn(), blockUsers: jest.fn(), diff --git a/test/mocks/friend.ts b/test/mocks/friend.ts index abf186f..221d1a2 100644 --- a/test/mocks/friend.ts +++ b/test/mocks/friend.ts @@ -1,8 +1,13 @@ -import { Friend } from '../../src/types' -import { getProfileHasClaimedName, getProfileName, getProfilePictureUrl, getProfileUserId } from '../../src/logic/profiles' +import { User } from '../../src/types' +import { + getProfileHasClaimedName, + getProfileName, + getProfilePictureUrl, + getProfileUserId +} from '../../src/logic/profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -export const createMockFriend = (address: string): Friend => ({ +export const createMockFriend = (address: string): User => ({ address }) diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 392b75b..68e11e4 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -625,7 +625,23 @@ describe('db', () => { expect(result).toEqual(mockBlockedUsers.map((user) => user.address)) expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining('LOWER(blocker_address) ='), + text: expect.stringContaining('SELECT blocked_address as address FROM blocks WHERE LOWER(blocker_address) ='), + values: expect.arrayContaining([normalizeAddress('0x123')]) + }) + ) + }) + }) + + describe('getBlockedByUsers', () => { + it('should retrieve blocked by users', async () => { + const mockBlockedByUsers = [{ address: '0x456' }, { address: '0x789' }] + mockPg.query.mockResolvedValueOnce({ rows: mockBlockedByUsers, rowCount: mockBlockedByUsers.length }) + + const result = await dbComponent.getBlockedByUsers('0x123') + expect(result).toEqual(mockBlockedByUsers.map((user) => user.address)) + expect(mockPg.query).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining('SELECT blocker_address as address FROM blocks WHERE LOWER(blocked_address) ='), values: expect.arrayContaining([normalizeAddress('0x123')]) }) ) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index a4e9071..d86a592 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -4,7 +4,6 @@ import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' import { parseCatalystProfilesToProfiles } from '../../../../../src/logic/friends' -import { FRIENDSHIPS_PER_PAGE } from '../../../../../src/adapters/rpc-server/constants' describe('getBlockedUsersService', () => { let getBlockedUsers: ReturnType diff --git a/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts new file mode 100644 index 0000000..2b45d5b --- /dev/null +++ b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts @@ -0,0 +1,61 @@ +import { mockDb, mockLogs } from '../../../../mocks/components' +import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { RpcServerContext } from '../../../../../src/types' +import { createMockProfile } from '../../../../mocks/profile' +import { parseCatalystProfilesToProfiles } from '../../../../../src/logic/friends' +import { getBlockingStatusService } from '../../../../../src/adapters/rpc-server/services/get-blocking-status' + +describe('getBlockingStatusService', () => { + let getBlockingStatus: ReturnType + + const rpcContext: RpcServerContext = { + address: '0x123', + subscribersContext: undefined + } + + beforeEach(() => { + getBlockingStatus = getBlockingStatusService({ + components: { db: mockDb, logs: mockLogs } + }) + }) + + it('should return blocked users and blocked by users addresses', async () => { + const blockedAddresses = ['0x456', '0x789'] + const blockedByAddresses = ['0x123', '0x456'] + + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) + mockDb.getBlockedByUsers.mockResolvedValueOnce(blockedByAddresses) + const response = await getBlockingStatus({}, rpcContext) + + expect(response).toEqual({ + blockedUsers: blockedAddresses, + blockedByUsers: blockedByAddresses + }) + }) + + it('should handle errors in the getBlockedUsers db call gracefully', async () => { + const error = new Error('Database error') + + mockDb.getBlockedUsers.mockRejectedValueOnce(error) + + const response = await getBlockingStatus({}, rpcContext) + + expect(response).toEqual({ + blockedUsers: [], + blockedByUsers: [] + }) + }) + + it('should handle errors in the getBlockedByUsers db call gracefully', async () => { + const error = new Error('Database error') + + mockDb.getBlockedByUsers.mockRejectedValueOnce(error) + + const response = await getBlockingStatus({}, rpcContext) + + expect(response).toEqual({ + blockedUsers: [], + blockedByUsers: [] + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 0926047..320128f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,15 +756,15 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13524950006.commit-c63ab51.tgz": - version "1.0.0-13524950006.commit-c63ab51" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13524950006.commit-c63ab51.tgz#7f5abf55286fa6cc2661e35775942f35d92bbec2" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628460474.commit-45c04a2.tgz": + version "1.0.0-13628460474.commit-45c04a2" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628460474.commit-45c04a2.tgz#1d3d8d29ad3a3bb2bed9515653547b46b95d6328" dependencies: "@dcl/ts-proto" "1.154.0" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz": - version "1.0.0-13525612589.commit-f273ffa" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13525612589.commit-f273ffa.tgz#c1423467b62944d864b0141a90dfd866e47cae60" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz": + version "1.0.0-13628710700.commit-b858bae" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz#b58dac1d1acab2d5a7971fe7e3ed7abe673c9140" dependencies: "@dcl/ts-proto" "1.154.0" From 6e11ef88c84e541eff4879718d5bfde91e308a80 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Mon, 3 Mar 2025 12:44:29 +0100 Subject: [PATCH 12/26] feat: Block / Unblock also impact in the friendship history --- src/adapters/db.ts | 27 +++--- .../rpc-server/services/block-user.ts | 18 +++- .../rpc-server/services/get-blocked-users.ts | 4 +- .../rpc-server/services/get-friends.ts | 4 +- .../rpc-server/services/get-mutual-friends.ts | 4 +- ...ubscribe-to-friend-connectivity-updates.ts | 4 +- .../rpc-server/services/unblock-user.ts | 15 ++- src/logic/friends.ts | 6 +- src/logic/friendships.ts | 15 +-- src/types.ts | 9 +- test/unit/adapters/db.spec.ts | 96 ++++++++++++++----- .../rpc-server/services/block-user.spec.ts | 55 +++++++++-- .../services/get-blocked-users.spec.ts | 6 +- .../services/get-blocking-status.spec.ts | 3 - ...ibe-to-friend-connectivity-updates.spec.ts | 12 +-- .../subscribe-to-friendship-updates.spec.ts | 10 +- .../rpc-server/services/unblock-user.spec.ts | 53 ++++++++-- .../services/upsert-friendship.spec.ts | 2 +- test/unit/logic/friends.spec.ts | 10 +- test/unit/logic/friendships.spec.ts | 8 +- 20 files changed, 253 insertions(+), 108 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 9c10471..2c60b84 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -63,15 +63,11 @@ export function createDBComponent(components: Pick async getMutualFriendsCount(userAddress1, userAddress2) { return getCount(getMutualFriendsBaseQuery(userAddress1, userAddress2, { onlyCount: true })) }, - async getFriendship(users) { + async getFriendship(users, txClient) { const [userAddress1, userAddress2] = users.map(normalizeAddress) - const query = SQL` - SELECT * FROM friendships - WHERE (LOWER(address_requester), LOWER(address_requested)) IN ((${userAddress1}, ${userAddress2}), (${userAddress2}, ${userAddress1})) - ` - - const results = await pg.query(query) + const query = SQL`SELECT * FROM friendships WHERE (LOWER(address_requester), LOWER(address_requested)) IN ((${userAddress1}, ${userAddress2}), (${userAddress2}, ${userAddress1}))` + const results = txClient ? await txClient.query(query) : await pg.query(query) return results.rows[0] }, async getLastFriendshipActionByUsers(loggedUser: string, friendUser: string) { @@ -171,20 +167,29 @@ export function createDBComponent(components: Pick const results = await pg.query(query) return results.rows }, - async blockUser(blockerAddress, blockedAddress) { + async blockUser(blockerAddress, blockedAddress, txClient) { const query = SQL` INSERT INTO blocks (id, blocker_address, blocked_address) VALUES (${randomUUID()}, ${normalizeAddress(blockerAddress)}, ${normalizeAddress(blockedAddress)}) ON CONFLICT DO NOTHING` - await pg.query(query) + + if (txClient) { + await txClient.query(query) + } else { + await pg.query(query) + } }, - async unblockUser(blockerAddress, blockedAddress) { + async unblockUser(blockerAddress, blockedAddress, txClient) { const query = SQL` DELETE FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} AND LOWER(blocked_address) = ${normalizeAddress(blockedAddress)} ` - await pg.query(query) + if (txClient) { + await txClient.query(query) + } else { + await pg.query(query) + } }, async blockUsers(blockerAddress, blockedAddresses) { const query = SQL`INSERT INTO blocks (id, blocker_address, blocked_address) VALUES ` diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 41531c2..eb20acc 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -1,5 +1,5 @@ -import { parseCatalystProfileToProfile } from '../../../logic/friends' -import { RpcServerContext, RPCServiceContext } from '../../../types' +import { parseProfileToUserProfile } from '../../../logic/friends' +import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { BlockUserPayload, BlockUserResponse @@ -37,13 +37,23 @@ export function blockUserService({ } } - await db.blockUser(blockerAddress, blockedAddress) + await db.executeTx(async (tx) => { + await db.blockUser(blockerAddress, blockedAddress, tx) + + const friendship = await db.getFriendship([blockerAddress, blockedAddress], tx) + if (!friendship) return + + await Promise.all([ + db.updateFriendshipStatus(friendship.id, false, tx), + db.recordFriendshipAction(friendship.id, blockerAddress, Action.BLOCK, null, tx) + ]) + }) return { response: { $case: 'ok', ok: { - profile: parseCatalystProfileToProfile(profile) + profile: parseProfileToUserProfile(profile) } } } diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts index 2589c80..1e21993 100644 --- a/src/adapters/rpc-server/services/get-blocked-users.ts +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -1,4 +1,4 @@ -import { parseCatalystProfilesToProfiles } from '../../../logic/friends' +import { parseProfilesToUserProfiles } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -21,7 +21,7 @@ export function getBlockedUsersService({ const profiles = await catalystClient.getProfiles(blockedAddresses) return { - profiles: parseCatalystProfilesToProfiles(profiles), + profiles: parseProfilesToUserProfiles(profiles), paginationData: { total: blockedAddresses.length, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts index 06e4ddf..18c871e 100644 --- a/src/adapters/rpc-server/services/get-friends.ts +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -1,4 +1,4 @@ -import { parseCatalystProfilesToProfiles } from '../../../logic/friends' +import { parseProfilesToUserProfiles } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -28,7 +28,7 @@ export function getFriendsService({ const profiles = await catalystClient.getProfiles(friends.map((friend) => friend.address)) return { - friends: parseCatalystProfilesToProfiles(profiles), + friends: parseProfilesToUserProfiles(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-mutual-friends.ts b/src/adapters/rpc-server/services/get-mutual-friends.ts index 94df142..95bd452 100644 --- a/src/adapters/rpc-server/services/get-mutual-friends.ts +++ b/src/adapters/rpc-server/services/get-mutual-friends.ts @@ -6,7 +6,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { normalizeAddress } from '../../../utils/address' import { getPage } from '../../../utils/pagination' -import { parseCatalystProfilesToProfiles } from '../../../logic/friends' +import { parseProfilesToUserProfiles } from '../../../logic/friends' export function getMutualFriendsService({ components: { logs, db, catalystClient } @@ -30,7 +30,7 @@ export function getMutualFriendsService({ const profiles = await catalystClient.getProfiles(mutualFriends.map((friend) => friend.address)) return { - friends: parseCatalystProfilesToProfiles(profiles), + friends: parseProfilesToUserProfiles(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts index 11ff0d5..38de976 100644 --- a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts +++ b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts @@ -5,7 +5,7 @@ import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { parseEmittedUpdateToFriendConnectivityUpdate } from '../../../logic/friendships' -import { parseCatalystProfilesToProfiles } from '../../../logic/friends' +import { parseProfilesToUserProfiles } from '../../../logic/friends' import { handleSubscriptionUpdates } from '../../../logic/updates' export function subscribeToFriendConnectivityUpdatesService({ @@ -21,7 +21,7 @@ export function subscribeToFriendConnectivityUpdatesService({ const onlineFriends = await db.getOnlineFriends(context.address, onlinePeers) const profiles = await catalystClient.getProfiles(onlineFriends.map((friend) => friend.address)) - const parsedProfiles = parseCatalystProfilesToProfiles(profiles).map((friend) => ({ + const parsedProfiles = parseProfilesToUserProfiles(profiles).map((friend) => ({ friend, status: ConnectivityStatus.ONLINE })) diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index c5558f9..40e184e 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -1,5 +1,5 @@ -import { parseCatalystProfileToProfile } from '../../../logic/friends' -import { RpcServerContext, RPCServiceContext } from '../../../types' +import { parseProfileToUserProfile } from '../../../logic/friends' +import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { UnblockUserPayload, UnblockUserResponse @@ -35,13 +35,20 @@ export function unblockUserService({ } } - await db.unblockUser(blockerAddress, blockedAddress) + await db.executeTx(async (tx) => { + await db.unblockUser(blockerAddress, blockedAddress, tx) + + const friendship = await db.getFriendship([blockerAddress, blockedAddress], tx) + if (!friendship) return + + await db.recordFriendshipAction(friendship.id, blockerAddress, Action.DELETE, null, tx) + }) return { response: { $case: 'ok', ok: { - profile: parseCatalystProfileToProfile(profile) + profile: parseProfileToUserProfile(profile) } } } diff --git a/src/logic/friends.ts b/src/logic/friends.ts index 55f8202..7258c3c 100644 --- a/src/logic/friends.ts +++ b/src/logic/friends.ts @@ -2,7 +2,7 @@ import { UserProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2 import { getProfileName, getProfileHasClaimedName, getProfileUserId, getProfilePictureUrl } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -export function parseCatalystProfileToProfile(profile: Profile): UserProfile { +export function parseProfileToUserProfile(profile: Profile): UserProfile { const name = getProfileName(profile) const userId = getProfileUserId(profile) const hasClaimedName = getProfileHasClaimedName(profile) @@ -16,6 +16,6 @@ export function parseCatalystProfileToProfile(profile: Profile): UserProfile { } } -export function parseCatalystProfilesToProfiles(profiles: Profile[]): UserProfile[] { - return profiles.map((profile) => parseCatalystProfileToProfile(profile)) +export function parseProfilesToUserProfiles(profiles: Profile[]): UserProfile[] { + return profiles.map((profile) => parseProfileToUserProfile(profile)) } diff --git a/src/logic/friendships.ts b/src/logic/friendships.ts index 004c174..50d1dfc 100644 --- a/src/logic/friendships.ts +++ b/src/logic/friendships.ts @@ -7,7 +7,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, FriendshipAction, FriendshipRequest, FriendshipStatus, SubscriptionEventsEmitter } from '../types' import { normalizeAddress } from '../utils/address' -import { parseCatalystProfileToProfile } from './friends' +import { parseProfileToUserProfile } from './friends' import { getProfileUserId } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' @@ -17,7 +17,8 @@ export const FRIENDSHIP_ACTION_TRANSITIONS: Record = [Action.ACCEPT]: [Action.REQUEST], [Action.CANCEL]: [Action.REQUEST], [Action.REJECT]: [Action.REQUEST], - [Action.DELETE]: [Action.ACCEPT] + [Action.DELETE]: [Action.ACCEPT, Action.BLOCK], + [Action.BLOCK]: [Action.REQUEST, Action.CANCEL, Action.REJECT, Action.DELETE, Action.ACCEPT, null] } const FRIENDSHIP_STATUS_BY_ACTION: Record< @@ -29,8 +30,8 @@ const FRIENDSHIP_STATUS_BY_ACTION: Record< [Action.DELETE]: () => FriendshipRequestStatus.DELETED, [Action.REJECT]: () => FriendshipRequestStatus.REJECTED, [Action.REQUEST]: (actingUser, contextAddress) => - actingUser === contextAddress ? FriendshipRequestStatus.REQUEST_SENT : FriendshipRequestStatus.REQUEST_RECEIVED - // TODO: [Action.BLOCK]: () => FriendshipRequestStatus.BLOCKED, + actingUser === contextAddress ? FriendshipRequestStatus.REQUEST_SENT : FriendshipRequestStatus.REQUEST_RECEIVED, + [Action.BLOCK]: () => FriendshipRequestStatus.BLOCKED } export function isFriendshipActionValid(from: Action | null, to: Action) { @@ -142,7 +143,7 @@ export function parseEmittedUpdateToFriendshipUpdate( request: { id: update.id, createdAt: update.timestamp, - friend: parseCatalystProfileToProfile(profile), + friend: parseProfileToUserProfile(profile), message: update.metadata?.message } } @@ -202,7 +203,7 @@ export function parseEmittedUpdateToFriendConnectivityUpdate( ): FriendConnectivityUpdate | null { const { status } = update return { - friend: parseCatalystProfileToProfile(profile), + friend: parseProfileToUserProfile(profile), status } } @@ -224,7 +225,7 @@ export function parseFriendshipRequestToFriendshipRequestResponse( ): FriendshipRequestResponse { return { id: request.id, - friend: parseCatalystProfileToProfile(profile), + friend: parseProfileToUserProfile(profile), createdAt: new Date(request.timestamp).getTime(), message: request.metadata?.message || '' } diff --git a/src/types.ts b/src/types.ts index e5243f5..3256b9b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,7 +93,7 @@ export interface IDatabaseComponent { ): Promise getMutualFriends(userAddress1: string, userAddress2: string, pagination?: Pagination): Promise getMutualFriendsCount(userAddress1: string, userAddress2: string): Promise - getFriendship(userAddresses: [string, string]): Promise + getFriendship(userAddresses: [string, string], txClient?: PoolClient): Promise getLastFriendshipActionByUsers(loggedUser: string, friendUser: string): Promise recordFriendshipAction( friendshipId: string, @@ -107,8 +107,8 @@ export interface IDatabaseComponent { getSentFriendshipRequests(userAddress: string, pagination?: Pagination): Promise getSentFriendshipRequestsCount(userAddress: string): Promise getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise - blockUser(blockerAddress: string, blockedAddress: string): Promise - unblockUser(blockerAddress: string, blockedAddress: string): Promise + blockUser(blockerAddress: string, blockedAddress: string, txClient?: PoolClient): Promise + unblockUser(blockerAddress: string, blockedAddress: string, txClient?: PoolClient): Promise blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise getBlockedUsers(blockerAddress: string): Promise @@ -268,7 +268,8 @@ export enum Action { CANCEL = 'cancel', // cancel a friendship request ACCEPT = 'accept', // accept a friendship request REJECT = 'reject', // reject a friendship request - DELETE = 'delete' // delete a friendship + DELETE = 'delete', // delete a friendship + BLOCK = 'block' // block a user } export type FriendshipAction = { diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 68e11e4..093a724 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -3,7 +3,7 @@ import { Action } from '../../../src/types' import SQL from 'sql-template-strings' import { mockLogs, mockPg } from '../../mocks/components' import { normalizeAddress } from '../../../src/utils/address' - +import { PoolClient } from 'pg' jest.mock('node:crypto', () => ({ randomUUID: jest.fn().mockReturnValue('mock-uuid') })) @@ -290,15 +290,24 @@ describe('db', () => { }) describe('createFriendship', () => { - it('should create a new friendship', async () => { - mockPg.query.mockResolvedValueOnce({ + it.each([false, true])('should create a new friendship using txs: %s', async (withTxClient: boolean) => { + const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + const queryResult = { rows: [{ id: 'friendship-1', created_at: '2025-01-01T00:00:00.000Z' }], rowCount: 1 - }) + } + + if (withTxClient) { + mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) + } else { + mockPg.query.mockResolvedValueOnce(queryResult) + } - const result = await dbComponent.createFriendship(['0x123', '0x456'], true) + const result = await dbComponent.createFriendship(['0x123', '0x456'], true, mockClient) - expect(mockPg.query).toHaveBeenCalledWith( + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining( 'INSERT INTO friendships (id, address_requester, address_requested, is_active)' @@ -306,7 +315,7 @@ describe('db', () => { values: expect.arrayContaining([expect.any(String), '0x123', '0x456', true]) }) ) - expect(mockPg.query).toHaveBeenCalledWith( + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('RETURNING id, created_at') }) @@ -316,15 +325,24 @@ describe('db', () => { }) describe('updateFriendshipStatus', () => { - it('should update friendship status', async () => { - mockPg.query.mockResolvedValueOnce({ + it.each([false, true])('should update friendship status using txs: %s', async (withTxClient: boolean) => { + const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + const queryResult = { rowCount: 1, rows: [{ id: 'friendship-id', created_at: '2025-01-01T00:00:00.000Z' }] - }) + } - const result = await dbComponent.updateFriendshipStatus('friendship-id', false) + if (withTxClient) { + mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) + } else { + mockPg.query.mockResolvedValueOnce(queryResult) + } - expect(mockPg.query).toHaveBeenCalledWith( + const result = await dbComponent.updateFriendshipStatus('friendship-id', false, mockClient) + + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + + expect(queryToAssert).toHaveBeenCalledWith( SQL`UPDATE friendships SET is_active = ${false}, updated_at = now() WHERE id = ${'friendship-id'} RETURNING id, created_at` ) expect(result).toEqual({ @@ -335,18 +353,40 @@ describe('db', () => { }) describe('getFriendship', () => { - it('should retrieve a specific friendship', async () => { + it.each([true])('should retrieve a specific friendship using txs: %s', async (withTxClient: boolean) => { + const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined const mockFriendship = { id: 'friendship-1', address_requester: '0x123', address_requested: '0x456', is_active: true } - mockPg.query.mockResolvedValueOnce({ rows: [mockFriendship], rowCount: 1 }) - const result = await dbComponent.getFriendship(['0x123', '0x456']) + const queryResult = { + rows: [mockFriendship], + rowCount: 1 + } + + if (withTxClient) { + mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) + } else { + mockPg.query.mockResolvedValueOnce(queryResult) + } + + const result = await dbComponent.getFriendship(['0x123', '0x456'], mockClient) expect(result).toEqual(mockFriendship) + + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + + expect(queryToAssert).toHaveBeenCalledWith( + expect.objectContaining({ + text: expect.stringContaining( + 'SELECT * FROM friendships WHERE (LOWER(address_requester), LOWER(address_requested)) IN' + ), + values: expect.arrayContaining(['0x123', '0x456', '0x456', '0x123']) + }) + ) }) }) @@ -485,7 +525,8 @@ describe('db', () => { ) expect(result).toBe('mock-uuid') - expect(withTxClient ? mockClient.query : mockPg.query).toHaveBeenCalledWith( + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining( 'INSERT INTO friendship_actions (id, friendship_id, action, acting_user, metadata)' @@ -546,16 +587,20 @@ describe('db', () => { }) describe('blockUser', () => { - it('should block a user', async () => { - await dbComponent.blockUser('0x123', '0x456') - expect(mockPg.query).toHaveBeenCalledWith( + it.each([false, true])('should block a user using txs: %s', async (withTxClient: boolean) => { + const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + await dbComponent.blockUser('0x123', '0x456', mockClient) + + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('INSERT INTO blocks (id, blocker_address, blocked_address)'), values: expect.arrayContaining([expect.any(String), normalizeAddress('0x123'), normalizeAddress('0x456')]) }) ) - expect(mockPg.query).toHaveBeenCalledWith( + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('ON CONFLICT DO NOTHING') }) @@ -564,14 +609,17 @@ describe('db', () => { }) describe('unblockUser', () => { - it('should unblock a user', async () => { - await dbComponent.unblockUser('0x123', '0x456') + it.each([false, true])('should unblock a user using txs: %s', async (withTxClient: boolean) => { + const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + await dbComponent.unblockUser('0x123', '0x456', mockClient) const expectedQuery = SQL` DELETE FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress('0x123')} AND LOWER(blocked_address) = ${normalizeAddress('0x456')}` - expect(mockPg.query).toHaveBeenCalledWith( + const queryToAssert = withTxClient ? mockClient.query : mockPg.query + + expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining(expectedQuery.text), values: expectedQuery.values @@ -625,7 +673,7 @@ describe('db', () => { expect(result).toEqual(mockBlockedUsers.map((user) => user.address)) expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining('SELECT blocked_address as address FROM blocks WHERE LOWER(blocker_address) ='), + text: expect.stringContaining('LOWER(blocker_address) ='), values: expect.arrayContaining([normalizeAddress('0x123')]) }) ) diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 308f1ea..14f9afa 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -1,25 +1,30 @@ -import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components' +import { mockCatalystClient, mockDb, mockLogs, mockPg } from '../../../../mocks/components' import { blockUserService } from '../../../../../src/adapters/rpc-server/services/block-user' import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -import { RpcServerContext } from '../../../../../src/types' +import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' +import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { PoolClient } from 'pg' describe('blockUserService', () => { let blockUser: ReturnType + let mockClient: jest.Mocked const rpcContext: RpcServerContext = { address: '0x123', subscribersContext: undefined } - beforeEach(() => { + beforeEach(async () => { blockUser = blockUserService({ components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } }) + + mockClient = (await mockPg.getPool().connect()) as jest.Mocked + mockDb.executeTx.mockImplementationOnce(async (cb) => cb(mockClient)) }) - it('should block a user successfully', async () => { + it('should block a user successfully, update friendship status, and record friendship action if it exists', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { @@ -27,6 +32,7 @@ describe('blockUserService', () => { } mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) const response = await blockUser(request, rpcContext) @@ -34,12 +40,45 @@ describe('blockUserService', () => { response: { $case: 'ok', ok: { - profile: parseCatalystProfileToProfile(mockProfile) + profile: parseProfileToUserProfile(mockProfile) } } }) - expect(mockDb.blockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) - expect(mockLogs.getLogger('block-user-service')).toBeDefined() + expect(mockDb.blockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress, mockClient) + expect(mockDb.getFriendship).toHaveBeenCalledWith([rpcContext.address, blockedAddress], mockClient) + expect(mockDb.updateFriendshipStatus).toHaveBeenCalledWith(expect.any(String), false, mockClient) + expect(mockDb.recordFriendshipAction).toHaveBeenCalledWith( + expect.any(String), + rpcContext.address, + Action.BLOCK, + null, + mockClient + ) + }) + + it('should block a user successfully and do nothing else if friendship does not exist', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce(null) + + const response = await blockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'ok', + ok: { profile: parseProfileToUserProfile(mockProfile) } + } + }) + + expect(mockDb.blockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress, mockClient) + expect(mockDb.getFriendship).toHaveBeenCalledWith([rpcContext.address, blockedAddress], mockClient) + expect(mockDb.updateFriendshipStatus).not.toHaveBeenCalled() + expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) it('should return internalServerError when user address is missing', async () => { diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index d86a592..1cd1dca 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -3,7 +3,7 @@ import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/s import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseCatalystProfilesToProfiles } from '../../../../../src/logic/friends' +import { parseProfilesToUserProfiles } from '../../../../../src/logic/friends' describe('getBlockedUsersService', () => { let getBlockedUsers: ReturnType @@ -32,7 +32,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response).toEqual({ - profiles: parseCatalystProfilesToProfiles(mockProfiles), + profiles: parseProfilesToUserProfiles(mockProfiles), paginationData: { total: blockedAddresses.length, page: 1 @@ -52,7 +52,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response.paginationData.page).toBe(1) - expect(response.profiles).toEqual(parseCatalystProfilesToProfiles(mockProfiles)) + expect(response.profiles).toEqual(parseProfilesToUserProfiles(mockProfiles)) }) it('should handle errors gracefully', async () => { diff --git a/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts index 2b45d5b..4108aeb 100644 --- a/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts @@ -1,8 +1,5 @@ import { mockDb, mockLogs } from '../../../../mocks/components' -import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' -import { createMockProfile } from '../../../../mocks/profile' -import { parseCatalystProfilesToProfiles } from '../../../../../src/logic/friends' import { getBlockingStatusService } from '../../../../../src/adapters/rpc-server/services/get-blocking-status' describe('getBlockingStatusService', () => { diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts index 14b2198..1de120a 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts @@ -4,7 +4,7 @@ import { mockLogs, mockArchipelagoStats, mockDb, mockConfig, mockCatalystClient import { subscribeToFriendConnectivityUpdatesService } from '../../../../../src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates' import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockProfile } from '../../../../mocks/profile' -import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' +import { parseProfileToUserProfile } from '../../../../../src/logic/friends' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' @@ -42,7 +42,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockArchipelagoStats.getPeers.mockResolvedValue(['0x456', '0x789']) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -52,7 +52,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { expect(mockArchipelagoStats.getPeersFromCache).toHaveBeenCalled() expect(result.value).toEqual({ - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE }) @@ -65,7 +65,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -107,7 +107,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -125,7 +125,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts index 1c24ac8..f4385cb 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts @@ -4,7 +4,7 @@ import { Action, RpcServerContext } from '../../../../../src/types' import { mockCatalystClient, mockLogs } from '../../../../mocks/components' import { createMockProfile } from '../../../../mocks/profile' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' -import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' +import { parseProfileToUserProfile } from '../../../../../src/logic/friends' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' jest.mock('../../../../../src/logic/updates') @@ -41,7 +41,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should handle subscription updates', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -51,7 +51,7 @@ describe('subscribeToFriendshipUpdatesService', () => { const result = await generator.next() expect(result.value).toEqual({ - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp }) @@ -84,7 +84,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should get the proper address from the update', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -100,7 +100,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should filter updates based on address conditions', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseCatalystProfileToProfile(mockFriendProfile), + friend: parseProfileToUserProfile(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index 32cb3ac..c97ebc6 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -1,25 +1,30 @@ -import { mockCatalystClient, mockDb, mockLogs } from '../../../../mocks/components' +import { mockCatalystClient, mockDb, mockLogs, mockPg } from '../../../../mocks/components' import { unblockUserService } from '../../../../../src/adapters/rpc-server/services/unblock-user' import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -import { RpcServerContext } from '../../../../../src/types' +import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseCatalystProfileToProfile } from '../../../../../src/logic/friends' +import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { PoolClient } from 'pg' describe('unblockUserService', () => { let unblockUser: ReturnType + let mockClient: jest.Mocked const rpcContext: RpcServerContext = { address: '0x123', subscribersContext: undefined } - beforeEach(() => { + beforeEach(async () => { unblockUser = unblockUserService({ components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } }) + + mockClient = (await mockPg.getPool().connect()) as jest.Mocked + mockDb.executeTx.mockImplementationOnce(async (cb) => cb(mockClient)) }) - it('should unblock a user successfully', async () => { + it('should unblock a user successfully and mark as deleted if friendship exists', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { @@ -27,6 +32,7 @@ describe('unblockUserService', () => { } mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) const response = await unblockUser(request, rpcContext) @@ -34,12 +40,43 @@ describe('unblockUserService', () => { response: { $case: 'ok', ok: { - profile: parseCatalystProfileToProfile(mockProfile) + profile: parseProfileToUserProfile(mockProfile) } } }) - expect(mockDb.unblockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress) - expect(mockLogs.getLogger('unblock-user-service')).toBeDefined() + expect(mockDb.unblockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress, mockClient) + expect(mockDb.getFriendship).toHaveBeenCalledWith([rpcContext.address, blockedAddress], mockClient) + expect(mockDb.recordFriendshipAction).toHaveBeenCalledWith( + expect.any(String), + rpcContext.address, + Action.DELETE, + null, + mockClient + ) + }) + + it('should unblock a user successfully and do nothing else if friendship does not exist', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce(null) + + const response = await unblockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'ok', + ok: { profile: parseProfileToUserProfile(mockProfile) } + } + }) + + expect(mockDb.unblockUser).toHaveBeenCalledWith(rpcContext.address, blockedAddress, mockClient) + expect(mockDb.getFriendship).toHaveBeenCalledWith([rpcContext.address, blockedAddress], mockClient) + expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) it('should return internalServerError when user address is missing', async () => { diff --git a/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts b/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts index 7ccf19c..5102437 100644 --- a/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts +++ b/test/unit/adapters/rpc-server/services/upsert-friendship.spec.ts @@ -106,7 +106,7 @@ describe('upsertFriendshipService', () => { action: Action.REQUEST, user: rpcContext.address, metadata: { message: 'Hello' } - } + } as const jest.spyOn(FriendshipsLogic, 'parseUpsertFriendshipRequest').mockReturnValueOnce(mockSelfRequestParsed) const result: UpsertFriendshipResponse = await upsertFriendship(mockSelfRequest, rpcContext) diff --git a/test/unit/logic/friends.spec.ts b/test/unit/logic/friends.spec.ts index 0dce668..2999dec 100644 --- a/test/unit/logic/friends.spec.ts +++ b/test/unit/logic/friends.spec.ts @@ -1,9 +1,9 @@ -import { parseCatalystProfilesToProfiles, parseCatalystProfileToProfile } from '../../../src/logic/friends' +import { parseProfilesToUserProfiles, parseProfileToUserProfile } from '../../../src/logic/friends' import { mockProfile } from '../../mocks/profile' -describe('parseCatalystProfileToProfile', () => { +describe('parseProfileToUserProfile', () => { it('should parse profile to friend', () => { - const friend = parseCatalystProfileToProfile(mockProfile) + const friend = parseProfileToUserProfile(mockProfile) expect(friend).toEqual({ address: mockProfile.avatars[0].userId, name: mockProfile.avatars[0].name, @@ -13,7 +13,7 @@ describe('parseCatalystProfileToProfile', () => { }) }) -describe('parseCatalystProfilesToProfiles', () => { +describe('parseProfilesToUserProfiles', () => { it('should convert profiles to friend users', () => { const anotherProfile = { ...mockProfile, @@ -28,7 +28,7 @@ describe('parseCatalystProfilesToProfiles', () => { } const profiles = [mockProfile, anotherProfile] - const result = parseCatalystProfilesToProfiles(profiles) + const result = parseProfilesToUserProfiles(profiles) expect(result).toEqual([ { diff --git a/test/unit/logic/friendships.spec.ts b/test/unit/logic/friendships.spec.ts index 750931c..a464633 100644 --- a/test/unit/logic/friendships.spec.ts +++ b/test/unit/logic/friendships.spec.ts @@ -17,7 +17,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockExpectedFriendshipRequest, createMockFriendshipRequest } from '../../mocks/friendship-request' import { createMockProfile, mockProfile } from '../../mocks/profile' -import { parseCatalystProfileToProfile } from '../../../src/logic/friends' +import { parseProfileToUserProfile } from '../../../src/logic/friends' describe('isFriendshipActionValid()', () => { test('it should be valid if from is null and to is REQUEST ', () => { @@ -399,7 +399,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseCatalystProfileToProfile(mockProfile), + friend: parseProfileToUserProfile(mockProfile), message: undefined } } @@ -425,7 +425,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseCatalystProfileToProfile(mockProfile), + friend: parseProfileToUserProfile(mockProfile), message: 'Hi!' } } @@ -589,7 +589,7 @@ describe('parseEmittedUpdateToFriendConnectivityUpdate()', () => { ])('it should parse status %s update properly', (status) => { const update = { address: '0x123', status } expect(parseEmittedUpdateToFriendConnectivityUpdate(update, mockProfile)).toEqual({ - friend: parseCatalystProfileToProfile(mockProfile), + friend: parseProfileToUserProfile(mockProfile), status }) }) From b3d196616a0846f27ff30edd7edc6d81fd46f69c Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Mon, 3 Mar 2025 14:53:49 +0100 Subject: [PATCH 13/26] refactor: Back to FriendProfile --- package.json | 2 +- src/adapters/rpc-server/services/block-user.ts | 4 ++-- .../rpc-server/services/get-blocked-users.ts | 4 ++-- src/adapters/rpc-server/services/get-friends.ts | 4 ++-- .../rpc-server/services/get-mutual-friends.ts | 4 ++-- .../subscribe-to-friend-connectivity-updates.ts | 4 ++-- src/adapters/rpc-server/services/unblock-user.ts | 4 ++-- src/logic/friends.ts | 8 ++++---- src/logic/friendships.ts | 8 ++++---- .../adapters/rpc-server/services/block-user.spec.ts | 6 +++--- .../rpc-server/services/get-blocked-users.spec.ts | 6 +++--- .../subscribe-to-friend-connectivity-updates.spec.ts | 12 ++++++------ .../services/subscribe-to-friendship-updates.spec.ts | 10 +++++----- .../rpc-server/services/unblock-user.spec.ts | 6 +++--- test/unit/logic/friends.spec.ts | 10 +++++----- test/unit/logic/friendships.spec.ts | 8 ++++---- yarn.lock | 12 ++++++------ 17 files changed, 56 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 9b6ad71..8b86ce7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index eb20acc..5475fe6 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -1,4 +1,4 @@ -import { parseProfileToUserProfile } from '../../../logic/friends' +import { parseProfileToFriend } from '../../../logic/friends' import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { BlockUserPayload, @@ -53,7 +53,7 @@ export function blockUserService({ response: { $case: 'ok', ok: { - profile: parseProfileToUserProfile(profile) + profile: parseProfileToFriend(profile) } } } diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts index 1e21993..a5fc92a 100644 --- a/src/adapters/rpc-server/services/get-blocked-users.ts +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -1,4 +1,4 @@ -import { parseProfilesToUserProfiles } from '../../../logic/friends' +import { parseProfilesToFriends } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -21,7 +21,7 @@ export function getBlockedUsersService({ const profiles = await catalystClient.getProfiles(blockedAddresses) return { - profiles: parseProfilesToUserProfiles(profiles), + profiles: parseProfilesToFriends(profiles), paginationData: { total: blockedAddresses.length, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts index 18c871e..9950dfd 100644 --- a/src/adapters/rpc-server/services/get-friends.ts +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -1,4 +1,4 @@ -import { parseProfilesToUserProfiles } from '../../../logic/friends' +import { parseProfilesToFriends } from '../../../logic/friends' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -28,7 +28,7 @@ export function getFriendsService({ const profiles = await catalystClient.getProfiles(friends.map((friend) => friend.address)) return { - friends: parseProfilesToUserProfiles(profiles), + friends: parseProfilesToFriends(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-mutual-friends.ts b/src/adapters/rpc-server/services/get-mutual-friends.ts index 95bd452..9c05775 100644 --- a/src/adapters/rpc-server/services/get-mutual-friends.ts +++ b/src/adapters/rpc-server/services/get-mutual-friends.ts @@ -6,7 +6,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { normalizeAddress } from '../../../utils/address' import { getPage } from '../../../utils/pagination' -import { parseProfilesToUserProfiles } from '../../../logic/friends' +import { parseProfilesToFriends } from '../../../logic/friends' export function getMutualFriendsService({ components: { logs, db, catalystClient } @@ -30,7 +30,7 @@ export function getMutualFriendsService({ const profiles = await catalystClient.getProfiles(mutualFriends.map((friend) => friend.address)) return { - friends: parseProfilesToUserProfiles(profiles), + friends: parseProfilesToFriends(profiles), paginationData: { total, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts index 38de976..e9f2d35 100644 --- a/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts +++ b/src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.ts @@ -5,7 +5,7 @@ import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { parseEmittedUpdateToFriendConnectivityUpdate } from '../../../logic/friendships' -import { parseProfilesToUserProfiles } from '../../../logic/friends' +import { parseProfilesToFriends } from '../../../logic/friends' import { handleSubscriptionUpdates } from '../../../logic/updates' export function subscribeToFriendConnectivityUpdatesService({ @@ -21,7 +21,7 @@ export function subscribeToFriendConnectivityUpdatesService({ const onlineFriends = await db.getOnlineFriends(context.address, onlinePeers) const profiles = await catalystClient.getProfiles(onlineFriends.map((friend) => friend.address)) - const parsedProfiles = parseProfilesToUserProfiles(profiles).map((friend) => ({ + const parsedProfiles = parseProfilesToFriends(profiles).map((friend) => ({ friend, status: ConnectivityStatus.ONLINE })) diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index 40e184e..bb2b0cc 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -1,4 +1,4 @@ -import { parseProfileToUserProfile } from '../../../logic/friends' +import { parseProfileToFriend } from '../../../logic/friends' import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { UnblockUserPayload, @@ -48,7 +48,7 @@ export function unblockUserService({ response: { $case: 'ok', ok: { - profile: parseProfileToUserProfile(profile) + profile: parseProfileToFriend(profile) } } } diff --git a/src/logic/friends.ts b/src/logic/friends.ts index 7258c3c..9697bcb 100644 --- a/src/logic/friends.ts +++ b/src/logic/friends.ts @@ -1,8 +1,8 @@ -import { UserProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { FriendProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { getProfileName, getProfileHasClaimedName, getProfileUserId, getProfilePictureUrl } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -export function parseProfileToUserProfile(profile: Profile): UserProfile { +export function parseProfileToFriend(profile: Profile): FriendProfile { const name = getProfileName(profile) const userId = getProfileUserId(profile) const hasClaimedName = getProfileHasClaimedName(profile) @@ -16,6 +16,6 @@ export function parseProfileToUserProfile(profile: Profile): UserProfile { } } -export function parseProfilesToUserProfiles(profiles: Profile[]): UserProfile[] { - return profiles.map((profile) => parseProfileToUserProfile(profile)) +export function parseProfilesToFriends(profiles: Profile[]): FriendProfile[] { + return profiles.map((profile) => parseProfileToFriend(profile)) } diff --git a/src/logic/friendships.ts b/src/logic/friendships.ts index 50d1dfc..754b8ae 100644 --- a/src/logic/friendships.ts +++ b/src/logic/friendships.ts @@ -7,7 +7,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, FriendshipAction, FriendshipRequest, FriendshipStatus, SubscriptionEventsEmitter } from '../types' import { normalizeAddress } from '../utils/address' -import { parseProfileToUserProfile } from './friends' +import { parseProfileToFriend } from './friends' import { getProfileUserId } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' @@ -143,7 +143,7 @@ export function parseEmittedUpdateToFriendshipUpdate( request: { id: update.id, createdAt: update.timestamp, - friend: parseProfileToUserProfile(profile), + friend: parseProfileToFriend(profile), message: update.metadata?.message } } @@ -203,7 +203,7 @@ export function parseEmittedUpdateToFriendConnectivityUpdate( ): FriendConnectivityUpdate | null { const { status } = update return { - friend: parseProfileToUserProfile(profile), + friend: parseProfileToFriend(profile), status } } @@ -225,7 +225,7 @@ export function parseFriendshipRequestToFriendshipRequestResponse( ): FriendshipRequestResponse { return { id: request.id, - friend: parseProfileToUserProfile(profile), + friend: parseProfileToFriend(profile), createdAt: new Date(request.timestamp).getTime(), message: request.metadata?.message || '' } diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 14f9afa..4b3047d 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -3,7 +3,7 @@ import { blockUserService } from '../../../../../src/adapters/rpc-server/service import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { parseProfileToFriend } from '../../../../../src/logic/friends' import { PoolClient } from 'pg' describe('blockUserService', () => { @@ -40,7 +40,7 @@ describe('blockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToUserProfile(mockProfile) + profile: parseProfileToFriend(mockProfile) } } }) @@ -71,7 +71,7 @@ describe('blockUserService', () => { expect(response).toEqual({ response: { $case: 'ok', - ok: { profile: parseProfileToUserProfile(mockProfile) } + ok: { profile: parseProfileToFriend(mockProfile) } } }) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index 1cd1dca..139abae 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -3,7 +3,7 @@ import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/s import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfilesToUserProfiles } from '../../../../../src/logic/friends' +import { parseProfilesToFriends } from '../../../../../src/logic/friends' describe('getBlockedUsersService', () => { let getBlockedUsers: ReturnType @@ -32,7 +32,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response).toEqual({ - profiles: parseProfilesToUserProfiles(mockProfiles), + profiles: parseProfilesToFriends(mockProfiles), paginationData: { total: blockedAddresses.length, page: 1 @@ -52,7 +52,7 @@ describe('getBlockedUsersService', () => { const response = await getBlockedUsers(request, rpcContext) expect(response.paginationData.page).toBe(1) - expect(response.profiles).toEqual(parseProfilesToUserProfiles(mockProfiles)) + expect(response.profiles).toEqual(parseProfilesToFriends(mockProfiles)) }) it('should handle errors gracefully', async () => { diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts index 1de120a..7aba99d 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates.spec.ts @@ -4,7 +4,7 @@ import { mockLogs, mockArchipelagoStats, mockDb, mockConfig, mockCatalystClient import { subscribeToFriendConnectivityUpdatesService } from '../../../../../src/adapters/rpc-server/services/subscribe-to-friend-connectivity-updates' import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { parseProfileToFriend } from '../../../../../src/logic/friends' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' @@ -42,7 +42,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockArchipelagoStats.getPeers.mockResolvedValue(['0x456', '0x789']) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -52,7 +52,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { expect(mockArchipelagoStats.getPeersFromCache).toHaveBeenCalled() expect(result.value).toEqual({ - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), status: ConnectivityStatus.ONLINE }) @@ -65,7 +65,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -107,7 +107,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) @@ -125,7 +125,7 @@ describe('subscribeToFriendConnectivityUpdatesService', () => { mockCatalystClient.getProfiles.mockResolvedValueOnce([]) mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), status: ConnectivityStatus.ONLINE } }) diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts index f4385cb..75ad448 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-friendship-updates.spec.ts @@ -4,7 +4,7 @@ import { Action, RpcServerContext } from '../../../../../src/types' import { mockCatalystClient, mockLogs } from '../../../../mocks/components' import { createMockProfile } from '../../../../mocks/profile' import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' -import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { parseProfileToFriend } from '../../../../../src/logic/friends' import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' jest.mock('../../../../../src/logic/updates') @@ -41,7 +41,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should handle subscription updates', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -51,7 +51,7 @@ describe('subscribeToFriendshipUpdatesService', () => { const result = await generator.next() expect(result.value).toEqual({ - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp }) @@ -84,7 +84,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should get the proper address from the update', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } @@ -100,7 +100,7 @@ describe('subscribeToFriendshipUpdatesService', () => { it('should filter updates based on address conditions', async () => { mockHandler.mockImplementationOnce(async function* () { yield { - friend: parseProfileToUserProfile(mockFriendProfile), + friend: parseProfileToFriend(mockFriendProfile), action: mockUpdate.action, createdAt: mockUpdate.timestamp } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index c97ebc6..3685740 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -3,7 +3,7 @@ import { unblockUserService } from '../../../../../src/adapters/rpc-server/servi import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToUserProfile } from '../../../../../src/logic/friends' +import { parseProfileToFriend } from '../../../../../src/logic/friends' import { PoolClient } from 'pg' describe('unblockUserService', () => { @@ -40,7 +40,7 @@ describe('unblockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToUserProfile(mockProfile) + profile: parseProfileToFriend(mockProfile) } } }) @@ -70,7 +70,7 @@ describe('unblockUserService', () => { expect(response).toEqual({ response: { $case: 'ok', - ok: { profile: parseProfileToUserProfile(mockProfile) } + ok: { profile: parseProfileToFriend(mockProfile) } } }) diff --git a/test/unit/logic/friends.spec.ts b/test/unit/logic/friends.spec.ts index 2999dec..0f0277b 100644 --- a/test/unit/logic/friends.spec.ts +++ b/test/unit/logic/friends.spec.ts @@ -1,9 +1,9 @@ -import { parseProfilesToUserProfiles, parseProfileToUserProfile } from '../../../src/logic/friends' +import { parseProfilesToFriends, parseProfileToFriend } from '../../../src/logic/friends' import { mockProfile } from '../../mocks/profile' -describe('parseProfileToUserProfile', () => { +describe('parseProfileToFriend', () => { it('should parse profile to friend', () => { - const friend = parseProfileToUserProfile(mockProfile) + const friend = parseProfileToFriend(mockProfile) expect(friend).toEqual({ address: mockProfile.avatars[0].userId, name: mockProfile.avatars[0].name, @@ -13,7 +13,7 @@ describe('parseProfileToUserProfile', () => { }) }) -describe('parseProfilesToUserProfiles', () => { +describe('parseProfilesToFriends', () => { it('should convert profiles to friend users', () => { const anotherProfile = { ...mockProfile, @@ -28,7 +28,7 @@ describe('parseProfilesToUserProfiles', () => { } const profiles = [mockProfile, anotherProfile] - const result = parseProfilesToUserProfiles(profiles) + const result = parseProfilesToFriends(profiles) expect(result).toEqual([ { diff --git a/test/unit/logic/friendships.spec.ts b/test/unit/logic/friendships.spec.ts index a464633..173cc9e 100644 --- a/test/unit/logic/friendships.spec.ts +++ b/test/unit/logic/friendships.spec.ts @@ -17,7 +17,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { createMockExpectedFriendshipRequest, createMockFriendshipRequest } from '../../mocks/friendship-request' import { createMockProfile, mockProfile } from '../../mocks/profile' -import { parseProfileToUserProfile } from '../../../src/logic/friends' +import { parseProfileToFriend } from '../../../src/logic/friends' describe('isFriendshipActionValid()', () => { test('it should be valid if from is null and to is REQUEST ', () => { @@ -399,7 +399,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseProfileToUserProfile(mockProfile), + friend: parseProfileToFriend(mockProfile), message: undefined } } @@ -425,7 +425,7 @@ describe('parseEmittedUpdateToFriendshipUpdate()', () => { request: { id, createdAt: now, - friend: parseProfileToUserProfile(mockProfile), + friend: parseProfileToFriend(mockProfile), message: 'Hi!' } } @@ -589,7 +589,7 @@ describe('parseEmittedUpdateToFriendConnectivityUpdate()', () => { ])('it should parse status %s update properly', (status) => { const update = { address: '0x123', status } expect(parseEmittedUpdateToFriendConnectivityUpdate(update, mockProfile)).toEqual({ - friend: parseProfileToUserProfile(mockProfile), + friend: parseProfileToFriend(mockProfile), status }) }) diff --git a/yarn.lock b/yarn.lock index 320128f..524b5fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,18 +756,18 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628460474.commit-45c04a2.tgz": - version "1.0.0-13628460474.commit-45c04a2" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628460474.commit-45c04a2.tgz#1d3d8d29ad3a3bb2bed9515653547b46b95d6328" - dependencies: - "@dcl/ts-proto" "1.154.0" - "@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz": version "1.0.0-13628710700.commit-b858bae" resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz#b58dac1d1acab2d5a7971fe7e3ed7abe673c9140" dependencies: "@dcl/ts-proto" "1.154.0" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz": + version "1.0.0-13632387365.commit-db2b5fb" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz#b7c66e9bae705fc678452ee1c9d00223e174b663" + dependencies: + "@dcl/ts-proto" "1.154.0" + "@dcl/rpc@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@dcl/rpc/-/rpc-1.1.2.tgz#789f4f24c8d432a48df3e786b77d017883dda11a" From 4b815cc31e71d78ab36024eaac9bcac30a97ca76 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Mon, 3 Mar 2025 14:58:14 +0100 Subject: [PATCH 14/26] refactor: Abstract getProfileInfo --- src/logic/friends.ts | 7 ++----- src/logic/profiles.ts | 14 ++++++++++++++ test/unit/logic/profiles.spec.ts | 24 ++++++++++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/logic/friends.ts b/src/logic/friends.ts index 9697bcb..50fcd8c 100644 --- a/src/logic/friends.ts +++ b/src/logic/friends.ts @@ -1,12 +1,9 @@ import { FriendProfile } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -import { getProfileName, getProfileHasClaimedName, getProfileUserId, getProfilePictureUrl } from './profiles' +import { getProfileInfo } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' export function parseProfileToFriend(profile: Profile): FriendProfile { - const name = getProfileName(profile) - const userId = getProfileUserId(profile) - const hasClaimedName = getProfileHasClaimedName(profile) - const profilePictureUrl = getProfilePictureUrl(profile) + const { name, userId, hasClaimedName, profilePictureUrl } = getProfileInfo(profile) return { address: userId, diff --git a/src/logic/profiles.ts b/src/logic/profiles.ts index 880e2d3..a80a081 100644 --- a/src/logic/profiles.ts +++ b/src/logic/profiles.ts @@ -38,3 +38,17 @@ export function getProfilePictureUrl(profile: Pick): string return face256 } + +export function getProfileInfo(profile: Profile) { + const name = getProfileName(profile) + const userId = getProfileUserId(profile) + const hasClaimedName = getProfileHasClaimedName(profile) + const profilePictureUrl = getProfilePictureUrl(profile) + + return { + name, + userId, + hasClaimedName, + profilePictureUrl + } +} diff --git a/test/unit/logic/profiles.spec.ts b/test/unit/logic/profiles.spec.ts index 5e8955f..48efe11 100644 --- a/test/unit/logic/profiles.spec.ts +++ b/test/unit/logic/profiles.spec.ts @@ -1,5 +1,11 @@ import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -import { getProfileAvatarItem, getProfileName, getProfileUserId, getProfilePictureUrl } from '../../../src/logic/profiles' +import { + getProfileAvatarItem, + getProfileName, + getProfileUserId, + getProfilePictureUrl, + getProfileInfo +} from '../../../src/logic/profiles' import { mockProfile } from '../../mocks/profile' describe('getProfileAvatarItem', () => { @@ -67,10 +73,20 @@ describe('getProfilePictureUrl', () => { it('should throw on profile without avatar snapshots', () => { const profileWithoutSnapshots: Profile = { ...mockProfile, - avatars: [ - { ...mockProfile.avatars[0], avatar: { ...mockProfile.avatars[0].avatar, snapshots: undefined } } - ] + avatars: [{ ...mockProfile.avatars[0], avatar: { ...mockProfile.avatars[0].avatar, snapshots: undefined } }] } expect(() => getProfilePictureUrl(profileWithoutSnapshots)).toThrow('Missing profile avatar picture url') }) }) + +describe('getProfileInfo', () => { + it('should extract profile info from profile entity', () => { + const info = getProfileInfo(mockProfile) + expect(info).toEqual({ + name: mockProfile.avatars[0].name, + userId: mockProfile.avatars[0].userId, + hasClaimedName: mockProfile.avatars[0].hasClaimedName, + profilePictureUrl: mockProfile.avatars[0].avatar.snapshots.face256 + }) + }) +}) From d16b31b754351712ab3002e87996970331a040d4 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 4 Mar 2025 10:24:53 +0100 Subject: [PATCH 15/26] feat: Implement block updates --- package.json | 2 +- src/adapters/db.ts | 16 ++- src/adapters/pubsub.ts | 2 +- src/adapters/rpc-server/rpc-server.ts | 12 +- .../rpc-server/services/block-user.ts | 22 ++-- .../services/subscribe-to-block-updates.ts | 36 ++++++ .../rpc-server/services/unblock-user.ts | 14 ++- src/logic/blocks.ts | 38 ++++++ src/logic/updates.ts | 4 +- src/types.ts | 10 +- test/unit/adapters/db.spec.ts | 77 ++++++------- .../rpc-server/services/block-user.spec.ts | 34 +++++- .../subscribe-to-block-updates.spec.ts | 108 ++++++++++++++++++ .../rpc-server/services/unblock-user.spec.ts | 28 ++++- test/unit/logic/blocks.spec.ts | 52 +++++++++ test/unit/logic/updates.spec.ts | 28 ++++- yarn.lock | 12 +- 17 files changed, 413 insertions(+), 82 deletions(-) create mode 100644 src/adapters/rpc-server/services/subscribe-to-block-updates.ts create mode 100644 src/logic/blocks.ts create mode 100644 test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts create mode 100644 test/unit/logic/blocks.spec.ts diff --git a/package.json b/package.json index 8b86ce7..37c026a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 2c60b84..78dfe78 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -171,12 +171,18 @@ export function createDBComponent(components: Pick const query = SQL` INSERT INTO blocks (id, blocker_address, blocked_address) VALUES (${randomUUID()}, ${normalizeAddress(blockerAddress)}, ${normalizeAddress(blockedAddress)}) - ON CONFLICT DO NOTHING` + ON CONFLICT DO NOTHING + RETURNING id, blocked_at` - if (txClient) { - await txClient.query(query) - } else { - await pg.query(query) + const { + rows: [{ id, blocked_at }] + } = txClient + ? await txClient.query<{ id: string; blocked_at: Date }>(query) + : await pg.query<{ id: string; blocked_at: Date }>(query) + + return { + id, + blocked_at } }, async unblockUser(blockerAddress, blockedAddress, txClient) { diff --git a/src/adapters/pubsub.ts b/src/adapters/pubsub.ts index 2bcea29..2f8d6f1 100644 --- a/src/adapters/pubsub.ts +++ b/src/adapters/pubsub.ts @@ -2,7 +2,7 @@ import { AppComponents, IPubSubComponent } from '../types' export const FRIENDSHIP_UPDATES_CHANNEL = 'friendship.updates' export const FRIEND_STATUS_UPDATES_CHANNEL = 'friend.status.updates' - +export const BLOCK_UPDATES_CHANNEL = 'block.updates' export function createPubSubComponent(components: Pick): IPubSubComponent { const { logs, redis } = components const logger = logs.getLogger('pubsub-component') diff --git a/src/adapters/rpc-server/rpc-server.ts b/src/adapters/rpc-server/rpc-server.ts index 571b335..fc7b2b1 100644 --- a/src/adapters/rpc-server/rpc-server.ts +++ b/src/adapters/rpc-server/rpc-server.ts @@ -20,6 +20,8 @@ import { blockUserService } from './services/block-user' import { getBlockedUsersService } from './services/get-blocked-users' import { unblockUserService } from './services/unblock-user' import { getBlockingStatusService } from './services/get-blocking-status' +import { subscribeToBlockUpdatesService } from './services/subscribe-to-block-updates' + export async function createRpcServerComponent({ logs, db, @@ -60,9 +62,12 @@ export async function createRpcServerComponent({ const subscribeToFriendConnectivityUpdates = subscribeToFriendConnectivityUpdatesService({ components: { logs, db, archipelagoStats, catalystClient } }) + const subscribeToBlockUpdates = subscribeToBlockUpdatesService({ + components: { logs, catalystClient } + }) - const blockUser = blockUserService({ components: { logs, db, catalystClient } }) - const unblockUser = unblockUserService({ components: { logs, db, catalystClient } }) + const blockUser = blockUserService({ components: { logs, db, catalystClient, pubsub } }) + const unblockUser = unblockUserService({ components: { logs, db, catalystClient, pubsub } }) const getBlockedUsers = getBlockedUsersService({ components: { logs, db, catalystClient } }) const getBlockingStatus = getBlockingStatusService({ components: { logs, db } }) @@ -79,7 +84,8 @@ export async function createRpcServerComponent({ getBlockedUsers, getBlockingStatus, subscribeToFriendshipUpdates, - subscribeToFriendConnectivityUpdates + subscribeToFriendConnectivityUpdates, + subscribeToBlockUpdates })) }) diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 5475fe6..b9f161d 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -1,13 +1,14 @@ -import { parseProfileToFriend } from '../../../logic/friends' import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { BlockUserPayload, BlockUserResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { BLOCK_UPDATES_CHANNEL } from '../../pubsub' +import { parseProfileToBlockedUser } from '../../../logic/blocks' export function blockUserService({ - components: { logs, db, catalystClient } -}: RPCServiceContext<'logs' | 'db' | 'catalystClient'>) { + components: { logs, db, catalystClient, pubsub } +}: RPCServiceContext<'logs' | 'db' | 'catalystClient' | 'pubsub'>) { const logger = logs.getLogger('block-user-service') return async function (request: BlockUserPayload, context: RpcServerContext): Promise { @@ -37,23 +38,30 @@ export function blockUserService({ } } - await db.executeTx(async (tx) => { - await db.blockUser(blockerAddress, blockedAddress, tx) + const blockedAt = await db.executeTx(async (tx) => { + const { blocked_at } = await db.blockUser(blockerAddress, blockedAddress, tx) const friendship = await db.getFriendship([blockerAddress, blockedAddress], tx) - if (!friendship) return + if (!friendship) return blocked_at await Promise.all([ db.updateFriendshipStatus(friendship.id, false, tx), db.recordFriendshipAction(friendship.id, blockerAddress, Action.BLOCK, null, tx) ]) + + return blocked_at + }) + + await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { + address: blockedAddress, + isBlocked: true }) return { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(profile) + profile: parseProfileToBlockedUser(profile, blockedAt) } } } diff --git a/src/adapters/rpc-server/services/subscribe-to-block-updates.ts b/src/adapters/rpc-server/services/subscribe-to-block-updates.ts new file mode 100644 index 0000000..2ed4daf --- /dev/null +++ b/src/adapters/rpc-server/services/subscribe-to-block-updates.ts @@ -0,0 +1,36 @@ +import { Empty } from '@dcl/protocol/out-js/google/protobuf/empty.gen' +import { RpcServerContext, RPCServiceContext, SubscriptionEventsEmitter } from '../../../types' +import { BlockUpdate } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { parseEmittedUpdateToBlockUpdate } from '../../../logic/blocks' +import { handleSubscriptionUpdates } from '../../../logic/updates' + +export function subscribeToBlockUpdatesService({ + components: { logs, catalystClient } +}: RPCServiceContext<'logs' | 'catalystClient'>) { + const logger = logs.getLogger('subscribe-to-block-updates-service') + + return async function* (_request: Empty, context: RpcServerContext): AsyncGenerator { + let cleanup: (() => void) | undefined + + try { + cleanup = yield* handleSubscriptionUpdates({ + rpcContext: context, + eventName: 'blockUpdate', + components: { + catalystClient, + logger + }, + shouldRetrieveProfile: false, + getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address, + parser: parseEmittedUpdateToBlockUpdate, + shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address === context.address + }) + } catch (error: any) { + logger.error('Error in block updates subscription:', error) + throw error + } finally { + logger.info('Closing block updates subscription') + cleanup?.() + } + } +} diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index bb2b0cc..7a41ee1 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -1,13 +1,14 @@ -import { parseProfileToFriend } from '../../../logic/friends' import { Action, RpcServerContext, RPCServiceContext } from '../../../types' import { UnblockUserPayload, UnblockUserResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { BLOCK_UPDATES_CHANNEL } from '../../pubsub' +import { parseProfileToBlockedUser } from '../../../logic/blocks' export function unblockUserService({ - components: { logs, db, catalystClient } -}: RPCServiceContext<'logs' | 'db' | 'catalystClient'>) { + components: { logs, db, catalystClient, pubsub } +}: RPCServiceContext<'logs' | 'db' | 'catalystClient' | 'pubsub'>) { const logger = logs.getLogger('unblock-user-service') return async function (request: UnblockUserPayload, context: RpcServerContext): Promise { @@ -44,11 +45,16 @@ export function unblockUserService({ await db.recordFriendshipAction(friendship.id, blockerAddress, Action.DELETE, null, tx) }) + await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { + address: blockedAddress, + isBlocked: false + }) + return { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(profile) + profile: parseProfileToBlockedUser(profile) } } } diff --git a/src/logic/blocks.ts b/src/logic/blocks.ts new file mode 100644 index 0000000..66c6bb7 --- /dev/null +++ b/src/logic/blocks.ts @@ -0,0 +1,38 @@ +import { + BlockedUserProfile, + BlockUpdate +} from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { getProfileInfo, getProfileUserId } from './profiles' +import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' +import { SubscriptionEventsEmitter } from '../types' + +export function parseProfileToBlockedUser(profile: Profile, blockedAt?: Date): BlockedUserProfile { + const { name, userId, hasClaimedName, profilePictureUrl } = getProfileInfo(profile) + + return { + address: userId, + name, + hasClaimedName, + profilePictureUrl, + blockedAt: blockedAt?.getTime() + } +} + +export function parseProfilesToBlockedUsers( + profiles: Profile[], + blockedAtByAddress: Map +): BlockedUserProfile[] { + return profiles.map((profile) => { + const userId = getProfileUserId(profile) + const blockedAt = blockedAtByAddress.get(userId) + return parseProfileToBlockedUser(profile, blockedAt) + }) +} + +export function parseEmittedUpdateToBlockUpdate(update: SubscriptionEventsEmitter['blockUpdate']): BlockUpdate | null { + const { address, isBlocked } = update + return { + address, + isBlocked + } +} diff --git a/src/logic/updates.ts b/src/logic/updates.ts index b114360..742833a 100644 --- a/src/logic/updates.ts +++ b/src/logic/updates.ts @@ -26,6 +26,7 @@ interface SubscriptionHandlerParams { logger: ILogger catalystClient: ICatalystClientComponent } + shouldRetrieveProfile?: boolean getAddressFromUpdate: (update: U) => string shouldHandleUpdate: (update: U) => boolean parser: UpdateParser @@ -120,6 +121,7 @@ export async function* handleSubscriptionUpdates({ rpcContext, eventName, components: { catalystClient, logger }, + shouldRetrieveProfile = true, getAddressFromUpdate, shouldHandleUpdate, parser, @@ -143,7 +145,7 @@ export async function* handleSubscriptionUpdates({ continue } - const profile = await catalystClient.getProfile(getAddressFromUpdate(update as U)) + const profile = shouldRetrieveProfile ? await catalystClient.getProfile(getAddressFromUpdate(update as U)) : null const parsedUpdate = await parser(update as U, profile, ...parseArgs) if (parsedUpdate) { diff --git a/src/types.ts b/src/types.ts index 3256b9b..df1fdd9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -107,7 +107,11 @@ export interface IDatabaseComponent { getSentFriendshipRequests(userAddress: string, pagination?: Pagination): Promise getSentFriendshipRequestsCount(userAddress: string): Promise getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise - blockUser(blockerAddress: string, blockedAddress: string, txClient?: PoolClient): Promise + blockUser( + blockerAddress: string, + blockedAddress: string, + txClient?: PoolClient + ): Promise<{ id: string; blocked_at: Date }> unblockUser(blockerAddress: string, blockedAddress: string, txClient?: PoolClient): Promise blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise @@ -241,6 +245,10 @@ export type SubscriptionEventsEmitter = { address: string status: ConnectivityStatus } + blockUpdate: { + address: string + isBlocked: boolean + } } export type Subscribers = Record> diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 093a724..e93af50 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -4,6 +4,7 @@ import SQL from 'sql-template-strings' import { mockLogs, mockPg } from '../../mocks/components' import { normalizeAddress } from '../../../src/utils/address' import { PoolClient } from 'pg' + jest.mock('node:crypto', () => ({ randomUUID: jest.fn().mockReturnValue('mock-uuid') })) @@ -291,22 +292,13 @@ describe('db', () => { describe('createFriendship', () => { it.each([false, true])('should create a new friendship using txs: %s', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined - const queryResult = { + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { rows: [{ id: 'friendship-1', created_at: '2025-01-01T00:00:00.000Z' }], rowCount: 1 - } - - if (withTxClient) { - mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) - } else { - mockPg.query.mockResolvedValueOnce(queryResult) - } + }) const result = await dbComponent.createFriendship(['0x123', '0x456'], true, mockClient) - const queryToAssert = withTxClient ? mockClient.query : mockPg.query - expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining( @@ -326,22 +318,13 @@ describe('db', () => { describe('updateFriendshipStatus', () => { it.each([false, true])('should update friendship status using txs: %s', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined - const queryResult = { + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { rowCount: 1, rows: [{ id: 'friendship-id', created_at: '2025-01-01T00:00:00.000Z' }] - } - - if (withTxClient) { - mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) - } else { - mockPg.query.mockResolvedValueOnce(queryResult) - } + }) const result = await dbComponent.updateFriendshipStatus('friendship-id', false, mockClient) - const queryToAssert = withTxClient ? mockClient.query : mockPg.query - expect(queryToAssert).toHaveBeenCalledWith( SQL`UPDATE friendships SET is_active = ${false}, updated_at = now() WHERE id = ${'friendship-id'} RETURNING id, created_at` ) @@ -354,31 +337,21 @@ describe('db', () => { describe('getFriendship', () => { it.each([true])('should retrieve a specific friendship using txs: %s', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined const mockFriendship = { id: 'friendship-1', address_requester: '0x123', address_requested: '0x456', is_active: true } - - const queryResult = { + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { rows: [mockFriendship], rowCount: 1 - } - - if (withTxClient) { - mockClient.query = jest.fn().mockResolvedValueOnce(queryResult) - } else { - mockPg.query.mockResolvedValueOnce(queryResult) - } + }) const result = await dbComponent.getFriendship(['0x123', '0x456'], mockClient) expect(result).toEqual(mockFriendship) - const queryToAssert = withTxClient ? mockClient.query : mockPg.query - expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining( @@ -512,8 +485,12 @@ describe('db', () => { }) describe('recordFriendshipAction', () => { - it.each([false, true])('should record a friendship action', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + it.each([false, true])('should record a friendship action using txs: %s', async (withTxClient: boolean) => { + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { + rows: [{ id: 'mock-uuid' }], + rowCount: 1 + }) + const result = await dbComponent.recordFriendshipAction( 'friendship-id', '0x123', @@ -525,7 +502,6 @@ describe('db', () => { ) expect(result).toBe('mock-uuid') - const queryToAssert = withTxClient ? mockClient.query : mockPg.query expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining( @@ -588,10 +564,12 @@ describe('db', () => { describe('blockUser', () => { it.each([false, true])('should block a user using txs: %s', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined - await dbComponent.blockUser('0x123', '0x456', mockClient) + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { + rows: [{ id: 'block-id', blocked_at: new Date() }], + rowCount: 1 + }) - const queryToAssert = withTxClient ? mockClient.query : mockPg.query + await dbComponent.blockUser('0x123', '0x456', mockClient) expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ @@ -610,15 +588,17 @@ describe('db', () => { describe('unblockUser', () => { it.each([false, true])('should unblock a user using txs: %s', async (withTxClient: boolean) => { - const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined + const { query: queryToAssert, mockClient } = await mockQuery(withTxClient, { + rows: [{ id: 'block-id', blocked_at: new Date() }], + rowCount: 1 + }) + await dbComponent.unblockUser('0x123', '0x456', mockClient) const expectedQuery = SQL` DELETE FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress('0x123')} AND LOWER(blocked_address) = ${normalizeAddress('0x456')}` - const queryToAssert = withTxClient ? mockClient.query : mockPg.query - expect(queryToAssert).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining(expectedQuery.text), @@ -770,4 +750,15 @@ describe('db', () => { }) ) } + + async function mockQuery(withTxClient: boolean, result?: any) { + if (withTxClient) { + const mockClient = await mockPg.getPool().connect() + mockClient.query = jest.fn().mockResolvedValueOnce(result) + return { mockClient, query: mockClient.query } + } else { + mockPg.query.mockResolvedValueOnce(result) + return { query: mockPg.query } + } + } }) diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 4b3047d..e7365f6 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -1,14 +1,16 @@ -import { mockCatalystClient, mockDb, mockLogs, mockPg } from '../../../../mocks/components' +import { mockCatalystClient, mockDb, mockLogs, mockPg, mockPubSub } from '../../../../mocks/components' import { blockUserService } from '../../../../../src/adapters/rpc-server/services/block-user' import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToFriend } from '../../../../../src/logic/friends' import { PoolClient } from 'pg' +import { BLOCK_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' +import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' describe('blockUserService', () => { let blockUser: ReturnType let mockClient: jest.Mocked + let blockedAt: Date const rpcContext: RpcServerContext = { address: '0x123', @@ -17,11 +19,13 @@ describe('blockUserService', () => { beforeEach(async () => { blockUser = blockUserService({ - components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } + components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient, pubsub: mockPubSub } }) mockClient = (await mockPg.getPool().connect()) as jest.Mocked mockDb.executeTx.mockImplementationOnce(async (cb) => cb(mockClient)) + + blockedAt = new Date() }) it('should block a user successfully, update friendship status, and record friendship action if it exists', async () => { @@ -33,6 +37,7 @@ describe('blockUserService', () => { mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) + mockDb.blockUser.mockResolvedValueOnce({ id: 'block-id', blocked_at: blockedAt }) const response = await blockUser(request, rpcContext) @@ -40,7 +45,7 @@ describe('blockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(mockProfile) + profile: parseProfileToBlockedUser(mockProfile, blockedAt) } } }) @@ -65,13 +70,14 @@ describe('blockUserService', () => { mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) mockDb.getFriendship.mockResolvedValueOnce(null) + mockDb.blockUser.mockResolvedValueOnce({ id: 'block-id', blocked_at: blockedAt }) const response = await blockUser(request, rpcContext) expect(response).toEqual({ response: { $case: 'ok', - ok: { profile: parseProfileToFriend(mockProfile) } + ok: { profile: parseProfileToBlockedUser(mockProfile, blockedAt) } } }) @@ -81,6 +87,24 @@ describe('blockUserService', () => { expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) + it('should publish a block update event after blocking a user', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) + mockDb.blockUser.mockResolvedValueOnce({ id: 'block-id', blocked_at: blockedAt }) + await blockUser(request, rpcContext) + + expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(BLOCK_UPDATES_CHANNEL, { + address: blockedAddress, + isBlocked: true + }) + }) + it('should return internalServerError when user address is missing', async () => { const request: BlockUserPayload = { user: { address: '' } diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts new file mode 100644 index 0000000..aa71f2a --- /dev/null +++ b/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts @@ -0,0 +1,108 @@ +import { subscribeToBlockUpdatesService } from '../../../../../src/adapters/rpc-server/services/subscribe-to-block-updates' +import { Empty } from '@dcl/protocol/out-js/google/protobuf/empty.gen' +import { RpcServerContext } from '../../../../../src/types' +import { mockCatalystClient, mockLogs } from '../../../../mocks/components' +import { handleSubscriptionUpdates } from '../../../../../src/logic/updates' +import { createSubscribersContext } from '../../../../../src/adapters/rpc-server' + +jest.mock('../../../../../src/logic/updates') + +describe('subscribeToBlockUpdatesService', () => { + let subscribeToBlockUpdates: ReturnType + let rpcContext: RpcServerContext + const subscribersContext = createSubscribersContext() + const mockHandler = handleSubscriptionUpdates as jest.Mock + + const mockUpdate = { + address: '0x456', + isBlocked: true + } + + beforeEach(async () => { + subscribeToBlockUpdates = subscribeToBlockUpdatesService({ + components: { + logs: mockLogs, + catalystClient: mockCatalystClient + } + }) + + rpcContext = { + address: '0x123', + subscribersContext + } + }) + + it('should handle subscription updates', async () => { + mockHandler.mockImplementationOnce(async function* () { + yield mockUpdate + }) + + const generator = subscribeToBlockUpdates({} as Empty, rpcContext) + const result = await generator.next() + + expect(result.value).toEqual(mockUpdate) + expect(result.done).toBe(false) + }) + + it('should handle errors during subscription', async () => { + const testError = new Error('Test error') + mockHandler.mockImplementationOnce(async function* () { + throw testError + }) + + const generator = subscribeToBlockUpdates({} as Empty, rpcContext) + await expect(generator.next()).rejects.toThrow(testError) + }) + + it('should properly clean up subscription on return', async () => { + mockHandler.mockImplementationOnce(async function* () { + while (true) { + yield undefined + } + }) + + const generator = subscribeToBlockUpdates({} as Empty, rpcContext) + const result = await generator.return(undefined) + + expect(result.done).toBe(true) + }) + + it('should get the proper address from the update', async () => { + mockHandler.mockImplementationOnce(async function* () { + yield mockUpdate + }) + + const generator = subscribeToBlockUpdates({} as Empty, rpcContext) + const result = await generator.next() + + const getAddressFromUpdate = mockHandler.mock.calls[0][0].getAddressFromUpdate + expect(getAddressFromUpdate(mockUpdate)).toBe(mockUpdate.address) + }) + + it('should filter updates based on address conditions', async () => { + mockHandler.mockImplementationOnce(async function* () { + yield mockUpdate + }) + + const loggedUserAddress = '0x123' + + const mockUpdateBlockingNonLoggedUser = { + address: '0x456', + isBlocked: true + } + + const mockUpdateBlockingLoggedUser = { + address: loggedUserAddress, + isBlocked: true + } + + const generator = subscribeToBlockUpdates({} as Empty, rpcContext) + const result = await generator.next() + + // Extract the shouldHandleUpdate function from the handler call + const shouldHandleUpdate = mockHandler.mock.calls[0][0].shouldHandleUpdate + + expect(shouldHandleUpdate(mockUpdateBlockingNonLoggedUser)).toBe(false) + expect(shouldHandleUpdate(mockUpdateBlockingLoggedUser)).toBe(true) + }) +}) diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index 3685740..f0aa620 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -1,10 +1,12 @@ -import { mockCatalystClient, mockDb, mockLogs, mockPg } from '../../../../mocks/components' +import { mockCatalystClient, mockDb, mockLogs, mockPg, mockPubSub } from '../../../../mocks/components' import { unblockUserService } from '../../../../../src/adapters/rpc-server/services/unblock-user' import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' import { parseProfileToFriend } from '../../../../../src/logic/friends' import { PoolClient } from 'pg' +import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' +import { BLOCK_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' describe('unblockUserService', () => { let unblockUser: ReturnType @@ -17,7 +19,7 @@ describe('unblockUserService', () => { beforeEach(async () => { unblockUser = unblockUserService({ - components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient } + components: { db: mockDb, logs: mockLogs, catalystClient: mockCatalystClient, pubsub: mockPubSub } }) mockClient = (await mockPg.getPool().connect()) as jest.Mocked @@ -40,7 +42,7 @@ describe('unblockUserService', () => { response: { $case: 'ok', ok: { - profile: parseProfileToFriend(mockProfile) + profile: parseProfileToBlockedUser(mockProfile) } } }) @@ -70,7 +72,7 @@ describe('unblockUserService', () => { expect(response).toEqual({ response: { $case: 'ok', - ok: { profile: parseProfileToFriend(mockProfile) } + ok: { profile: parseProfileToBlockedUser(mockProfile) } } }) @@ -79,6 +81,24 @@ describe('unblockUserService', () => { expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) + it('should publish a block update event after unblocking a user', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) + + await unblockUser(request, rpcContext) + + expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(BLOCK_UPDATES_CHANNEL, { + address: blockedAddress, + isBlocked: false + }) + }) + it('should return internalServerError when user address is missing', async () => { const request: UnblockUserPayload = { user: { address: '' } diff --git a/test/unit/logic/blocks.spec.ts b/test/unit/logic/blocks.spec.ts new file mode 100644 index 0000000..57ea7c8 --- /dev/null +++ b/test/unit/logic/blocks.spec.ts @@ -0,0 +1,52 @@ +import { parseProfilesToBlockedUsers, parseProfileToBlockedUser } from '../../../src/logic/blocks' +import { mockProfile } from '../../mocks/profile' + +describe('parseProfileToBlockedUser', () => { + it('should parse profile to blocked user', () => { + const blockedAt = new Date() + const blockedUser = parseProfileToBlockedUser(mockProfile, blockedAt) + expect(blockedUser).toEqual({ + address: mockProfile.avatars[0].userId, + name: mockProfile.avatars[0].name, + hasClaimedName: mockProfile.avatars[0].hasClaimedName, + profilePictureUrl: mockProfile.avatars[0].avatar.snapshots.face256, + blockedAt: blockedAt.getTime() + }) + }) +}) + +describe('parseProfilesToBlockedUsers', () => { + it('should convert profiles to blocked users', () => { + const anotherProfile = { + ...mockProfile, + avatars: [ + { + ...mockProfile.avatars[0], + userId: '0x123aBcDE', + name: 'TestUser2', + hasClaimedName: false + } + ] + } + const profiles = [mockProfile, anotherProfile] + const blockedAt123 = new Date() + const blockedAtByAddress = new Map([['0x123', blockedAt123]]) + const result = parseProfilesToBlockedUsers(profiles, blockedAtByAddress) + + expect(result).toEqual([ + { + address: '0x123', + name: 'TestUser', + hasClaimedName: true, + profilePictureUrl: mockProfile.avatars[0].avatar.snapshots.face256, + blockedAt: blockedAt123.getTime() + }, + { + address: '0x123abcde', + name: 'TestUser2', + hasClaimedName: false, + profilePictureUrl: anotherProfile.avatars[0].avatar.snapshots.face256 + } + ]) + }) +}) diff --git a/test/unit/logic/updates.spec.ts b/test/unit/logic/updates.spec.ts index a05c55f..1beffaf 100644 --- a/test/unit/logic/updates.spec.ts +++ b/test/unit/logic/updates.spec.ts @@ -222,6 +222,7 @@ describe('updates handlers', () => { let subscribersContext: ISubscribersContext const friendshipUpdate = { id: '1', to: '0x456', from: '0x123', action: Action.REQUEST, timestamp: Date.now() } + const blockUpdate = { address: '0x123', isBlocked: true } beforeEach(() => { eventEmitter = mitt() @@ -339,7 +340,7 @@ describe('updates handlers', () => { parser }) - const resultPromise = generator.next() + generator.next() rpcContext.subscribersContext.getOrAddSubscriber('0x123').emit('friendshipUpdate', friendshipUpdate) await sleep(100) @@ -376,5 +377,30 @@ describe('updates handlers', () => { event: 'friendshipUpdate' }) }) + + it('should skip retrieving profile if shouldRetrieveProfile is false', async () => { + parser.mockResolvedValueOnce({ parsed: true }) + + const generator = handleSubscriptionUpdates({ + rpcContext, + eventName: 'blockUpdate', + components: { + catalystClient: mockCatalystClient, + logger + }, + shouldRetrieveProfile: false, + getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address, + shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address === '0x123', + parser + }) + + const resultPromise = generator.next() + rpcContext.subscribersContext.getOrAddSubscriber('0x123').emit('blockUpdate', blockUpdate) + + const result = await resultPromise + expect(result.value).toEqual({ parsed: true }) + expect(parser).toHaveBeenCalledWith(blockUpdate, null) + expect(mockCatalystClient.getProfile).not.toHaveBeenCalled() + }) }) }) diff --git a/yarn.lock b/yarn.lock index 524b5fb..0f6d8ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,18 +756,18 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz": - version "1.0.0-13628710700.commit-b858bae" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13628710700.commit-b858bae.tgz#b58dac1d1acab2d5a7971fe7e3ed7abe673c9140" - dependencies: - "@dcl/ts-proto" "1.154.0" - "@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz": version "1.0.0-13632387365.commit-db2b5fb" resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz#b7c66e9bae705fc678452ee1c9d00223e174b663" dependencies: "@dcl/ts-proto" "1.154.0" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz": + version "1.0.0-13649677549.commit-1cf5d36" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz#2da416e27288a4e3bf1d92bf36ffa253b292cb0a" + dependencies: + "@dcl/ts-proto" "1.154.0" + "@dcl/rpc@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@dcl/rpc/-/rpc-1.1.2.tgz#789f4f24c8d432a48df3e786b77d017883dda11a" From a92010263b2239c2639e38bddf6ba9cf63401267 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 4 Mar 2025 13:54:04 +0100 Subject: [PATCH 16/26] feat: GetFriendshipStatus distinguish between BLOCKED and BLOCKED BY --- package.json | 2 +- .../rpc-server/services/get-friendship-status.ts | 1 - src/logic/friendships.ts | 3 ++- test/unit/logic/friendships.spec.ts | 12 ++++++++++++ yarn.lock | 12 ++++++------ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 37c026a..65e09f9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/rpc-server/services/get-friendship-status.ts b/src/adapters/rpc-server/services/get-friendship-status.ts index 5817571..8ab7146 100644 --- a/src/adapters/rpc-server/services/get-friendship-status.ts +++ b/src/adapters/rpc-server/services/get-friendship-status.ts @@ -5,7 +5,6 @@ import { GetFriendshipStatusResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -// TODO(feat/blocks): What should happen if one of the users is blocked? export function getFriendshipStatusService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { const logger = logs.getLogger('get-sent-friendship-requests-service') diff --git a/src/logic/friendships.ts b/src/logic/friendships.ts index 754b8ae..217d4b2 100644 --- a/src/logic/friendships.ts +++ b/src/logic/friendships.ts @@ -31,7 +31,8 @@ const FRIENDSHIP_STATUS_BY_ACTION: Record< [Action.REJECT]: () => FriendshipRequestStatus.REJECTED, [Action.REQUEST]: (actingUser, contextAddress) => actingUser === contextAddress ? FriendshipRequestStatus.REQUEST_SENT : FriendshipRequestStatus.REQUEST_RECEIVED, - [Action.BLOCK]: () => FriendshipRequestStatus.BLOCKED + [Action.BLOCK]: (actingUser, contextAddress) => + actingUser === contextAddress ? FriendshipRequestStatus.BLOCKED : FriendshipRequestStatus.BLOCKED_BY } export function isFriendshipActionValid(from: Action | null, to: Action) { diff --git a/test/unit/logic/friendships.spec.ts b/test/unit/logic/friendships.spec.ts index 173cc9e..451d9eb 100644 --- a/test/unit/logic/friendships.spec.ts +++ b/test/unit/logic/friendships.spec.ts @@ -580,6 +580,18 @@ describe('getFriendshipRequestStatus()', () => { const requestMadeByAnotherUser = { ...friendshipAction, acting_user: '0x456', action: Action.REQUEST } expect(getFriendshipRequestStatus(requestMadeByAnotherUser, '0x123')).toBe(FriendshipRequestStatus.REQUEST_RECEIVED) }) + + test('when the last action is block and the acting user is the logged user it should return blocked', () => { + expect(getFriendshipRequestStatus({ ...friendshipAction, action: Action.BLOCK }, '0x123')).toBe( + FriendshipRequestStatus.BLOCKED + ) + }) + + test('when the last action is block and the acting user is not the logged user it should return blocked by', () => { + expect(getFriendshipRequestStatus({ ...friendshipAction, action: Action.BLOCK }, '0x456')).toBe( + FriendshipRequestStatus.BLOCKED_BY + ) + }) }) describe('parseEmittedUpdateToFriendConnectivityUpdate()', () => { diff --git a/yarn.lock b/yarn.lock index 0f6d8ec..53e476e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,18 +756,18 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz": - version "1.0.0-13632387365.commit-db2b5fb" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13632387365.commit-db2b5fb.tgz#b7c66e9bae705fc678452ee1c9d00223e174b663" - dependencies: - "@dcl/ts-proto" "1.154.0" - "@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz": version "1.0.0-13649677549.commit-1cf5d36" resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz#2da416e27288a4e3bf1d92bf36ffa253b292cb0a" dependencies: "@dcl/ts-proto" "1.154.0" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz": + version "1.0.0-13654029584.commit-3cb93f2" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz#ef9c7a5df65853e7251643f6a8f83a82f87e2314" + dependencies: + "@dcl/ts-proto" "1.154.0" + "@dcl/rpc@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@dcl/rpc/-/rpc-1.1.2.tgz#789f4f24c8d432a48df3e786b77d017883dda11a" From 68ccc5770d607859b0d649a6c41d6df381d86e7d Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Tue, 4 Mar 2025 19:39:29 +0100 Subject: [PATCH 17/26] chore: Adapt readme --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed6c84d..608f3ed 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ A microservice that handles social interactions (friendships) for Decentraland, - [🏗 Architecture](#-architecture) - [Component-Based Architecture](#component-based-architecture) - [Database Design](#database-design) - - [Flow Diagrams](#flow-diagrams) + - [Friendship Flow Diagrams](#friendship-flow-diagrams) + - [Block System Flow](#block-system-flow) - [🚀 Getting Started](#-getting-started) - [Prerequisites](#prerequisites) - [Local Development](#local-development) @@ -27,6 +28,7 @@ A microservice that handles social interactions (friendships) for Decentraland, - Mutual friends discovery - Online status tracking - Integration with Archipelago for peer synchronization +- User blocking system ## 🏗 Architecture @@ -63,20 +65,28 @@ erDiagram jsonb metadata timestamp timestamp } + BLOCKS { + uuid id PK + varchar blocker_address + varchar blocked_address + timestamp blocked_at + } FRIENDSHIPS ||--o{ FRIENDSHIP_ACTIONS : "has" + BLOCKS ||--o{ FRIENDSHIPS : "blocks" ``` The database schema supports: - Bidirectional friendships - Action history tracking +- User blocking system - Metadata for requests - Optimized queries with proper indexes See migrations for details: [migrations](./src/migrations) -### Flow Diagrams +### Friendship Flow Diagrams ```mermaid sequenceDiagram @@ -147,6 +157,37 @@ sequenceDiagram deactivate NATS ``` +### Block System Flow + +```mermaid +sequenceDiagram + participant Client + participant RPC Server + participant DB + participant Redis + participant PubSub + + Note over Client,PubSub: Block User Flow + Client->>RPC Server: Block User Request + RPC Server->>DB: Create Block Record + RPC Server->>DB: Update Friendship Status (if exists) + RPC Server->>PubSub: Publish Block Update + PubSub-->>Client: Block Status Update + + Note over Client,PubSub: Unblock User Flow + Client->>RPC Server: Unblock User Request + RPC Server->>DB: Remove Block Record + RPC Server->>PubSub: Publish Unblock Update + PubSub-->>Client: Block Status Update + + Note over Client,PubSub: Block Status Updates + Client->>RPC Server: Subscribe to Block Updates + loop Block Updates + PubSub-->>RPC Server: Block Status Change + RPC Server-->>Client: Stream Block Update + end +``` + ## 🚀 Getting Started ### Prerequisites From 67a9bb962977cc189b5880d498955f8bdd131ec1 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 5 Mar 2025 20:30:29 +0100 Subject: [PATCH 18/26] fix: Return proper blocked at date when getting blocked users --- src/adapters/db.ts | 18 ++++++++--------- .../rpc-server/services/get-blocked-users.ts | 7 ++++--- .../services/get-blocking-status.ts | 5 ++++- src/logic/blocks.ts | 5 +++-- src/types.ts | 8 ++++++-- test/unit/adapters/db.spec.ts | 10 +++++----- .../services/get-blocked-users.spec.ts | 20 +++++++++---------- .../services/get-blocking-status.spec.ts | 12 +++++------ test/unit/logic/blocks.spec.ts | 4 ++-- 9 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 78dfe78..fe8d127 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -8,7 +8,8 @@ import { FriendshipRequest, IDatabaseComponent, User, - Pagination + Pagination, + BlockUserWithDate } from '../types' import { FRIENDSHIPS_PER_PAGE } from './rpc-server/constants' import { normalizeAddress } from '../utils/address' @@ -41,11 +42,6 @@ export function createDBComponent(components: Pick } } - async function getAddressesFromQuery(query: SQLStatement): Promise { - const result = await pg.query(query) - return result.rows.map((row) => row.address) - } - return { async getFriends(userAddress, { onlyActive, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 } } = {}) { const query: SQLStatement = getFriendsBaseQuery(userAddress, { onlyActive, pagination }) @@ -220,15 +216,17 @@ export function createDBComponent(components: Pick }, async getBlockedUsers(blockerAddress) { const query = SQL` - SELECT blocked_address as address FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} + SELECT blocked_address as address, blocked_at FROM blocks WHERE LOWER(blocker_address) = ${normalizeAddress(blockerAddress)} ` - return getAddressesFromQuery(query) + const result = await pg.query(query) + return result.rows }, async getBlockedByUsers(blockedAddress) { const query = SQL` - SELECT blocker_address as address FROM blocks WHERE LOWER(blocked_address) = ${normalizeAddress(blockedAddress)} + SELECT blocker_address as address, blocked_at FROM blocks WHERE LOWER(blocked_address) = ${normalizeAddress(blockedAddress)} ` - return getAddressesFromQuery(query) + const result = await pg.query(query) + return result.rows }, async isFriendshipBlocked(loggedUserAddress, anotherUserAddress) { const normalizedLoggedUserAddress = normalizeAddress(loggedUserAddress) diff --git a/src/adapters/rpc-server/services/get-blocked-users.ts b/src/adapters/rpc-server/services/get-blocked-users.ts index a5fc92a..cb758be 100644 --- a/src/adapters/rpc-server/services/get-blocked-users.ts +++ b/src/adapters/rpc-server/services/get-blocked-users.ts @@ -1,4 +1,4 @@ -import { parseProfilesToFriends } from '../../../logic/friends' +import { parseProfilesToBlockedUsers } from '../../../logic/blocks' import { RpcServerContext, RPCServiceContext } from '../../../types' import { getPage } from '../../../utils/pagination' import { FRIENDSHIPS_PER_PAGE } from '../constants' @@ -17,11 +17,12 @@ export function getBlockedUsersService({ const { address: loggedUserAddress } = context try { - const blockedAddresses = await db.getBlockedUsers(loggedUserAddress) + const blockedUsers = await db.getBlockedUsers(loggedUserAddress) + const blockedAddresses = blockedUsers.map((user) => user.address) const profiles = await catalystClient.getProfiles(blockedAddresses) return { - profiles: parseProfilesToFriends(profiles), + profiles: parseProfilesToBlockedUsers(profiles, blockedUsers), paginationData: { total: blockedAddresses.length, page: getPage(pagination?.limit || FRIENDSHIPS_PER_PAGE, pagination?.offset) diff --git a/src/adapters/rpc-server/services/get-blocking-status.ts b/src/adapters/rpc-server/services/get-blocking-status.ts index 563dad3..79d8e69 100644 --- a/src/adapters/rpc-server/services/get-blocking-status.ts +++ b/src/adapters/rpc-server/services/get-blocking-status.ts @@ -9,11 +9,14 @@ export function getBlockingStatusService({ components: { logs, db } }: RPCServic const { address } = context try { - const [blockedAddresses, blockedByAddresses] = await Promise.all([ + const [blockedUsers, blockedByUsers] = await Promise.all([ db.getBlockedUsers(address), db.getBlockedByUsers(address) ]) + const blockedAddresses = blockedUsers.map((user) => user.address) + const blockedByAddresses = blockedByUsers.map((user) => user.address) + return { blockedUsers: blockedAddresses, blockedByUsers: blockedByAddresses diff --git a/src/logic/blocks.ts b/src/logic/blocks.ts index 66c6bb7..0675b35 100644 --- a/src/logic/blocks.ts +++ b/src/logic/blocks.ts @@ -4,7 +4,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { getProfileInfo, getProfileUserId } from './profiles' import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' -import { SubscriptionEventsEmitter } from '../types' +import { BlockUserWithDate, SubscriptionEventsEmitter } from '../types' export function parseProfileToBlockedUser(profile: Profile, blockedAt?: Date): BlockedUserProfile { const { name, userId, hasClaimedName, profilePictureUrl } = getProfileInfo(profile) @@ -20,8 +20,9 @@ export function parseProfileToBlockedUser(profile: Profile, blockedAt?: Date): B export function parseProfilesToBlockedUsers( profiles: Profile[], - blockedAtByAddress: Map + blockedUsers: BlockUserWithDate[] ): BlockedUserProfile[] { + const blockedAtByAddress = new Map(blockedUsers.map((user) => [user.address, user.blocked_at])) return profiles.map((profile) => { const userId = getProfileUserId(profile) const blockedAt = blockedAtByAddress.get(userId) diff --git a/src/types.ts b/src/types.ts index fb67f19..779293e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -116,8 +116,8 @@ export interface IDatabaseComponent { unblockUser(blockerAddress: string, blockedAddress: string, txClient?: PoolClient): Promise blockUsers(blockerAddress: string, blockedAddresses: string[]): Promise unblockUsers(blockerAddress: string, blockedAddresses: string[]): Promise - getBlockedUsers(blockerAddress: string): Promise - getBlockedByUsers(blockedAddress: string): Promise + getBlockedUsers(blockerAddress: string): Promise + getBlockedByUsers(blockedAddress: string): Promise isFriendshipBlocked(blockerAddress: string, blockedAddress: string): Promise executeTx(cb: (client: PoolClient) => Promise): Promise } @@ -281,6 +281,10 @@ export type User = { address: string } +export type BlockUserWithDate = User & { + blocked_at: Date +} + export enum Action { REQUEST = 'request', // request a friendship CANCEL = 'cancel', // cancel a friendship request diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index e93af50..4b30988 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -646,11 +646,11 @@ describe('db', () => { describe('getBlockedUsers', () => { it('should retrieve blocked users', async () => { - const mockBlockedUsers = [{ address: '0x456' }, { address: '0x789' }] + const mockBlockedUsers = [{ address: '0x456', blocked_at: new Date() }, { address: '0x789', blocked_at: new Date() }] mockPg.query.mockResolvedValueOnce({ rows: mockBlockedUsers, rowCount: mockBlockedUsers.length }) const result = await dbComponent.getBlockedUsers('0x123') - expect(result).toEqual(mockBlockedUsers.map((user) => user.address)) + expect(result).toEqual(mockBlockedUsers) expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('LOWER(blocker_address) ='), @@ -662,14 +662,14 @@ describe('db', () => { describe('getBlockedByUsers', () => { it('should retrieve blocked by users', async () => { - const mockBlockedByUsers = [{ address: '0x456' }, { address: '0x789' }] + const mockBlockedByUsers = [{ address: '0x456', blocked_at: new Date() }, { address: '0x789', blocked_at: new Date() }] mockPg.query.mockResolvedValueOnce({ rows: mockBlockedByUsers, rowCount: mockBlockedByUsers.length }) const result = await dbComponent.getBlockedByUsers('0x123') - expect(result).toEqual(mockBlockedByUsers.map((user) => user.address)) + expect(result).toEqual(mockBlockedByUsers) expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ - text: expect.stringContaining('SELECT blocker_address as address FROM blocks WHERE LOWER(blocked_address) ='), + text: expect.stringContaining('SELECT blocker_address as address, blocked_at FROM blocks WHERE LOWER(blocked_address) ='), values: expect.arrayContaining([normalizeAddress('0x123')]) }) ) diff --git a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts index 139abae..3b31852 100644 --- a/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocked-users.spec.ts @@ -3,7 +3,7 @@ import { getBlockedUsersService } from '../../../../../src/adapters/rpc-server/s import { GetBlockedUsersPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfilesToFriends } from '../../../../../src/logic/friends' +import { parseProfilesToBlockedUsers } from '../../../../../src/logic/blocks' describe('getBlockedUsersService', () => { let getBlockedUsers: ReturnType @@ -20,21 +20,21 @@ describe('getBlockedUsersService', () => { }) it('should return blocked users with profiles and pagination', async () => { - const blockedAddresses = ['0x456', '0x789'] - const mockProfiles = blockedAddresses.map(createMockProfile) + const blockedUsers = [{ address: '0x456', blocked_at: new Date() }, { address: '0x789', blocked_at: new Date() }] + const mockProfiles = blockedUsers.map((user) => createMockProfile(user.address)) const request: GetBlockedUsersPayload = { pagination: { limit: 10, offset: 0 } } - mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedUsers) mockCatalystClient.getProfiles.mockResolvedValueOnce(mockProfiles) const response = await getBlockedUsers(request, rpcContext) expect(response).toEqual({ - profiles: parseProfilesToFriends(mockProfiles), + profiles: parseProfilesToBlockedUsers(mockProfiles, blockedUsers), paginationData: { - total: blockedAddresses.length, + total: blockedUsers.length, page: 1 } }) @@ -42,17 +42,17 @@ describe('getBlockedUsersService', () => { }) it('should use default pagination when not provided', async () => { - const blockedAddresses = ['0x456'] - const mockProfiles = blockedAddresses.map(createMockProfile) + const blockedUsers = [{ address: '0x456', blocked_at: new Date() }] + const mockProfiles = blockedUsers.map((user) => createMockProfile(user.address)) const request: GetBlockedUsersPayload = {} - mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedUsers) mockCatalystClient.getProfiles.mockResolvedValueOnce(mockProfiles) const response = await getBlockedUsers(request, rpcContext) expect(response.paginationData.page).toBe(1) - expect(response.profiles).toEqual(parseProfilesToFriends(mockProfiles)) + expect(response.profiles).toEqual(parseProfilesToBlockedUsers(mockProfiles, blockedUsers)) }) it('should handle errors gracefully', async () => { diff --git a/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts index 4108aeb..f0c50c7 100644 --- a/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-blocking-status.spec.ts @@ -17,16 +17,16 @@ describe('getBlockingStatusService', () => { }) it('should return blocked users and blocked by users addresses', async () => { - const blockedAddresses = ['0x456', '0x789'] - const blockedByAddresses = ['0x123', '0x456'] + const blockedUsers = [{ address: '0x456', blocked_at: new Date() }, { address: '0x789', blocked_at: new Date() }] + const blockedByUsers = [{ address: '0x123', blocked_at: new Date() }, { address: '0x456', blocked_at: new Date() }] - mockDb.getBlockedUsers.mockResolvedValueOnce(blockedAddresses) - mockDb.getBlockedByUsers.mockResolvedValueOnce(blockedByAddresses) + mockDb.getBlockedUsers.mockResolvedValueOnce(blockedUsers) + mockDb.getBlockedByUsers.mockResolvedValueOnce(blockedByUsers) const response = await getBlockingStatus({}, rpcContext) expect(response).toEqual({ - blockedUsers: blockedAddresses, - blockedByUsers: blockedByAddresses + blockedUsers: blockedUsers.map((user) => user.address), + blockedByUsers: blockedByUsers.map((user) => user.address) }) }) diff --git a/test/unit/logic/blocks.spec.ts b/test/unit/logic/blocks.spec.ts index 57ea7c8..45e527c 100644 --- a/test/unit/logic/blocks.spec.ts +++ b/test/unit/logic/blocks.spec.ts @@ -30,8 +30,8 @@ describe('parseProfilesToBlockedUsers', () => { } const profiles = [mockProfile, anotherProfile] const blockedAt123 = new Date() - const blockedAtByAddress = new Map([['0x123', blockedAt123]]) - const result = parseProfilesToBlockedUsers(profiles, blockedAtByAddress) + const blockedUsers = [{ address: '0x123', blocked_at: blockedAt123 }] + const result = parseProfilesToBlockedUsers(profiles, blockedUsers) expect(result).toEqual([ { From 290c01ab0054410bde44a9729f4d9586539ad036 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 5 Mar 2025 20:42:02 +0100 Subject: [PATCH 19/26] feat: When block/unblock, send friendship status update --- .../rpc-server/services/block-user.ts | 22 +++++++++++----- .../rpc-server/services/unblock-user.ts | 17 ++++++++++--- .../rpc-server/services/block-user.spec.ts | 25 ++++++++++++++++++- .../rpc-server/services/unblock-user.spec.ts | 25 +++++++++++++++++-- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index b9f161d..4cfdaaa 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -3,7 +3,7 @@ import { BlockUserPayload, BlockUserResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -import { BLOCK_UPDATES_CHANNEL } from '../../pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../pubsub' import { parseProfileToBlockedUser } from '../../../logic/blocks' export function blockUserService({ @@ -38,20 +38,30 @@ export function blockUserService({ } } - const blockedAt = await db.executeTx(async (tx) => { - const { blocked_at } = await db.blockUser(blockerAddress, blockedAddress, tx) + const { actionId, blockedAt } = await db.executeTx(async (tx) => { + const { blocked_at: blockedAt } = await db.blockUser(blockerAddress, blockedAddress, tx) const friendship = await db.getFriendship([blockerAddress, blockedAddress], tx) - if (!friendship) return blocked_at + if (!friendship) return { blockedAt } - await Promise.all([ + const [_, actionId] = await Promise.all([ db.updateFriendshipStatus(friendship.id, false, tx), db.recordFriendshipAction(friendship.id, blockerAddress, Action.BLOCK, null, tx) ]) - return blocked_at + return { actionId, blockedAt } }) + if (actionId) { + await pubsub.publishInChannel(FRIENDSHIP_UPDATES_CHANNEL, { + id: actionId, + from: blockerAddress, + to: blockedAddress, + action: Action.BLOCK, + timestamp: blockedAt.getTime() + }) + } + await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { address: blockedAddress, isBlocked: true diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index 7a41ee1..73dfdd4 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -3,7 +3,7 @@ import { UnblockUserPayload, UnblockUserResponse } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' -import { BLOCK_UPDATES_CHANNEL } from '../../pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../pubsub' import { parseProfileToBlockedUser } from '../../../logic/blocks' export function unblockUserService({ @@ -36,15 +36,26 @@ export function unblockUserService({ } } - await db.executeTx(async (tx) => { + const actionId = await db.executeTx(async (tx) => { await db.unblockUser(blockerAddress, blockedAddress, tx) const friendship = await db.getFriendship([blockerAddress, blockedAddress], tx) if (!friendship) return - await db.recordFriendshipAction(friendship.id, blockerAddress, Action.DELETE, null, tx) + const actionId = await db.recordFriendshipAction(friendship.id, blockerAddress, Action.DELETE, null, tx) + return actionId }) + if (actionId) { + await pubsub.publishInChannel(FRIENDSHIP_UPDATES_CHANNEL, { + id: actionId, + from: blockerAddress, + to: blockedAddress, + action: Action.DELETE, + timestamp: Date.now() + }) + } + await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { address: blockedAddress, isBlocked: false diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index e7365f6..4323358 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -4,7 +4,7 @@ import { BlockUserPayload } from '@dcl/protocol/out-js/decentraland/social_servi import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' import { PoolClient } from 'pg' -import { BLOCK_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' describe('blockUserService', () => { @@ -87,6 +87,29 @@ describe('blockUserService', () => { expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) + it('should publish a friendship update event after blocking a user if friendship exists', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: BlockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) + mockDb.blockUser.mockResolvedValueOnce({ id: 'block-id', blocked_at: blockedAt }) + mockDb.recordFriendshipAction.mockResolvedValueOnce('action-id') + + await blockUser(request, rpcContext) + + expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(FRIENDSHIP_UPDATES_CHANNEL, { + id: 'action-id', + from: rpcContext.address, + to: blockedAddress, + action: Action.BLOCK, + timestamp: blockedAt.getTime() + }) + }) + it('should publish a block update event after blocking a user', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index f0aa620..22482b0 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -3,10 +3,9 @@ import { unblockUserService } from '../../../../../src/adapters/rpc-server/servi import { UnblockUserPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { Action, Friendship, RpcServerContext } from '../../../../../src/types' import { createMockProfile } from '../../../../mocks/profile' -import { parseProfileToFriend } from '../../../../../src/logic/friends' import { PoolClient } from 'pg' import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' -import { BLOCK_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' describe('unblockUserService', () => { let unblockUser: ReturnType @@ -81,6 +80,28 @@ describe('unblockUserService', () => { expect(mockDb.recordFriendshipAction).not.toHaveBeenCalled() }) + it('should publish a friendship update event after unblocking a user if friendship exists', async () => { + const blockedAddress = '0x456' + const mockProfile = createMockProfile(blockedAddress) + const request: UnblockUserPayload = { + user: { address: blockedAddress } + } + + mockCatalystClient.getProfile.mockResolvedValueOnce(mockProfile) + mockDb.getFriendship.mockResolvedValueOnce({ id: 'friendship-id' } as Friendship) + mockDb.recordFriendshipAction.mockResolvedValueOnce('action-id') + + await unblockUser(request, rpcContext) + + expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(FRIENDSHIP_UPDATES_CHANNEL, { + id: 'action-id', + from: rpcContext.address, + to: blockedAddress, + action: Action.DELETE, + timestamp: Date.now() + }) + }) + it('should publish a block update event after unblocking a user', async () => { const blockedAddress = '0x456' const mockProfile = createMockProfile(blockedAddress) From a91147085a896e18096a8c39ce6b1d01e3380d22 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Wed, 5 Mar 2025 20:42:28 +0100 Subject: [PATCH 20/26] fix: GetFriends only retrieve active friendships --- src/adapters/rpc-server/services/get-friends.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts index 9950dfd..dc460ee 100644 --- a/src/adapters/rpc-server/services/get-friends.ts +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -21,8 +21,8 @@ export function getFriendsService({ try { const [friends, total] = await Promise.all([ - db.getFriends(loggedUserAddress, { pagination }), - db.getFriendsCount(loggedUserAddress) + db.getFriends(loggedUserAddress, { pagination, onlyActive: true }), + db.getFriendsCount(loggedUserAddress, { onlyActive: true }) ]) const profiles = await catalystClient.getProfiles(friends.map((friend) => friend.address)) From 2231b4c98d63e34b19f462e3a19a1ec13d34560e Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 09:21:47 +0100 Subject: [PATCH 21/26] fix: Drop table after dropping indexes --- src/migrations/1737745803297_add-blocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/migrations/1737745803297_add-blocks.ts b/src/migrations/1737745803297_add-blocks.ts index bb604c7..5343460 100644 --- a/src/migrations/1737745803297_add-blocks.ts +++ b/src/migrations/1737745803297_add-blocks.ts @@ -28,8 +28,8 @@ export async function up(pgm: MigrationBuilder): Promise { } export async function down(pgm: MigrationBuilder): Promise { - pgm.dropTable('blocks') pgm.dropIndex('blocks', ['blocker_address']) pgm.dropIndex('blocks', ['blocked_address']) pgm.dropIndex('blocks', ['blocker_address', 'blocked_address']) + pgm.dropTable('blocks') } From b7b296517e224a01596e7c2f1a4b776a969a5110 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 09:45:56 +0100 Subject: [PATCH 22/26] feat: Return more informative errores on block/unblock --- package.json | 2 +- .../rpc-server/services/block-user.ts | 13 +++--- .../rpc-server/services/unblock-user.ts | 12 ++--- .../rpc-server/services/block-user.spec.ts | 46 +++++++++++-------- .../rpc-server/services/unblock-user.spec.ts | 46 ++++++++++++------- yarn.lock | 12 ++--- 6 files changed, 77 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 65e09f9..333d85b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 4cfdaaa..fcf4c78 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -5,6 +5,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../pubsub' import { parseProfileToBlockedUser } from '../../../logic/blocks' +import { EthAddress } from '@dcl/schemas' export function blockUserService({ components: { logs, db, catalystClient, pubsub } @@ -16,11 +17,11 @@ export function blockUserService({ const { address: blockerAddress } = context const blockedAddress = request.user?.address - if (!blockedAddress) { + if (!EthAddress.validate(blockedAddress)) { return { response: { - $case: 'internalServerError', - internalServerError: { message: 'User address is missing in the request payload' } + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } } } } @@ -30,9 +31,9 @@ export function blockUserService({ if (!profile) { return { response: { - $case: 'internalServerError', - internalServerError: { - message: 'Profile not found' + $case: 'profileNotFound', + profileNotFound: { + message: `Profile not found for address ${blockedAddress}` } } } diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index 73dfdd4..e7dee3a 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -5,7 +5,7 @@ import { } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../pubsub' import { parseProfileToBlockedUser } from '../../../logic/blocks' - +import { EthAddress } from '@dcl/schemas' export function unblockUserService({ components: { logs, db, catalystClient, pubsub } }: RPCServiceContext<'logs' | 'db' | 'catalystClient' | 'pubsub'>) { @@ -16,11 +16,11 @@ export function unblockUserService({ const { address: blockerAddress } = context const blockedAddress = request.user?.address - if (!blockedAddress) { + if (!EthAddress.validate(blockedAddress)) { return { response: { - $case: 'internalServerError', - internalServerError: { message: 'User address is missing in the request payload' } + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } } } } @@ -30,8 +30,8 @@ export function unblockUserService({ if (!profile) { return { response: { - $case: 'internalServerError', - internalServerError: { message: 'Profile not found' } + $case: 'profileNotFound', + profileNotFound: { message: `Profile not found for address ${blockedAddress}` } } } } diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 4323358..4dc5a6c 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -6,11 +6,14 @@ import { createMockProfile } from '../../../../mocks/profile' import { PoolClient } from 'pg' import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' - +import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' +import { EthAddress } from '@dcl/schemas' describe('blockUserService', () => { let blockUser: ReturnType let mockClient: jest.Mocked + let blockedAddress: EthAddress let blockedAt: Date + let mockProfile: Profile const rpcContext: RpcServerContext = { address: '0x123', @@ -25,12 +28,12 @@ describe('blockUserService', () => { mockClient = (await mockPg.getPool().connect()) as jest.Mocked mockDb.executeTx.mockImplementationOnce(async (cb) => cb(mockClient)) + blockedAddress = '0x12356abC4078a0Cc3b89b419928b857B8AF826ef' + mockProfile = createMockProfile(blockedAddress) blockedAt = new Date() }) it('should block a user successfully, update friendship status, and record friendship action if it exists', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } @@ -62,8 +65,6 @@ describe('blockUserService', () => { }) it('should block a user successfully and do nothing else if friendship does not exist', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } @@ -88,8 +89,6 @@ describe('blockUserService', () => { }) it('should publish a friendship update event after blocking a user if friendship exists', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } @@ -111,8 +110,6 @@ describe('blockUserService', () => { }) it('should publish a block update event after blocking a user', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } @@ -128,7 +125,7 @@ describe('blockUserService', () => { }) }) - it('should return internalServerError when user address is missing', async () => { + it('should return invalidRequest when user address is missing', async () => { const request: BlockUserPayload = { user: { address: '' } } @@ -137,15 +134,30 @@ describe('blockUserService', () => { expect(response).toEqual({ response: { - $case: 'internalServerError', - internalServerError: { message: 'User address is missing in the request payload' } + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } } }) expect(mockDb.blockUser).not.toHaveBeenCalled() }) - it('should return internalServerError when profile is not found', async () => { - const blockedAddress = '0x456' + it('should return invalidRequest when user address is invalid', async () => { + const request: BlockUserPayload = { + user: { address: 'invalid-address' } + } + + const response = await blockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } + } + }) + expect(mockDb.blockUser).not.toHaveBeenCalled() + }) + + it('should return profileNotFound when profile is not found', async () => { const request: BlockUserPayload = { user: { address: blockedAddress } } @@ -156,16 +168,14 @@ describe('blockUserService', () => { expect(response).toEqual({ response: { - $case: 'internalServerError', - internalServerError: { message: 'Profile not found' } + $case: 'profileNotFound', + profileNotFound: { message: `Profile not found for address ${blockedAddress}` } } }) expect(mockDb.blockUser).not.toHaveBeenCalled() }) it('should handle database errors', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: BlockUserPayload = { user: { address: blockedAddress } } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index 22482b0..b33e81f 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -6,10 +6,14 @@ import { createMockProfile } from '../../../../mocks/profile' import { PoolClient } from 'pg' import { parseProfileToBlockedUser } from '../../../../../src/logic/blocks' import { BLOCK_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../../../src/adapters/pubsub' +import { EthAddress } from '@dcl/schemas' +import { Profile } from 'dcl-catalyst-client/dist/client/specs/lambdas-client' describe('unblockUserService', () => { let unblockUser: ReturnType let mockClient: jest.Mocked + let blockedAddress: EthAddress + let mockProfile: Profile const rpcContext: RpcServerContext = { address: '0x123', @@ -23,11 +27,12 @@ describe('unblockUserService', () => { mockClient = (await mockPg.getPool().connect()) as jest.Mocked mockDb.executeTx.mockImplementationOnce(async (cb) => cb(mockClient)) + + blockedAddress = '0x12356abC4078a0Cc3b89b419928b857B8AF826ef' + mockProfile = createMockProfile(blockedAddress) }) it('should unblock a user successfully and mark as deleted if friendship exists', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } @@ -57,8 +62,6 @@ describe('unblockUserService', () => { }) it('should unblock a user successfully and do nothing else if friendship does not exist', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } @@ -81,8 +84,6 @@ describe('unblockUserService', () => { }) it('should publish a friendship update event after unblocking a user if friendship exists', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } @@ -103,8 +104,6 @@ describe('unblockUserService', () => { }) it('should publish a block update event after unblocking a user', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } @@ -120,7 +119,7 @@ describe('unblockUserService', () => { }) }) - it('should return internalServerError when user address is missing', async () => { + it('should return invalidRequest when user address is missing', async () => { const request: UnblockUserPayload = { user: { address: '' } } @@ -129,15 +128,30 @@ describe('unblockUserService', () => { expect(response).toEqual({ response: { - $case: 'internalServerError', - internalServerError: { message: 'User address is missing in the request payload' } + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } } }) expect(mockDb.unblockUser).not.toHaveBeenCalled() }) - it('should return internalServerError when profile is not found', async () => { - const blockedAddress = '0x456' + it('should return invalidRequest when user address is invalid', async () => { + const request: UnblockUserPayload = { + user: { address: 'invalid-address' } + } + + const response = await unblockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Invalid user address in the request payload' } + } + }) + expect(mockDb.unblockUser).not.toHaveBeenCalled() + }) + + it('should return profileNotFound when profile is not found', async () => { const request: UnblockUserPayload = { user: { address: blockedAddress } } @@ -148,16 +162,14 @@ describe('unblockUserService', () => { expect(response).toEqual({ response: { - $case: 'internalServerError', - internalServerError: { message: 'Profile not found' } + $case: 'profileNotFound', + profileNotFound: { message: `Profile not found for address ${blockedAddress}` } } }) expect(mockDb.unblockUser).not.toHaveBeenCalled() }) it('should handle database errors', async () => { - const blockedAddress = '0x456' - const mockProfile = createMockProfile(blockedAddress) const request: UnblockUserPayload = { user: { address: blockedAddress } } diff --git a/yarn.lock b/yarn.lock index 53e476e..02eb4ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,18 +756,18 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz": - version "1.0.0-13649677549.commit-1cf5d36" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13649677549.commit-1cf5d36.tgz#2da416e27288a4e3bf1d92bf36ffa253b292cb0a" - dependencies: - "@dcl/ts-proto" "1.154.0" - "@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz": version "1.0.0-13654029584.commit-3cb93f2" resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz#ef9c7a5df65853e7251643f6a8f83a82f87e2314" dependencies: "@dcl/ts-proto" "1.154.0" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz": + version "1.0.0-13694322690.commit-6b791cc" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz#d176398cf51f9278fcb7677ef45a58a1c44eb8d4" + dependencies: + "@dcl/ts-proto" "1.154.0" + "@dcl/rpc@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@dcl/rpc/-/rpc-1.1.2.tgz#789f4f24c8d432a48df3e786b77d017883dda11a" From 25c7b629820aad8a1fa9d4ea78a78093208960c0 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 10:35:56 +0100 Subject: [PATCH 23/26] fix: Missing block updates handler + should not block/unblock yourself --- package.json | 2 +- src/adapters/rpc-server/rpc-server.ts | 6 ++++-- src/adapters/rpc-server/services/block-user.ts | 9 +++++++++ src/adapters/rpc-server/services/unblock-user.ts | 9 +++++++++ src/logic/friendships.ts | 11 +++++++++++ src/logic/updates.ts | 13 +++++++++++++ test/unit/adapters/rpc-server.spec.ts | 8 ++++---- .../rpc-server/services/block-user.spec.ts | 16 ++++++++++++++++ .../rpc-server/services/unblock-user.spec.ts | 16 ++++++++++++++++ yarn.lock | 12 ++++++------ 10 files changed, 89 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 333d85b..42b2ecf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-sns": "^3.734.0", "@dcl/platform-crypto-middleware": "^1.1.0", - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13695216731.commit-2860fc3.tgz", "@dcl/rpc": "^1.1.2", "@dcl/schemas": "^16.0.0", "@well-known-components/env-config-provider": "^1.2.0", diff --git a/src/adapters/rpc-server/rpc-server.ts b/src/adapters/rpc-server/rpc-server.ts index e39b4d2..eb6db5c 100644 --- a/src/adapters/rpc-server/rpc-server.ts +++ b/src/adapters/rpc-server/rpc-server.ts @@ -10,11 +10,12 @@ import { SocialServiceDefinition } from '@dcl/protocol/out-js/decentraland/socia import { getSentFriendshipRequestsService } from './services/get-sent-friendship-requests' import { getFriendshipStatusService } from './services/get-friendship-status' import { subscribeToFriendConnectivityUpdatesService } from './services/subscribe-to-friend-connectivity-updates' -import { FRIEND_STATUS_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIEND_STATUS_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../pubsub' import { friendshipUpdateHandler, friendConnectivityUpdateHandler, - friendshipAcceptedUpdateHandler + friendshipAcceptedUpdateHandler, + blockUpdateHandler } from '../../logic/updates' import { blockUserService } from './services/block-user' import { getBlockedUsersService } from './services/get-blocked-users' @@ -114,6 +115,7 @@ export async function createRpcServerComponent({ FRIEND_STATUS_UPDATES_CHANNEL, friendConnectivityUpdateHandler(subscribersContext, logger, db) ) + await pubsub.subscribeToChannel(BLOCK_UPDATES_CHANNEL, blockUpdateHandler(subscribersContext, logger)) }, attachUser({ transport, address }) { logger.debug('[DEBUGGING CONNECTION] Attaching user to RPC', { diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index fcf4c78..6127ffd 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -17,6 +17,15 @@ export function blockUserService({ const { address: blockerAddress } = context const blockedAddress = request.user?.address + if (blockerAddress === blockedAddress) { + return { + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Cannot block yourself' } + } + } + } + if (!EthAddress.validate(blockedAddress)) { return { response: { diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index e7dee3a..d2e5eb3 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -16,6 +16,15 @@ export function unblockUserService({ const { address: blockerAddress } = context const blockedAddress = request.user?.address + if (blockerAddress === blockedAddress) { + return { + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Cannot unblock yourself' } + } + } + } + if (!EthAddress.validate(blockedAddress)) { return { response: { diff --git a/src/logic/friendships.ts b/src/logic/friendships.ts index 217d4b2..960f3ce 100644 --- a/src/logic/friendships.ts +++ b/src/logic/friendships.ts @@ -193,6 +193,17 @@ export function parseEmittedUpdateToFriendshipUpdate( } } } + case Action.BLOCK: + return { + update: { + $case: 'block', + block: { + user: { + address: update.from + } + } + } + } default: return null } diff --git a/src/logic/updates.ts b/src/logic/updates.ts index 742833a..701838b 100644 --- a/src/logic/updates.ts +++ b/src/logic/updates.ts @@ -117,6 +117,19 @@ export function friendConnectivityUpdateHandler( }, logger) } +export function blockUpdateHandler(subscribersContext: ISubscribersContext, logger: ILogger) { + return handleUpdate<'blockUpdate'>((update) => { + logger.info('Block update', { + update: JSON.stringify(update) + }) + + const updateEmitter = subscribersContext.getOrAddSubscriber(update.address) + if (updateEmitter) { + updateEmitter.emit('blockUpdate', update) + } + }, logger) +} + export async function* handleSubscriptionUpdates({ rpcContext, eventName, diff --git a/test/unit/adapters/rpc-server.spec.ts b/test/unit/adapters/rpc-server.spec.ts index 4caaa06..81f4b7d 100644 --- a/test/unit/adapters/rpc-server.spec.ts +++ b/test/unit/adapters/rpc-server.spec.ts @@ -11,9 +11,8 @@ import { mockUWs, mockWorldsStats } from '../../mocks/components' -import { FRIEND_STATUS_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../src/adapters/pubsub' +import { BLOCK_UPDATES_CHANNEL, FRIEND_STATUS_UPDATES_CHANNEL, FRIENDSHIP_UPDATES_CHANNEL } from '../../../src/adapters/pubsub' import { mockSns } from '../../mocks/components/sns' -import mitt from 'mitt' import * as updates from '../../../src/logic/updates' jest.mock('@dcl/rpc', () => ({ @@ -28,7 +27,6 @@ describe('createRpcServerComponent', () => { let rpcServerMock: jest.Mocked> let setHandlerMock: jest.Mock, attachTransportMock: jest.Mock let mockTransport: Transport - let mockEmitter: ReturnType let subscribersContext: ISubscribersContext beforeEach(async () => { @@ -41,7 +39,6 @@ describe('createRpcServerComponent', () => { setHandlerMock = rpcServerMock.setHandler as jest.Mock attachTransportMock = rpcServerMock.attachTransport as jest.Mock - mockEmitter = mitt() mockTransport = { on: jest.fn(), send: jest.fn(), @@ -71,6 +68,7 @@ describe('createRpcServerComponent', () => { jest.spyOn(updates, 'friendshipUpdateHandler') jest.spyOn(updates, 'friendshipAcceptedUpdateHandler') jest.spyOn(updates, 'friendConnectivityUpdateHandler') + jest.spyOn(updates, 'blockUpdateHandler') mockConfig.getNumber.mockResolvedValueOnce(8085) }) @@ -82,6 +80,7 @@ describe('createRpcServerComponent', () => { expect(mockPubSub.subscribeToChannel).toHaveBeenCalledWith(FRIENDSHIP_UPDATES_CHANNEL, expect.any(Function)) expect(mockPubSub.subscribeToChannel).toHaveBeenCalledWith(FRIENDSHIP_UPDATES_CHANNEL, expect.any(Function)) expect(mockPubSub.subscribeToChannel).toHaveBeenCalledWith(FRIEND_STATUS_UPDATES_CHANNEL, expect.any(Function)) + expect(mockPubSub.subscribeToChannel).toHaveBeenCalledWith(BLOCK_UPDATES_CHANNEL, expect.any(Function)) }) it('should call the correct handlers', async () => { @@ -92,6 +91,7 @@ describe('createRpcServerComponent', () => { expect(updates.friendshipUpdateHandler).toHaveBeenCalledWith(subscribersContext, mockLogger) expect(updates.friendshipAcceptedUpdateHandler).toHaveBeenCalledWith(subscribersContext, mockLogger) expect(updates.friendConnectivityUpdateHandler).toHaveBeenCalledWith(subscribersContext, mockLogger, mockDb) + expect(updates.blockUpdateHandler).toHaveBeenCalledWith(subscribersContext, mockLogger) }) }) diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 4dc5a6c..6b98393 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -125,6 +125,22 @@ describe('blockUserService', () => { }) }) + it('should return invalidRequest when user is trying to block himself', async () => { + const request: BlockUserPayload = { + user: { address: rpcContext.address } + } + + const response = await blockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Cannot block yourself' } + } + }) + expect(mockDb.blockUser).not.toHaveBeenCalled() + }) + it('should return invalidRequest when user address is missing', async () => { const request: BlockUserPayload = { user: { address: '' } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index b33e81f..d8d260e 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -119,6 +119,22 @@ describe('unblockUserService', () => { }) }) + it('should return invalidRequest when user is trying to unblock himself', async () => { + const request: UnblockUserPayload = { + user: { address: rpcContext.address } + } + + const response = await unblockUser(request, rpcContext) + + expect(response).toEqual({ + response: { + $case: 'invalidRequest', + invalidRequest: { message: 'Cannot unblock yourself' } + } + }) + expect(mockDb.unblockUser).not.toHaveBeenCalled() + }) + it('should return invalidRequest when user address is missing', async () => { const request: UnblockUserPayload = { user: { address: '' } diff --git a/yarn.lock b/yarn.lock index 02eb4ec..e19248f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -756,18 +756,18 @@ "@well-known-components/fetch-component" "^2.0.2" "@well-known-components/interfaces" "^1.4.2" -"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz": - version "1.0.0-13654029584.commit-3cb93f2" - resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13654029584.commit-3cb93f2.tgz#ef9c7a5df65853e7251643f6a8f83a82f87e2314" - dependencies: - "@dcl/ts-proto" "1.154.0" - "@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz": version "1.0.0-13694322690.commit-6b791cc" resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13694322690.commit-6b791cc.tgz#d176398cf51f9278fcb7677ef45a58a1c44eb8d4" dependencies: "@dcl/ts-proto" "1.154.0" +"@dcl/protocol@https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13695216731.commit-2860fc3.tgz": + version "1.0.0-13695216731.commit-2860fc3" + resolved "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-13695216731.commit-2860fc3.tgz#d7706767d7f09782d304167a0f316a34ae72b332" + dependencies: + "@dcl/ts-proto" "1.154.0" + "@dcl/rpc@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@dcl/rpc/-/rpc-1.1.2.tgz#789f4f24c8d432a48df3e786b77d017883dda11a" From 79f6a9cf886eee50c879ffec72067e2ab36168fa Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 12:19:16 +0100 Subject: [PATCH 24/26] chore: Reduce noise in the logs --- src/logic/updates.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/logic/updates.ts b/src/logic/updates.ts index 701838b..7130988 100644 --- a/src/logic/updates.ts +++ b/src/logic/updates.ts @@ -100,9 +100,7 @@ export function friendConnectivityUpdateHandler( logger.debug('Processing connectivity update:', { update: JSON.stringify(update), subscribersCount: onlineSubscribers.length, - subscribers: onlineSubscribers.join(', '), - friendsCount: friends.length, - friends: JSON.stringify(friends) + friendsCount: friends.length }) friends.forEach(({ address: friendAddress }) => { From 30c53c0d67bc45c9f2b822ddd634d8c2526c9419 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 13:52:04 +0100 Subject: [PATCH 25/26] fix: Get Mutual Friends query was using incorrect table alias in the subqueries --- src/adapters/db.ts | 6 ++++-- src/logic/queries.ts | 10 ++++++---- test/unit/adapters/db.spec.ts | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index fe8d127..77635e9 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -53,11 +53,13 @@ export function createDBComponent(components: Pick return getCount(query) }, async getMutualFriends(userAddress1, userAddress2, pagination = { limit: FRIENDSHIPS_PER_PAGE, offset: 0 }) { - const result = await pg.query(getMutualFriendsBaseQuery(userAddress1, userAddress2, { pagination })) + const query = getMutualFriendsBaseQuery(userAddress1, userAddress2, { pagination }) + const result = await pg.query(query) return result.rows }, async getMutualFriendsCount(userAddress1, userAddress2) { - return getCount(getMutualFriendsBaseQuery(userAddress1, userAddress2, { onlyCount: true })) + const query = getMutualFriendsBaseQuery(userAddress1, userAddress2, { onlyCount: true }) + return getCount(query) }, async getFriendship(users, txClient) { const [userAddress1, userAddress2] = users.map(normalizeAddress) diff --git a/src/logic/queries.ts b/src/logic/queries.ts index aba2346..4bfc20a 100644 --- a/src/logic/queries.ts +++ b/src/logic/queries.ts @@ -150,10 +150,11 @@ export function getMutualFriendsBaseQuery( const normalizedUserAddress2 = normalizeAddress(userAddress2) const { pagination, onlyCount } = options - const friendsSubquery = (address: string, tableAlias: string) => - SQL` + const friendsSubquery = (address: string, tableAlias: string) => { + const friendAddressCase = getFriendAddressCase(address, tableAlias) + return SQL` SELECT ` - .append(getFriendAddressCase(address)) + .append(friendAddressCase) .append( SQL` as address FROM friendships ` @@ -165,7 +166,8 @@ export function getMutualFriendsBaseQuery( .append(tableAlias) .append(SQL`.is_active = true`) .append(SQL` AND `) - .append(getBlockingCondition(address)) + .append(getBlockingCondition(address, friendAddressCase)) + } const query = SQL`WITH friendsA as (`.append(friendsSubquery(normalizedUserAddress1, 'f_a')).append(SQL`) SELECT `) diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 4b30988..670ec36 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -163,7 +163,7 @@ describe('db', () => { const expectedQueryFragments = [ { - text: 'WHEN LOWER(address_requester) =', + text: 'WHEN LOWER(f_a.address_requester) =', values: ['0x123'] }, { @@ -226,7 +226,7 @@ describe('db', () => { values: [] }, { - text: 'WHEN LOWER(address_requester) =', + text: 'WHEN LOWER(f_a.address_requester) =', values: ['0x123'] }, { From 53c50dbff81056eb3093a45d22ab4b5f36cb07d8 Mon Sep 17 00:00:00 2001 From: Kevin Szuchet Date: Thu, 6 Mar 2025 15:18:55 +0100 Subject: [PATCH 26/26] fix: Block updates should notify who blocks/unblocks to the blocked/unblocked --- .../rpc-server/services/block-user.ts | 3 +- .../services/subscribe-to-block-updates.ts | 6 ++- .../rpc-server/services/unblock-user.ts | 3 +- src/logic/blocks.ts | 8 +-- src/logic/updates.ts | 2 +- src/types.ts | 3 +- .../rpc-server/services/block-user.spec.ts | 3 +- .../subscribe-to-block-updates.spec.ts | 11 ++-- .../rpc-server/services/unblock-user.spec.ts | 3 +- test/unit/logic/blocks.spec.ts | 10 +++- test/unit/logic/updates.spec.ts | 52 +++++++++++++++++-- 11 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/adapters/rpc-server/services/block-user.ts b/src/adapters/rpc-server/services/block-user.ts index 6127ffd..9e72e46 100644 --- a/src/adapters/rpc-server/services/block-user.ts +++ b/src/adapters/rpc-server/services/block-user.ts @@ -73,7 +73,8 @@ export function blockUserService({ } await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { - address: blockedAddress, + blockerAddress, + blockedAddress, isBlocked: true }) diff --git a/src/adapters/rpc-server/services/subscribe-to-block-updates.ts b/src/adapters/rpc-server/services/subscribe-to-block-updates.ts index 2ed4daf..ed8b140 100644 --- a/src/adapters/rpc-server/services/subscribe-to-block-updates.ts +++ b/src/adapters/rpc-server/services/subscribe-to-block-updates.ts @@ -12,6 +12,7 @@ export function subscribeToBlockUpdatesService({ return async function* (_request: Empty, context: RpcServerContext): AsyncGenerator { let cleanup: (() => void) | undefined + // The blocked/unblocked user should know who blocked/unblocked them try { cleanup = yield* handleSubscriptionUpdates({ rpcContext: context, @@ -21,9 +22,10 @@ export function subscribeToBlockUpdatesService({ logger }, shouldRetrieveProfile: false, - getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address, + getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.blockerAddress, parser: parseEmittedUpdateToBlockUpdate, - shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address === context.address + shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => + update.blockedAddress === context.address }) } catch (error: any) { logger.error('Error in block updates subscription:', error) diff --git a/src/adapters/rpc-server/services/unblock-user.ts b/src/adapters/rpc-server/services/unblock-user.ts index d2e5eb3..2a9cebe 100644 --- a/src/adapters/rpc-server/services/unblock-user.ts +++ b/src/adapters/rpc-server/services/unblock-user.ts @@ -66,7 +66,8 @@ export function unblockUserService({ } await pubsub.publishInChannel(BLOCK_UPDATES_CHANNEL, { - address: blockedAddress, + blockerAddress, + blockedAddress, isBlocked: false }) diff --git a/src/logic/blocks.ts b/src/logic/blocks.ts index 0675b35..2929d46 100644 --- a/src/logic/blocks.ts +++ b/src/logic/blocks.ts @@ -30,10 +30,12 @@ export function parseProfilesToBlockedUsers( }) } -export function parseEmittedUpdateToBlockUpdate(update: SubscriptionEventsEmitter['blockUpdate']): BlockUpdate | null { - const { address, isBlocked } = update +export function parseEmittedUpdateToBlockUpdate( + update: Pick +): BlockUpdate | null { + const { blockerAddress, isBlocked } = update return { - address, + address: blockerAddress, isBlocked } } diff --git a/src/logic/updates.ts b/src/logic/updates.ts index 7130988..b17c934 100644 --- a/src/logic/updates.ts +++ b/src/logic/updates.ts @@ -121,7 +121,7 @@ export function blockUpdateHandler(subscribersContext: ISubscribersContext, logg update: JSON.stringify(update) }) - const updateEmitter = subscribersContext.getOrAddSubscriber(update.address) + const updateEmitter = subscribersContext.getOrAddSubscriber(update.blockedAddress) if (updateEmitter) { updateEmitter.emit('blockUpdate', update) } diff --git a/src/types.ts b/src/types.ts index 779293e..e63abf1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -256,7 +256,8 @@ export type SubscriptionEventsEmitter = { status: ConnectivityStatus } blockUpdate: { - address: string + blockerAddress: string + blockedAddress: string isBlocked: boolean } } diff --git a/test/unit/adapters/rpc-server/services/block-user.spec.ts b/test/unit/adapters/rpc-server/services/block-user.spec.ts index 6b98393..abf01ca 100644 --- a/test/unit/adapters/rpc-server/services/block-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/block-user.spec.ts @@ -120,7 +120,8 @@ describe('blockUserService', () => { await blockUser(request, rpcContext) expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(BLOCK_UPDATES_CHANNEL, { - address: blockedAddress, + blockerAddress: rpcContext.address, + blockedAddress, isBlocked: true }) }) diff --git a/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts b/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts index aa71f2a..26b8530 100644 --- a/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts +++ b/test/unit/adapters/rpc-server/services/subscribe-to-block-updates.spec.ts @@ -14,7 +14,8 @@ describe('subscribeToBlockUpdatesService', () => { const mockHandler = handleSubscriptionUpdates as jest.Mock const mockUpdate = { - address: '0x456', + blockerAddress: '0x123', + blockedAddress: '0x456', isBlocked: true } @@ -76,7 +77,7 @@ describe('subscribeToBlockUpdatesService', () => { const result = await generator.next() const getAddressFromUpdate = mockHandler.mock.calls[0][0].getAddressFromUpdate - expect(getAddressFromUpdate(mockUpdate)).toBe(mockUpdate.address) + expect(getAddressFromUpdate(mockUpdate)).toBe(mockUpdate.blockerAddress) }) it('should filter updates based on address conditions', async () => { @@ -87,12 +88,14 @@ describe('subscribeToBlockUpdatesService', () => { const loggedUserAddress = '0x123' const mockUpdateBlockingNonLoggedUser = { - address: '0x456', + blockedAddress: '0x456', + blockerAddress: loggedUserAddress, isBlocked: true } const mockUpdateBlockingLoggedUser = { - address: loggedUserAddress, + blockedAddress: loggedUserAddress, + blockerAddress: '0x456', isBlocked: true } diff --git a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts index d8d260e..571dd5c 100644 --- a/test/unit/adapters/rpc-server/services/unblock-user.spec.ts +++ b/test/unit/adapters/rpc-server/services/unblock-user.spec.ts @@ -114,7 +114,8 @@ describe('unblockUserService', () => { await unblockUser(request, rpcContext) expect(mockPubSub.publishInChannel).toHaveBeenCalledWith(BLOCK_UPDATES_CHANNEL, { - address: blockedAddress, + blockerAddress: rpcContext.address, + blockedAddress, isBlocked: false }) }) diff --git a/test/unit/logic/blocks.spec.ts b/test/unit/logic/blocks.spec.ts index 45e527c..3b22c88 100644 --- a/test/unit/logic/blocks.spec.ts +++ b/test/unit/logic/blocks.spec.ts @@ -1,4 +1,4 @@ -import { parseProfilesToBlockedUsers, parseProfileToBlockedUser } from '../../../src/logic/blocks' +import { parseEmittedUpdateToBlockUpdate, parseProfilesToBlockedUsers, parseProfileToBlockedUser } from '../../../src/logic/blocks' import { mockProfile } from '../../mocks/profile' describe('parseProfileToBlockedUser', () => { @@ -50,3 +50,11 @@ describe('parseProfilesToBlockedUsers', () => { ]) }) }) + +describe('parseEmittedUpdateToBlockUpdate', () => { + it('should parse emitted update to block update', () => { + const update = { blockerAddress: '0x123', blockedAddress: '0x456', isBlocked: true } + const result = parseEmittedUpdateToBlockUpdate(update) + expect(result).toEqual({ address: '0x123', isBlocked: true }) + }) +}) \ No newline at end of file diff --git a/test/unit/logic/updates.spec.ts b/test/unit/logic/updates.spec.ts index 1beffaf..83f644c 100644 --- a/test/unit/logic/updates.spec.ts +++ b/test/unit/logic/updates.spec.ts @@ -3,7 +3,8 @@ import { friendConnectivityUpdateHandler, handleSubscriptionUpdates, ILogger, - friendshipAcceptedUpdateHandler + friendshipAcceptedUpdateHandler, + blockUpdateHandler } from '../../../src/logic/updates' import { ConnectivityStatus } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' import { mockCatalystClient, mockDb, mockLogs } from '../../mocks/components' @@ -214,6 +215,49 @@ describe('updates handlers', () => { }) }) + describe('blockUpdateHandler', () => { + it('should emit block update to the correct subscriber', () => { + const handler = blockUpdateHandler(subscribersContext, logger) + const subscriber = subscribersContext.getOrAddSubscriber('0x456') + const emitSpy = jest.spyOn(subscriber, 'emit') + + const update = { + blockerAddress: '0x123', + blockedAddress: '0x456', + isBlocked: true + } + + handler(JSON.stringify(update)) + + expect(emitSpy).toHaveBeenCalledWith('blockUpdate', update) + }) + + it('should not emit if subscriber does not exist', () => { + const handler = blockUpdateHandler(subscribersContext, logger) + const nonExistentUpdate = { + id: 'update-1', + from: '0x123', + to: '0xNONEXISTENT', + action: Action.REQUEST, + timestamp: Date.now() + } + + expect(handler(JSON.stringify(nonExistentUpdate))).resolves.toBeUndefined() + }) + + it('should log error on invalid JSON', () => { + const handler = blockUpdateHandler(subscribersContext, logger) + const errorSpy = jest.spyOn(logger, 'error') + + handler('invalid json') + + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Error handling update:'), + expect.objectContaining({ message: 'invalid json' }) + ) + }) + }) + describe('handleSubscriptionUpdates', () => { let eventEmitter: Emitter let logger: ILogger @@ -222,7 +266,7 @@ describe('updates handlers', () => { let subscribersContext: ISubscribersContext const friendshipUpdate = { id: '1', to: '0x456', from: '0x123', action: Action.REQUEST, timestamp: Date.now() } - const blockUpdate = { address: '0x123', isBlocked: true } + const blockUpdate = { blockerAddress: '0x456', blockedAddress: '0x123', isBlocked: true } beforeEach(() => { eventEmitter = mitt() @@ -389,8 +433,8 @@ describe('updates handlers', () => { logger }, shouldRetrieveProfile: false, - getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address, - shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.address === '0x123', + getAddressFromUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.blockerAddress, + shouldHandleUpdate: (update: SubscriptionEventsEmitter['blockUpdate']) => update.blockedAddress === '0x123', parser })