From 4ce25e04bffcfb8b5ceb430168a6bda23b516012 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 14:18:30 -0300 Subject: [PATCH 01/12] fixes --- package.json | 1 - src/metrics.ts | 2 +- yarn.lock | 71 ++------------------------------------------------ 3 files changed, 3 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index bf85a1c..17b0310 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@dcl/rpc": "^1.1.2", "@well-known-components/env-config-provider": "^1.2.0", "@well-known-components/fetch-component": "^2.0.2", - "@well-known-components/http-server": "^2.1.0", "@well-known-components/interfaces": "^1.4.3", "@well-known-components/logger": "^3.1.3", "@well-known-components/metrics": "^2.1.0", diff --git a/src/metrics.ts b/src/metrics.ts index 295db30..074ae14 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,5 +1,5 @@ import { validateMetricsDeclaration } from '@well-known-components/metrics' -import { getDefaultHttpMetrics } from '@well-known-components/http-server' +import { getDefaultHttpMetrics } from '@well-known-components/uws-http-server' import { metricDeclarations as logsMetricsDeclarations } from '@well-known-components/logger' export const metricDeclarations = { diff --git a/yarn.lock b/yarn.lock index 6096c95..5bd0186 100644 --- a/yarn.lock +++ b/yarn.lock @@ -942,11 +942,6 @@ dependencies: "@types/node" "*" -"@types/http-errors@^2.0.1": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" @@ -1169,20 +1164,6 @@ "@well-known-components/interfaces" "^1.4.1" cross-fetch "^3.1.5" -"@well-known-components/http-server@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@well-known-components/http-server/-/http-server-2.1.0.tgz#23a18edc82904b3a575452c2d7e618c7da37a07f" - integrity sha512-IHD7aLTA+9DYEchQubHDBwc4FmVEmQC+2TWbi8Tz+QlkiQdtndcuba8XHH+EwqlB5sna/EAJGZGXPxS7okcHKA== - dependencies: - "@types/http-errors" "^2.0.1" - destroy "^1.2.0" - fp-future "^1.0.1" - http-errors "^2.0.0" - mitt "^3.0.0" - node-fetch "^2.6.9" - on-finished "^2.4.1" - path-to-regexp "^6.2.1" - "@well-known-components/interfaces@^1.1.0", "@well-known-components/interfaces@^1.4.1", "@well-known-components/interfaces@^1.4.2", "@well-known-components/interfaces@^1.4.3": version "1.4.3" resolved "https://registry.npmjs.org/@well-known-components/interfaces/-/interfaces-1.4.3.tgz" @@ -1703,16 +1684,6 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -destroy@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -1764,11 +1735,6 @@ dprint-node@^1.0.8: dependencies: detect-libc "^1.0.3" -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - electron-to-chromium@^1.4.668: version "1.4.708" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.708.tgz" @@ -2284,17 +2250,6 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -2339,7 +2294,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4: +inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3071,7 +3026,7 @@ nise@^5.1.5: just-extend "^6.2.0" path-to-regexp "^6.2.1" -node-fetch@^2.6.12, node-fetch@^2.6.9: +node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -3143,13 +3098,6 @@ obuf@~1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@^2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - once@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -3613,11 +3561,6 @@ semver@^7.5.3, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -3699,11 +3642,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -3825,11 +3763,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" From c443d31ca14c7008be9a15fbd28dbd6e663db4de Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 14:19:19 -0300 Subject: [PATCH 02/12] wip tests --- jest.config.js | 12 ++--- src/types.ts | 20 ++----- test/components.ts | 15 +++--- test/integration/rpcserver.spec.ts | 27 ++++++++++ test/rpc.ts | 85 ++++++++++++++++++++++++++++++ test/tsconfig.json | 26 +++++---- 6 files changed, 140 insertions(+), 45 deletions(-) create mode 100644 test/integration/rpcserver.spec.ts create mode 100644 test/rpc.ts diff --git a/jest.config.js b/jest.config.js index 90bc9fd..d8e79b2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,10 @@ module.exports = { transform: { - "^.+\\.(ts|tsx)$": ["ts-jest", {tsconfig: "test/tsconfig.json"}] + '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'test/tsconfig.json' }] }, - moduleFileExtensions: ["ts", "js"], - coverageDirectory: "coverage", - collectCoverageFrom: ["src/**/*.ts", "src/**/*.js"], - testMatch: ["**/*.spec.(ts)"], - testEnvironment: "node", + moduleFileExtensions: ['ts', 'js'], + coverageDirectory: 'coverage', + collectCoverageFrom: ['src/**/*.ts', 'src/**/*.js'], + testMatch: ['**/*.spec.(ts)'], + testEnvironment: 'node' } diff --git a/src/types.ts b/src/types.ts index 795c491..e6ec9e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ import type { IConfigComponent, ILoggerComponent, - IHttpServerComponent, IBaseComponent, IMetricsComponent, IFetchComponent @@ -9,13 +8,14 @@ import type { import { IPgComponent } from '@well-known-components/pg-component' import { WebSocketServer } from 'ws' import { Emitter } from 'mitt' +import { HttpRequest, HttpResponse, IUWsComponent, WebSocket } from '@well-known-components/uws-http-server' import { metricDeclarations } from './metrics' import { IDatabaseComponent } from './adapters/db' import { IRedisComponent } from './adapters/redis' import { IRPCServerComponent } from './adapters/rpcServer' import { IPubSubComponent } from './adapters/pubsub' -import { HttpRequest, HttpResponse, IUWsComponent, WebSocket } from '@well-known-components/uws-http-server' import { IUWebSocketEventMap } from './utils/UWebSocketTransport' +import { ISocialServiceRpcClientComponent } from '../test/rpc' export type GlobalContext = { components: BaseComponents @@ -40,8 +40,7 @@ export type AppComponents = BaseComponents // components used in tests export type TestComponents = BaseComponents & { - // A fetch component that only hits the test server - localFetch: IFetchComponent + socialServiceClient: ISocialServiceRpcClientComponent } export type JsonBody = Record @@ -73,19 +72,6 @@ export type WsUserData = export type InternalWebSocket = WebSocket -// this type simplifies the typings of http handlers -export type HandlerContextWithPath< - ComponentNames extends keyof AppComponents, - Path extends string = any -> = IHttpServerComponent.PathAwareContext< - IHttpServerComponent.DefaultContext<{ - components: Pick - }>, - Path -> - -export type Context = IHttpServerComponent.PathAwareContext - export type IWebSocketComponent = IBaseComponent & { ws: WebSocketServer } diff --git a/test/components.ts b/test/components.ts index 9e2bc80..779879d 100644 --- a/test/components.ts +++ b/test/components.ts @@ -1,11 +1,12 @@ // This file is the "test-environment" analogous for src/components.ts // Here we define the test components to be used in the testing environment -import { createRunner, createLocalFetchCompoment } from "@well-known-components/test-helpers" +import { createRunner, createLocalFetchCompoment } from '@well-known-components/test-helpers' -import { main } from "../src/service" -import { TestComponents } from "../src/types" -import { initComponents as originalInitComponents } from "../src/components" +import { main } from '../src/service' +import { TestComponents } from '../src/types' +import { initComponents as originalInitComponents } from '../src/components' +import { createSocialServiceRpcClientComponent } from './rpc' /** * Behaves like Jest "describe" function, used to describe a test for a @@ -16,16 +17,14 @@ import { initComponents as originalInitComponents } from "../src/components" */ export const test = createRunner({ main, - initComponents, + initComponents }) async function initComponents(): Promise { const components = await originalInitComponents() - const { config } = components - return { ...components, - localFetch: await createLocalFetchCompoment(config), + socialServiceClient: await createSocialServiceRpcClientComponent({ logs: components.logs }) } } diff --git a/test/integration/rpcserver.spec.ts b/test/integration/rpcserver.spec.ts new file mode 100644 index 0000000..c13ac2e --- /dev/null +++ b/test/integration/rpcserver.spec.ts @@ -0,0 +1,27 @@ +import { test } from '../components' +import { AUTH_ADDRESS } from '../rpc' + +test('RpcServer', ({ components }) => { + it('should create a friendship', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'request', + request: { + user: { + address: '0xA' + } + } + } + }) + console.log('response > ', response) + expect(response?.response).not.toBe(undefined) + expect(response?.response?.$case).toBe('accepted') + console.log('expected all good') + // const friendship = await components.db.getFriendship([AUTH_ADDRESS, '0xa']) + // expect(friendship).not.toBe(undefined) + // expect(friendship?.is_active).toBeFalsy() + // expect(friendship?.address_requester).toBe(AUTH_ADDRESS) + // expect(friendship?.address_requested).toBe('0xa') + }) +}) diff --git a/test/rpc.ts b/test/rpc.ts new file mode 100644 index 0000000..ba86955 --- /dev/null +++ b/test/rpc.ts @@ -0,0 +1,85 @@ +import { SocialServiceDefinition } from '@dcl/protocol/out-js/decentraland/social_service_v2/social_service.gen' +import { createRpcClient } from '@dcl/rpc' +import { AuthLinkType, Authenticator } from '@dcl/crypto' +import { loadService } from '@dcl/rpc/dist/codegen' +import { IWebSocket, WebSocketTransport } from '@dcl/rpc/dist/transports/WebSocket' +import createAuthChainHeaders from '@dcl/platform-crypto-middleware/dist/createAuthChainHeader' + +import { WebSocket } from 'ws' +import future from 'fp-future' +import { IBaseComponent, ILoggerComponent } from '@well-known-components/interfaces' +import { FromTsProtoServiceDefinition, RawClient } from '@dcl/rpc/dist/codegen-types' + +const identity = { + ephemeralIdentity: { + address: '0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34', + publicKey: + '0x0420c548d960b06dac035d1daf826472eded46b8b9d123294f1199c56fa235c89f2515158b1e3be0874bfb15b42d1551db8c276787a654d0b8d7b4d4356e70fe42', + privateKey: '0xbc453a92d9baeb3d10294cbc1d48ef6738f718fd31b4eb8085efe7b311299399' + }, + expiration: new Date('3021-10-16T22:32:29.626Z'), + authChain: [ + { + type: AuthLinkType.SIGNER, + payload: '0x7949f9f239d1a0816ce5eb364a1f588ae9cc1bf5', + signature: '' + }, + { + type: AuthLinkType.ECDSA_PERSONAL_EPHEMERAL, + payload: `Decentraland Login\nEphemeral address: 0x84452bbFA4ca14B7828e2F3BBd106A2bD495CD34\nExpiration: 3021-10-16T22:32:29.626Z`, + signature: + '0x39dd4ddf131ad2435d56c81c994c4417daef5cf5998258027ef8a1401470876a1365a6b79810dc0c4a2e9352befb63a9e4701d67b38007d83ffc4cd2b7a38ad51b' + } + ] +} + +export const AUTH_ADDRESS = identity.authChain[0].payload + +export type ISocialServiceRpcClientComponent = IBaseComponent & { + client: RawClient> +} + +export async function createSocialServiceRpcClientComponent({ + logs +}: { + logs: ILoggerComponent +}): Promise { + const logger = logs.getLogger('social-service-rpc-client') + let socialServiceClient: RawClient> + let connection: WebSocket | undefined + + return { + get client() { + return socialServiceClient! + }, + start: async () => { + const ws = new WebSocket('ws://0.0.0.0:3000') + const ts = Date.now() + const payload = ['get', '/', String(ts), JSON.stringify({})].join(':').toLowerCase() + const chain = Authenticator.signPayload(identity, payload) + const headers = createAuthChainHeaders(chain, ts) + + const toBeResolved = future() + + ws.on('open', () => { + ws.send(JSON.stringify(headers)) + toBeResolved.resolve(true) + }) + + await toBeResolved + + const transport = WebSocketTransport(ws as IWebSocket) + const client = await createRpcClient(transport) + const port = await client.createPort('ss-client') + + const service = loadService(port, SocialServiceDefinition) + + socialServiceClient = service + connection = ws + }, + stop: async () => { + logger.debug('closing connection') + connection?.close() + } + } +} diff --git a/test/tsconfig.json b/test/tsconfig.json index a98d147..5341bb6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,17 +1,15 @@ { "compilerOptions": { - "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ - "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": true, /* Generates corresponding '.map' file. */ - "types": [ - "node", - "jest" - ], - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ - "noEmit": true + "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "declaration": true /* Generates corresponding '.d.ts' file. */, + "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + "types": ["node", "jest"], + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "noEmit": true, + "strict": true } -} \ No newline at end of file +} From d3f6558fa82c7042974a3ba853fb8f57c32e17f2 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:23:51 -0300 Subject: [PATCH 03/12] local dev tools --- .dockerignore | 4 +++- Makefile | 44 ++++++++++++++++++++++++++++++++++++++++ docker-compose.local.yml | 23 +++++++++++++++++++++ docker-compose.test.yml | 19 +++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 docker-compose.local.yml create mode 100644 docker-compose.test.yml diff --git a/.dockerignore b/.dockerignore index 5171c54..f3f226f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ node_modules -npm-debug.log \ No newline at end of file +npm-debug.log +docker-compose.* +Makefile \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..927cb71 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +DOCKER_COMPOSE_EXISTS = $(shell which docker-compose > /dev/null && echo 1 || echo 0) + +RUN_SERVICES = docker-compose -f docker-compose.local.yml up -d && docker exec social_service_ea_db bash -c "until pg_isready; do sleep 1; done" > /dev/null && sleep 5 +RUN_TEST_SERVICES = docker-compose -f docker-compose.test.yml up -d && docker exec social_service_ea_test_db bash -c "until pg_isready; do sleep 1; done" > /dev/null && sleep 5 +LOCAL_DB = $(shell docker ps | grep social_service_ea_db > /dev/null && echo 1 || echo 0) + +run-services: +ifeq ($(DOCKER_COMPOSE_EXISTS), 1) + @$(RUN_SERVICES) +else + @$(ERROR) "Install Docker in order to run the local DB" + @exit 1; +endif + +run-test-services: +ifeq ($(DOCKER_COMPOSE_EXISTS), 1) + @$(RUN_TEST_SERVICES) +else + @$(ERROR) "Install Docker in order to run the local DB" + @exit 1; +endif + +stop-services: + -@docker stop social_service_ea_db + -@docker stop social_service_ea_redis + +stop-test-services: + -@docker stop social_service_ea_test_db + -@docker stop social_service_ea_test_redis + -@docker rm social_service_ea_test_db + -@docker rm social_service_ea_test_redis + +# Local testing +tests: +ifeq ($(LOCAL_DB), 1) + @make stop-services + @make run-test-services + -@npm run test + @make stop-test-services +else + @make run-test-services + -@npm run test + @make stop-test-services +endif \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..e851926 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,23 @@ +version: '3.8' +services: + postgres: + container_name: 'social_service_ea_db' + image: 'postgres:latest' + restart: always + user: postgres + volumes: + - postgres_volume:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=social_service_ea + ports: + - '5432:5432' + redis: + container_name: 'social_service_ea_redis' + image: 'redis:latest' + restart: always + user: redis + ports: + - '6379:6379' +volumes: + postgres_volume: diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..c89609b --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,19 @@ +version: '3.8' +services: + postgres: + container_name: 'social_service_ea_test_db' + image: 'postgres:latest' + restart: always + user: postgres + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=social_service_ea + ports: + - '5432:5432' + redis: + container_name: 'social_service_ea_test_redis' + image: 'redis:latest' + restart: always + user: redis + ports: + - '6379:6379' From 90eacc4ed595457baa20ed731fd25c3df0b75a8b Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:24:27 -0300 Subject: [PATCH 04/12] release client after commit --- src/adapters/db.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 4cdcaff..73466c9 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -129,8 +129,6 @@ export function createDBComponent(components: Pick async updateFriendshipStatus(friendshipId, isActive, txClient) { logger.debug(`updating ${friendshipId} - ${isActive}`) const query = SQL`UPDATE friendships SET is_active = ${isActive}, updated_at = now() WHERE id = ${friendshipId}` - console.log(query.text) - console.log(query.values) if (txClient) { const results = await txClient.query(query) @@ -195,16 +193,20 @@ export function createDBComponent(components: Pick async executeTx(cb: (client: PoolClient) => Promise): Promise { const pool = pg.getPool() const client = await pool.connect() + let res: T | undefined await client.query('BEGIN') try { - const res = await cb(client) + const callbackResult = await cb(client) await client.query('COMMIT') - return res + res = callbackResult } catch (error) { logger.error(error as any) await client.query('ROLLBACK') client.release() throw error + } finally { + client.release() + return res as T } } } From 5c2cea9cf9fdc70a88dff4d4704523585c155479 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:24:37 -0300 Subject: [PATCH 05/12] stop pubsub without checking --- src/adapters/pubsub.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/adapters/pubsub.ts b/src/adapters/pubsub.ts index edcc862..60686db 100644 --- a/src/adapters/pubsub.ts +++ b/src/adapters/pubsub.ts @@ -28,13 +28,9 @@ export default function createPubSubComponent(components: Pick Date: Fri, 26 Apr 2024 16:24:47 -0300 Subject: [PATCH 06/12] integration tests --- test/db.ts | 18 ++ test/integration/rpcserver.spec.ts | 275 +++++++++++++++++++++++++++-- test/rpc.ts | 25 ++- 3 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 test/db.ts diff --git a/test/db.ts b/test/db.ts new file mode 100644 index 0000000..3146be3 --- /dev/null +++ b/test/db.ts @@ -0,0 +1,18 @@ +import { IDatabaseComponent } from '../src/adapters/db' +import { Action } from '../src/types' + +export async function createFriendshipRequest( + db: IDatabaseComponent, + users: [string, string], + metadata?: Record +) { + const newFriendshipId = await db.createFriendship(users, false) + await db.recordFriendshipAction(newFriendshipId, users[0], Action.REQUEST, metadata || null) + return newFriendshipId +} + +export async function createActiveFriendship(db: IDatabaseComponent, users: [string, string]) { + const id = await db.createFriendship(users, true) + await db.recordFriendshipAction(id, users[0], Action.REQUEST, null) + await db.recordFriendshipAction(id, users[1], Action.ACCEPT, null) +} diff --git a/test/integration/rpcserver.spec.ts b/test/integration/rpcserver.spec.ts index c13ac2e..fdf6a1c 100644 --- a/test/integration/rpcserver.spec.ts +++ b/test/integration/rpcserver.spec.ts @@ -1,27 +1,264 @@ +import { Action } from '../../src/types' +import { normalizeAddress } from '../../src/utils/address' import { test } from '../components' -import { AUTH_ADDRESS } from '../rpc' +import { createActiveFriendship, createFriendshipRequest } from '../db' +import { AUTH_ADDRESS, Identity, getIdentity } from '../rpc' test('RpcServer', ({ components }) => { - it('should create a friendship', async () => { - const { socialServiceClient } = components - const response = await socialServiceClient.client.upsertFriendship({ - action: { - $case: 'request', - request: { - user: { - address: '0xA' - } + describe('getFriends()', () => { + it('should return all active friendships', async () => { + await createActiveFriendship(components.db, [AUTH_ADDRESS, '0xb']) + + const gen = components.socialServiceClient.client.getFriends({}) + const next = await gen.next() + expect(next.value.users.length).toBe(1) + expect(next.value.users[0].address).toBe('0xb') + const next2 = await gen.next() + expect(next2.done).toBeTruthy() + expect(next2.value.users.length).toBe(0) + }) + }) + + describe('getMutualFriends()', () => { + it('should return all mutual friends between two users', async () => { + await createActiveFriendship(components.db, [AUTH_ADDRESS, '0xb']) + await createActiveFriendship(components.db, [AUTH_ADDRESS, '0xc']) + await createActiveFriendship(components.db, ['0xb', '0xc']) + + const gen = components.socialServiceClient.client.getMutualFriends({ + user: { + address: '0xb' } + }) + const next = await gen.next() + expect(next.value.users.length).toBe(1) + expect(next.value.users[0].address).toBe('0xc') + const next2 = await gen.next() + expect(next2.done).toBeTruthy() + expect(next2.value.users.length).toBe(0) + }) + }) + + describe('getPendingFriendshipRequests()', () => { + it('should return all pending requests', async () => { + const { db, socialServiceClient } = components + { + const id = await createFriendshipRequest(db, ['0xd', AUTH_ADDRESS]) + + const response = await socialServiceClient.client.getPendingFriendshipRequests({}) + + expect(response.response?.$case).toBe('requests') + expect((response.response as any).requests.requests.length).toBe(1) + expect((response.response as any).requests.requests[0].user.address).toBe('0xd') + const requestAction = await db.getLastFriendshipAction(id) + expect((response.response as any).requests.requests[0].createdAt).toBe( + new Date(requestAction!.timestamp).getTime() + ) + expect((response.response as any).requests.requests[0].message).toBe('') + } + + { + const id = await createFriendshipRequest(db, ['0xe', AUTH_ADDRESS], { message: 'Hi!' }) + + const response = await socialServiceClient.client.getPendingFriendshipRequests({}) + + expect(response.response?.$case).toBe('requests') + expect((response.response as any).requests.requests.length).toBe(2) + expect((response.response as any).requests.requests[1].user.address).toBe('0xe') + const requestAction = await db.getLastFriendshipAction(id) + expect((response.response as any).requests.requests[1].createdAt).toBe( + new Date(requestAction!.timestamp).getTime() + ) + expect((response.response as any).requests.requests[1].message).toBe(requestAction?.metadata?.message) } }) - console.log('response > ', response) - expect(response?.response).not.toBe(undefined) - expect(response?.response?.$case).toBe('accepted') - console.log('expected all good') - // const friendship = await components.db.getFriendship([AUTH_ADDRESS, '0xa']) - // expect(friendship).not.toBe(undefined) - // expect(friendship?.is_active).toBeFalsy() - // expect(friendship?.address_requester).toBe(AUTH_ADDRESS) - // expect(friendship?.address_requested).toBe('0xa') + }) + + describe('getSentFriendshipRequests()', () => { + it('should return all sent requests', async () => { + const { db, socialServiceClient } = components + { + const id = await createFriendshipRequest(db, [AUTH_ADDRESS, '0xf']) + + const response = await socialServiceClient.client.getSentFriendshipRequests({}) + + expect(response.response?.$case).toBe('requests') + expect((response.response as any).requests.requests.length).toBe(1) + expect((response.response as any).requests.requests[0].user.address).toBe('0xf') + const requestAction = await db.getLastFriendshipAction(id) + expect((response.response as any).requests.requests[0].createdAt).toBe( + new Date(requestAction!.timestamp).getTime() + ) + expect((response.response as any).requests.requests[0].message).toBe('') + } + + { + const id = await createFriendshipRequest(db, [AUTH_ADDRESS, '0xg'], { message: 'hi!' }) + + const response = await socialServiceClient.client.getSentFriendshipRequests({}) + + expect(response.response?.$case).toBe('requests') + expect((response.response as any).requests.requests.length).toBe(2) + expect((response.response as any).requests.requests[1].user.address).toBe('0xg') + const requestAction = await db.getLastFriendshipAction(id) + expect((response.response as any).requests.requests[1].createdAt).toBe( + new Date(requestAction!.timestamp).getTime() + ) + expect((response.response as any).requests.requests[1].message).toBe('hi!') + } + }) + }) + + describe('upsertFriendship()', () => { + let identity: Identity + beforeAll(async () => { + identity = await getIdentity() + }) + it('should create a new NOT active friendship', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'request', + request: { + user: { + address: identity.realAccount.address + } + } + } + }) + expect(response?.response).not.toBe(undefined) + expect(response?.response?.$case).toBe('accepted') + const friendship = await components.db.getFriendship([ + AUTH_ADDRESS, + normalizeAddress(identity.realAccount.address) + ]) + expect(friendship).not.toBe(undefined) + expect(friendship?.is_active).toBeFalsy() + expect(friendship?.address_requester).toBe(AUTH_ADDRESS) + expect(friendship?.address_requested).toBe(normalizeAddress(identity.realAccount.address)) + const lastActionRecorded = await components.db.getLastFriendshipAction(friendship!.id) + expect(lastActionRecorded?.action).toBe(Action.REQUEST) + expect(lastActionRecorded?.acting_user).toBe(AUTH_ADDRESS) + }) + + it('should create a new NOT active friendship WITH a request message', async () => { + const { socialServiceClient } = components + const newIdentity = await getIdentity() + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'request', + request: { + user: { + address: newIdentity.realAccount.address + }, + message: 'Hi!, how u doing?' + } + } + }) + expect(response?.response).not.toBe(undefined) + expect(response?.response?.$case).toBe('accepted') + const friendship = await components.db.getFriendship([ + AUTH_ADDRESS, + normalizeAddress(newIdentity.realAccount.address) + ]) + expect(friendship).not.toBe(undefined) + expect(friendship?.is_active).toBeFalsy() + expect(friendship?.address_requester).toBe(AUTH_ADDRESS) + expect(friendship?.address_requested).toBe(normalizeAddress(newIdentity.realAccount.address)) + const lastActionRecorded = await components.db.getLastFriendshipAction(friendship!.id) + expect(lastActionRecorded?.action).toBe(Action.REQUEST) + expect(lastActionRecorded?.acting_user).toBe(AUTH_ADDRESS) + expect(lastActionRecorded?.metadata).toBeTruthy() + expect(lastActionRecorded?.metadata).toEqual({ + message: 'Hi!, how u doing?' + }) + }) + + it('should be an invalid friendship action if requester sends a REJECT', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'reject', + reject: { + user: { + address: identity.realAccount.address + } + } + } + }) + + expect(response.response?.$case).toBe('invalidFriendshipAction') + }) + + it('should be an invalid friendship action if requester sends an ACCEPT', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'accept', + accept: { + user: { + address: identity.realAccount.address + } + } + } + }) + + expect(response.response?.$case).toBe('invalidFriendshipAction') + }) + + it('should be an invalid friendship action if requester sends a DELETE', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'delete', + delete: { + user: { + address: identity.realAccount.address + } + } + } + }) + + expect(response.response?.$case).toBe('invalidFriendshipAction') + }) + + it('should be an invalid friendship action if user has already requested friendship', async () => { + const { socialServiceClient } = components + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'request', + request: { + user: { + address: identity.realAccount.address + } + } + } + }) + + expect(response.response?.$case).toBe('invalidFriendshipAction') + }) + + it('should active the friendship if requested user sends an ACCEPT', async () => { + const newFriendshipId = await createFriendshipRequest(components.db, ['0xa', AUTH_ADDRESS]) + const { socialServiceClient } = components + + const response = await socialServiceClient.client.upsertFriendship({ + action: { + $case: 'accept', + accept: { + user: { + address: '0xA' + } + } + } + }) + + expect(response.response?.$case).toBe('accepted') + const friendship = await components.db.getFriendship([AUTH_ADDRESS, '0xa']) + expect(friendship?.id).toBe(newFriendshipId) + expect(friendship?.is_active).toBeTruthy() + expect(friendship?.address_requested).toBe(AUTH_ADDRESS) + expect(friendship?.address_requester).toBe('0xa') + }) }) }) diff --git a/test/rpc.ts b/test/rpc.ts index ba86955..f592423 100644 --- a/test/rpc.ts +++ b/test/rpc.ts @@ -1,6 +1,7 @@ import { SocialServiceDefinition } from '@dcl/protocol/out-js/decentraland/social_service_v2/social_service.gen' import { createRpcClient } from '@dcl/rpc' -import { AuthLinkType, Authenticator } from '@dcl/crypto' +import { AuthLinkType, Authenticator, AuthIdentity, IdentityType } from '@dcl/crypto' +import { createUnsafeIdentity } from '@dcl/crypto/dist/crypto' import { loadService } from '@dcl/rpc/dist/codegen' import { IWebSocket, WebSocketTransport } from '@dcl/rpc/dist/transports/WebSocket' import createAuthChainHeaders from '@dcl/platform-crypto-middleware/dist/createAuthChainHeader' @@ -66,6 +67,10 @@ export async function createSocialServiceRpcClientComponent({ toBeResolved.resolve(true) }) + ws.on('error', (err) => { + logger.error(err as any) + }) + await toBeResolved const transport = WebSocketTransport(ws as IWebSocket) @@ -83,3 +88,21 @@ export async function createSocialServiceRpcClientComponent({ } } } + +export type Identity = { authChain: AuthIdentity; realAccount: IdentityType; ephemeralIdentity: IdentityType } + +export async function getIdentity(): Promise { + const ephemeralIdentity = createUnsafeIdentity() + const realAccount = createUnsafeIdentity() + + const authChain = await Authenticator.initializeAuthChain( + realAccount.address, + ephemeralIdentity, + 10, + async (message) => { + return Authenticator.createSignature(realAccount, message) + } + ) + + return { authChain, realAccount, ephemeralIdentity } +} From 575daf2c0d54d3fd84d19b59a47d9c797f4f9771 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:30:56 -0300 Subject: [PATCH 07/12] add redis service --- .github/workflows/build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 217bc32..18bf1a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,4 +5,14 @@ on: jobs: build: + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 uses: decentraland/platform-actions/.github/workflows/apps-with-db-build.yml@main From d680d3a80d4cd6da2135f16ce3302b4c7db88e4d Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:38:15 -0300 Subject: [PATCH 08/12] update dockerfile --- Dockerfile | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index db828b6..558794a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,19 @@ ARG RUN -FROM node:lts as builderenv +FROM node:18-alpine as builderenv WORKDIR /app # some packages require a build step -RUN apt-get update && apt-get -y -qq install build-essential - -# We use Tini to handle signals and PID1 (https://github.com/krallin/tini, read why here https://github.com/krallin/tini/issues/8) -ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini - -# install dependencies -COPY package.json /app/package.json -COPY yarn.lock /app/yarn.lock -RUN yarn +RUN apk update && apk add wget # build the app COPY . /app +RUN yarn install --frozen-lockfile RUN yarn build -RUN yarn test # remove devDependencies, keep only used dependencies -RUN yarn install --frozen-lockfile --production +RUN yarn install --prod --frozen-lockfile ########################## END OF BUILD STAGE ########################## From 4a03e10aac51cd3f8c53c6c08c997e81a41a3df7 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:41:05 -0300 Subject: [PATCH 09/12] fix dockerfile --- Dockerfile | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 558794a..6bb18f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,18 +17,24 @@ RUN yarn install --prod --frozen-lockfile ########################## END OF BUILD STAGE ########################## -FROM node:lts +FROM node:18-alpine + +RUN apk update && apk add --update wget && apk add --update tini # NODE_ENV is used to configure some runtime options, like JSON logger ENV NODE_ENV production +ARG COMMIT_HASH=local +ENV COMMIT_HASH=${COMMIT_HASH:-local} + +ARG CURRENT_VERSION=Unknown +ENV CURRENT_VERSION=${CURRENT_VERSION:-Unknown} + WORKDIR /app COPY --from=builderenv /app /app -COPY --from=builderenv /tini /tini -# Please _DO NOT_ use a custom ENTRYPOINT because it may prevent signals -# (i.e. SIGTERM) to reach the service -# Read more here: https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/ -# and: https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/ + +RUN echo "" > /app/.env + ENTRYPOINT ["/tini", "--"] # Run the program under Tini CMD [ "/usr/local/bin/node", "--trace-warnings", "--abort-on-uncaught-exception", "--unhandled-rejections=strict", "dist/index.js" ] From 38a04ad4b1f800f60d56ac0ec79163be355e2854 Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 16:58:44 -0300 Subject: [PATCH 10/12] fix workflow --- .github/workflows/build.yml | 50 ++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18bf1a2..35cd1bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,39 @@ -name: build +name: build-app on: push: jobs: - build: + validations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + - name: install + run: yarn install --frozen-lockfile + - name: lint + run: yarn lint:check + + test: + runs-on: ubuntu-latest services: + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_DB: db + POSTGRES_PASSWORD: pass1234 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 redis: image: redis options: >- @@ -15,4 +43,20 @@ jobs: --health-retries 5 ports: - 6379:6379 - uses: decentraland/platform-actions/.github/workflows/apps-with-db-build.yml@main + steps: + - uses: actions/checkout@v3 + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + - name: install + run: yarn install --frozen-lockfile + - name: create .env + run: echo "" >> .env + - name: build + run: yarn build + - name: test + run: yarn test + env: + PG_COMPONENT_PSQL_CONNECTION_STRING: postgres://postgres:pass1234@localhost/db From 0bf36780fc4847e69037ae35cfa8892bff5358bc Mon Sep 17 00:00:00 2001 From: lauti7 Date: Fri, 26 Apr 2024 18:25:27 -0300 Subject: [PATCH 11/12] fix --- src/adapters/db.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index 73466c9..be8eeb7 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -193,12 +193,11 @@ export function createDBComponent(components: Pick async executeTx(cb: (client: PoolClient) => Promise): Promise { const pool = pg.getPool() const client = await pool.connect() - let res: T | undefined await client.query('BEGIN') try { - const callbackResult = await cb(client) + const res = await cb(client) await client.query('COMMIT') - res = callbackResult + return res } catch (error) { logger.error(error as any) await client.query('ROLLBACK') @@ -206,7 +205,6 @@ export function createDBComponent(components: Pick throw error } finally { client.release() - return res as T } } } From 8d1b83ec1978dcc31a08569dce9fb2ba9a1a4b5d Mon Sep 17 00:00:00 2001 From: lauti7 Date: Thu, 4 Jul 2024 15:36:19 -0300 Subject: [PATCH 12/12] one release --- src/adapters/db.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/adapters/db.ts b/src/adapters/db.ts index be8eeb7..54766fc 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -201,7 +201,6 @@ export function createDBComponent(components: Pick } catch (error) { logger.error(error as any) await client.query('ROLLBACK') - client.release() throw error } finally { client.release()