From 8c5d92fa74e565f54482daf937ad07b4e3954add Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio <1120791+LautaroPetaccio@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:07:26 -0300 Subject: [PATCH] feat: Add more data to terms of services (#762) * feat: Add more ToS events * fix: Add tests --- src/Collection/Collection.router.spec.ts | 93 ++++++++++++++++++++++++ src/Collection/Collection.router.ts | 61 +++++++++++----- src/Collection/Collection.schema.ts | 18 ++++- src/Collection/Collection.types.ts | 5 ++ 4 files changed, 157 insertions(+), 20 deletions(-) diff --git a/src/Collection/Collection.router.spec.ts b/src/Collection/Collection.router.spec.ts index ab68b3ef..5310fe2a 100644 --- a/src/Collection/Collection.router.spec.ts +++ b/src/Collection/Collection.router.spec.ts @@ -68,6 +68,7 @@ import { } from '../SlotUsageCheque' import { CurationStatus } from '../Curation' import { isCommitteeMember } from '../Committee' +import * as Warehouse from '../warehouse' import { app } from '../server' import { hasPublicAccess } from './access' import { toFullCollection } from './utils' @@ -78,6 +79,7 @@ import { FullCollection, CollectionSort, CollectionTypeFilter, + TermsOfServiceEvent, } from './Collection.types' const server = supertest(app.getApp()) @@ -93,6 +95,7 @@ jest.mock('../Committee') jest.mock('../Item/Item.model') jest.mock('./Collection.model') jest.mock('./access') +jest.mock('../warehouse') const thirdPartyAPIMock = thirdPartyAPI as jest.Mocked const tpUrnPrefix = 'urn:decentraland:amoy:collections-v2' @@ -3097,4 +3100,94 @@ describe('Collection router', () => { }) }) }) + + describe('when saving the ToS', () => { + let body: any + + beforeEach(() => { + url = `/collections/${dbCollectionMock.id}/tos` + }) + + describe('and the collection does not exist', () => { + beforeEach(() => { + ;(Collection.count as jest.Mock).mockResolvedValueOnce(0) + body = { + email: 'email@company.com', + } + }) + + it('should throw a 404 error', () => { + return server + .post(buildURL(url)) + .set(createAuthHeaders('post', url)) + .send(body) + .expect(404) + .then((response: any) => { + expect(response.body).toEqual({ + data: { id: dbCollection.id, tableName: Collection.tableName }, + error: `Couldn't find "${dbCollection.id}" on ${Collection.tableName}`, + ok: false, + }) + }) + }) + }) + + describe('and the collection exists', () => { + beforeEach(() => { + ;(Collection.count as jest.Mock).mockResolvedValueOnce(1) + }) + + describe("and the ToS don't follow the schema", () => { + beforeEach(() => { + body = { + email: 'This is not an email', + } + }) + + it('should throw a 400 error', () => { + return server + .post(buildURL(url)) + .set(createAuthHeaders('post', url)) + .send(body) + .expect(400) + .then((response: any) => { + expect(response.body).toEqual({ + data: expect.any(Object), + error: 'Invalid request body', + ok: false, + }) + }) + }) + }) + + describe('and the ToS is correct', () => { + beforeEach(() => { + ;(Collection.findByIds as jest.Mock).mockResolvedValueOnce([ + dbCollection, + ]) + ;(Warehouse.sendDataToWarehouse as jest.Mock).mockResolvedValueOnce( + undefined + ) + body = { + email: 'email@company.com', + event: TermsOfServiceEvent.PUBLISH_THIRD_PARTY_ITEMS, + hashes: ['hash1', 'hash2'], + } + }) + + it('should save the ToS and return a 200', () => { + return server + .post(buildURL(url)) + .set(createAuthHeaders('post', url)) + .send(body) + .expect(200) + .then((response: any) => { + expect(response.body).toEqual({ + ok: true, + }) + }) + }) + }) + }) + }) }) diff --git a/src/Collection/Collection.router.ts b/src/Collection/Collection.router.ts index a320a5f0..47b47c19 100644 --- a/src/Collection/Collection.router.ts +++ b/src/Collection/Collection.router.ts @@ -4,7 +4,6 @@ import { omit } from 'decentraland-commons/dist/utils' import { withCors } from '../middleware/cors' import { Router } from '../common/Router' import { HTTPError, STATUS_CODES } from '../common/HTTPError' -import { getValidator } from '../utils/validator' import { InvalidRequestError } from '../utils/errors' import { withModelAuthorization, @@ -39,7 +38,11 @@ import { } from '../Pagination/utils' import { CurationStatusFilter } from '../Curation' import { addCustomMaxAgeCacheControlHeader } from '../common/headers' -import { hasTPCollectionURN, isTPCollection } from '../utils/urn' +import { + getThirdPartyCollectionURN, + hasTPCollectionURN, + isTPCollection, +} from '../utils/urn' import { ForumService } from '../Forum/Forum.service' import { Collection } from './Collection.model' import { CollectionService } from './Collection.service' @@ -49,6 +52,7 @@ import { FullCollection, CollectionTypeFilter, CollectionSort, + TermsOfServiceEvent, } from './Collection.types' import { upsertCollectionSchema, saveTOSSchema } from './Collection.schema' import { hasPublicAccess } from './access' @@ -64,8 +68,6 @@ import { WrongCollectionError, } from './Collection.errors' -const validator = getValidator() - export class CollectionRouter extends Router { public service = new CollectionService() public forumService = new ForumService() @@ -150,6 +152,7 @@ export class CollectionRouter extends Router { withCors, withAuthentication, withCollectionExists, + withSchemaValidation(saveTOSSchema), server.handleRequest(this.saveTOS) ) @@ -478,23 +481,45 @@ export class CollectionRouter extends Router { } saveTOS = async (req: AuthRequest): Promise => { - const tosValidator = validator.compile(saveTOSSchema) - tosValidator(req.body) - if (tosValidator.errors) { - throw new HTTPError( - 'Invalid request', - tosValidator.errors, - STATUS_CODES.badRequest - ) + const id = server.extractFromReq(req, 'id') + const collection = await this.service.getCollection(id) + const collection_address = collection.contract_address ?? 'Unknown address' + const eth_address = req.auth.ethAddress + const urn = + collection.third_party_id && collection.urn_suffix + ? getThirdPartyCollectionURN( + collection.third_party_id, + collection.urn_suffix + ) + : 'Unknown urn' + const event = req.body.event ?? TermsOfServiceEvent.PUBLISH_COLLECTION + + let body: + | ({ email: string; eth_address: string } & { + collection_address: string + }) + | { urn: string; hashes: string[] } + + switch (event) { + case TermsOfServiceEvent.PUBLISH_THIRD_PARTY_ITEMS: + body = { + email: req.body.email, + urn, + hashes: req.body.hashes, + } + break + case TermsOfServiceEvent.PUBLISH_COLLECTION: + default: + body = { + email: req.body.email, + eth_address, + collection_address, + } + break } - const eth_address = req.auth.ethAddress try { - await sendDataToWarehouse('builder', 'publish_collection_tos', { - email: req.body.email, - eth_address: eth_address, - collection_address: req.body.collection_address, - }) + await sendDataToWarehouse('builder', event, body) } catch (e) { throw new HTTPError( "The TOS couldn't be recorded", diff --git a/src/Collection/Collection.schema.ts b/src/Collection/Collection.schema.ts index 177d1dec..80382077 100644 --- a/src/Collection/Collection.schema.ts +++ b/src/Collection/Collection.schema.ts @@ -1,5 +1,6 @@ import { ContractNetwork } from '@dcl/schemas' import { matchers } from '../common/matchers' +import { TermsOfServiceEvent } from './Collection.types' export const collectionSchema = Object.freeze({ type: 'object', @@ -59,8 +60,21 @@ export const saveTOSSchema = Object.freeze({ type: 'string', pattern: `^${matchers.email}$`, }, - collection_address: { type: 'string', pattern: `^${matchers.address}$` }, + collection_address: { type: 'string' }, + event: { + type: 'string', + enum: [ + TermsOfServiceEvent.PUBLISH_COLLECTION, + TermsOfServiceEvent.PUBLISH_THIRD_PARTY_ITEMS, + ], + }, + hashes: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + maxItems: 40000, + }, }, + required: ['email'], additionalProperties: false, - required: ['email', 'collection_address'], }) diff --git a/src/Collection/Collection.types.ts b/src/Collection/Collection.types.ts index bad94c03..e98f6e8c 100644 --- a/src/Collection/Collection.types.ts +++ b/src/Collection/Collection.types.ts @@ -28,6 +28,11 @@ export type CollectionAttributes = { updated_at: Date } +export enum TermsOfServiceEvent { + PUBLISH_COLLECTION = 'publish_collection_tos', + PUBLISH_THIRD_PARTY_ITEMS = 'publish_third_party_items_tos', +} + export type ThirdPartyCollectionAttributes = CollectionAttributes & { third_party_id: string urn_suffix: string