From 143f81eb18d87243070dd03e3028dcc56e8b2068 Mon Sep 17 00:00:00 2001 From: wayn3r Date: Fri, 5 Jan 2024 12:11:40 -0400 Subject: [PATCH 01/32] test(e2e): add tests to buckets and users (v1 & v2) --- infrastructure/mongo-init.js | 3 +- package.json | 3 + tests/e2e/e2e-spec.ts | 209 +++++++++++++++++++++++++++++++++++ tests/e2e/jest-e2e.json | 9 ++ tests/e2e/users.fixtures.ts | 42 +++++++ tests/e2e/utils.ts | 60 ++++++++++ yarn.lock | 112 ++++++++++++++++++- 7 files changed, 433 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/e2e-spec.ts create mode 100644 tests/e2e/jest-e2e.json create mode 100644 tests/e2e/users.fixtures.ts create mode 100644 tests/e2e/utils.ts diff --git a/infrastructure/mongo-init.js b/infrastructure/mongo-init.js index 25e26bd9b..baeeecf3e 100644 --- a/infrastructure/mongo-init.js +++ b/infrastructure/mongo-init.js @@ -3,7 +3,8 @@ db.createUser({ pwd: 'password', roles: [ { role: 'readWrite', db: '__inxt-network' }, - { role: 'readAnyDatabase', db: 'admin' } + { role: 'readAnyDatabase', db: 'admin' }, + { role: 'clusterMonitor', db: 'admin' } ], }); diff --git a/package.json b/package.json index 3e84389a8..60119a793 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "build": "tsc", "test": "jest --testPathIgnorePatterns ./tests/lib/mongo", "test:cov": "jest --coverage", + "test:e2e": "NODE_ENV=test jest --config ./tests/e2e/jest-e2e.json --setupFiles dotenv/config", "test-mongo-init": "ts-node ./tests/lib/mongo/init", "test-mongo": "jest ./tests/lib/mongo --testTimeout 30000 --runInBand", "clean": "ts-node ./bin/delete-objects", @@ -62,6 +63,7 @@ "@types/node": "^17.0.23", "@types/node-mongodb-fixtures": "^3.2.3", "@types/sinon": "^10.0.11", + "@types/supertest": "^2.0.12", "@types/uuid": "^8.3.4", "chai": "^4.2.0", "coveralls": "^2.11.6", @@ -85,6 +87,7 @@ "redis-mock": "^0.16.0", "rimraf": "^2.6.3", "sinon": "^13.0.1", + "supertest": "^6.2.4", "ts-jest": "^27.1.4", "ts-node": "^10.7.0" }, diff --git a/tests/e2e/e2e-spec.ts b/tests/e2e/e2e-spec.ts new file mode 100644 index 000000000..7c3a6c1c0 --- /dev/null +++ b/tests/e2e/e2e-spec.ts @@ -0,0 +1,209 @@ +import supertest from 'supertest' +import { checkConnection, cleanDataBase, createTestUser, getAuth, testUser } from './utils' +// Remove jest args so there is no conflict with storj-bridge +process.argv = process.argv.slice(0, 2) +const engine = require('../../bin/storj-bridge') + +import sendGridMail from '@sendgrid/mail' + +jest.mock('@sendgrid/mail', () => ({ + setApiKey: jest.fn(), + send: jest.fn((_, __, done) => typeof done === 'function' ? done() : Promise.resolve()), +})) + +const dispatchSendGridMock = jest.fn((_, __, ___, cb) => { + sendGridMail.send(null as any, null as any, cb) +}) +engine.mailer.dispatchSendGrid = dispatchSendGridMock + +checkConnection(engine.storage) + +describe('Bridge E2E Tests', () => { + + beforeEach(() => { + jest.clearAllMocks() + }) + + beforeAll(() => { + cleanDataBase(engine.storage) + }) + + afterAll(() => { + cleanDataBase(engine.storage) + }) + + describe('Buckets', () => { + + beforeAll(async () => { + await createTestUser(engine.storage) + }) + + it('should create a bucket', async () => { + + const response = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name' + }) + .expect(201); + + const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + + expect(buckets).toHaveLength(1) + + }) + + + it('should update a bucket', async () => { + + const { body: bucket } = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-1' + }) + .expect(201); + + const response = await supertest(engine.server.app) + .patch(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .send({ pubkeys: [] }) + .expect(200); + + + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + + expect(dbBucket.toObject().pubkeys).toEqual([]) + + }) + + it('should delete a bucket', async () => { + + const { body: bucket } = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-2' + }) + .expect(201); + + await supertest(engine.server.app) + .delete(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .expect(204) + + const users = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) + + expect(users).toBeNull() + + }) + }) + + describe('Users', () => { + + describe('v1', () => { + it('should create a user', async () => { + + const response = await supertest(engine.server.app) + .post('/users') + .send({ email: 'test' + testUser.email, password: testUser.hashpass }) + .expect(201); + + const users = await engine.storage.models.User.find({ _id: response.body.id }) + + expect(users).toHaveLength(1) + + // expect(dispatchSendGridMock).toHaveBeenCalled() + + }) + + it('should delete a user', async () => { + + // Create User + const { body: user } = await supertest(engine.server.app) + .post('/users') + .send({ email: 'test3' + testUser.email, password: testUser.hashpass, }) + .expect(201); + + // Request Deactivation + await supertest(engine.server.app) + .delete(`/users/${user.email}?deactivator=test-deactivator-token&redirect=/`) + .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) + .expect(200) + + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser).not.toBeNull() + + const token = dbUser.toObject().deactivator + expect(token).toBe('test-deactivator-token') + + expect(sendGridMail.send).toHaveBeenCalled() + + // Confirm Deactivation + await supertest(engine.server.app) + .get(`/deactivations/${token}`) + .expect(200) + + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).toBeNull() + }) + + }) + describe('v2', () => { + it('should create a user', async () => { + const response = await supertest(engine.server.app) + .post('/v2/users') + .send({ email: 'test_v2' + testUser.email, password: testUser.hashpass }) + // .expect(201); + .expect(200); + + const users = await engine.storage.models.User.find({ _id: response.body.id }) + + expect(users).toHaveLength(1) + }) + + it('should delete a user', async () => { + + const testEmail = 'test_v2_4' + testUser.email + // Create User + const { body: user } = await supertest(engine.server.app) + .post('/v2/users') + .send({ email: testEmail, password: testUser.hashpass, }) + // .expect(201) + .expect(200); + + + // Request Deactivation + await supertest(engine.server.app) + .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token&redirect=/`) + .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) + .expect(200) + + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser).not.toBeNull() + + const token = dbUser.toObject().deactivator + expect(token).toBe('test-deactivator-token') + + expect(sendGridMail.send).toHaveBeenCalled() + + // Confirm Deactivation + await supertest(engine.server.app) + .delete(`/v2/users/confirm-deactivate/${token}`) + .expect(200) + + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).toBeNull() + }) + + }) + }) + afterAll(() => { + process.emit('SIGINT') + }) +}) + diff --git a/tests/e2e/jest-e2e.json b/tests/e2e/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/tests/e2e/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tests/e2e/users.fixtures.ts b/tests/e2e/users.fixtures.ts new file mode 100644 index 000000000..60007a4e3 --- /dev/null +++ b/tests/e2e/users.fixtures.ts @@ -0,0 +1,42 @@ +import { User } from '../../lib/core/users/User'; +import { v4 } from 'uuid'; + +export type MongoUserModel = Required> & { + _id: string; +}; + +const formatUser = ({ _id, ...model }: MongoUserModel): User => ({ + ...model, + id: _id, +}); + +const usersTest: MongoUserModel[] = [ + { + _id: v4(), + hashpass: + '4b796e7bbe57f7092d3627d3fa7e6f645b0e50e3411e1fafd61dbf27241d836d', + subscriptionPlan: { + isSubscribed: false, + }, + referralPartner: null, + email: 'fff@ff.com', + maxSpaceBytes: 2147483648.0, + totalUsedSpaceBytes: 14596520, + preferences: { + dnt: false, + }, + isFreeTier: true, + activated: true, + resetter: null, + deactivator: null, + activator: + '424061e79e2370266726a4792519a86a56fc5137674c4b617c0ab25f4979ea50', + created: new Date('2022-05-24T14:18:04.078Z'), + uuid: '569983d6-9ec5-43ae-ad87-fb3c1307d938', + password: 'xxxxx', + migrated: false, + }, +]; + +export const users: MongoUserModel[] = usersTest; +export const userFixtures: User[] = usersTest.map(formatUser); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts new file mode 100644 index 000000000..e242e6185 --- /dev/null +++ b/tests/e2e/utils.ts @@ -0,0 +1,60 @@ +import { MongoUserModel, users } from './users.fixtures' +export const [testUser] = users + + +export const checkConnection = (storage: any) => { + if (!storage.connection.options.dbName.includes('test')) { + throw new Error("For caution test database must include test in it's name"); + } +} + +export const createTestUser = async (storage: any): Promise => { + const user:MongoUserModel = await new Promise(resolve => storage.models.User.create({ + email: testUser.email, + password: testUser.hashpass, + maxSpaceBytes: testUser.maxSpaceBytes, + uuid: testUser.uuid, + }, (err: Error, user: MongoUserModel) => { + if(err) throw err + resolve(user) + })) + + await storage.models.User.updateOne( + { + _id: user._id, + }, + { + maxSpaceBytes: testUser.maxSpaceBytes, + activated: testUser.activated, + } + ); + + return user +} + +export const deleteTestUser = async (storage: any): Promise => { + return await new Promise(resolve => storage.models.User.deleteOne({ + email: testUser.email, + }, (err: Error) => { + if(err) throw err + resolve() + })) + +} + +export const getAuth = (user: { email: string , hashpass: string }) => { + const credential = Buffer.from(`${user.email}:${user.hashpass}`).toString('base64'); + return `Basic ${credential}`; +} + + +export const cleanDataBase = (storage: any) => { + checkConnection(storage) + storage.models.User.deleteMany({}, (err: Error) => { + if(err) throw err + }) + + storage.models.Bucket.deleteMany({}, (err: Error) => { + if(err) throw err + }) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db6fcffc9..19f5b5e29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -748,6 +748,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/express-serve-static-core@^4.17.18": version "4.17.29" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" @@ -831,6 +836,11 @@ resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -917,6 +927,22 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.1.tgz#dbc620c5df3770b0c3092f947d6d5e808adae2bc" + integrity sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + +"@types/supertest@^2.0.12": + version "2.0.16" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.16.tgz#7a1294edebecb960d957bbe9b26002a2b7f21cd7" + integrity sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg== + dependencies: + "@types/superagent" "*" + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1221,6 +1247,11 @@ array.prototype.reduce@^1.0.4: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.7" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2042,6 +2073,11 @@ component-emitter@^1.2.0: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-emitter@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + component-type@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9" @@ -2125,6 +2161,11 @@ cookiejar@^2.1.0: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2287,7 +2328,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2422,6 +2463,14 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -3038,7 +3087,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-safe-stringify@^2.0.6: +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -3216,6 +3265,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -3239,6 +3297,16 @@ formidable@^1.2.0, formidable@^1.2.1: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3665,6 +3733,11 @@ helmet@^3.20.0: referrer-policy "1.2.0" x-xss-protection "1.3.0" +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hide-powered-by@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a" @@ -5407,7 +5480,7 @@ mime@1.6.0, mime@^1.3.4, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.0, mime@^2.4.3: +mime@2.6.0, mime@^2.4.0, mime@^2.4.3: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -6452,7 +6525,7 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@^6.10.3: +qs@^6.10.3, qs@^6.11.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -7042,6 +7115,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.8: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -7646,6 +7726,30 @@ superagent@^3.5.0: qs "^6.5.1" readable-stream "^2.3.5" +superagent@^8.0.5: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.2.4: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + supports-color@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" From 34a655546e5354e9c82c5de54b794ca1a39714da Mon Sep 17 00:00:00 2001 From: wayn3r Date: Fri, 5 Jan 2024 12:13:20 -0400 Subject: [PATCH 02/32] fix(users-controller): use user email instead of id At requestDestroyUser it was needed to use the email in order to work --- lib/server/http/users/controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/server/http/users/controller.ts b/lib/server/http/users/controller.ts index 1575906e5..0f7ef3631 100644 --- a/lib/server/http/users/controller.ts +++ b/lib/server/http/users/controller.ts @@ -8,7 +8,7 @@ import { UsersUsecase } from '../../../core'; -type AuthorizedRequest = Request & { user: { _id: string } }; +type AuthorizedRequest = Request & { user: { _id: string, email: string } }; export class HTTPUsersController { constructor( @@ -110,7 +110,7 @@ export class HTTPUsersController { res: Response ) { const { deactivator, redirect } = req.query; - const userId = (req as AuthorizedRequest).user._id; + const userEmail = (req as AuthorizedRequest).user.email; if (!deactivator || !redirect) { return res.status(400).send({ error: 'Missing required params' }); @@ -118,7 +118,7 @@ export class HTTPUsersController { try { const userRequestedToBeDestroyed = await this.usersUsecase.requestUserDestroy( - userId, + userEmail, deactivator, redirect ); From a45cb6b47c3a0156880c0db703df5d07ae7c8138 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Fri, 5 Jan 2024 15:38:04 -0400 Subject: [PATCH 03/32] test(e2e): check user is activated whe created --- tests/e2e/e2e-spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/e2e/e2e-spec.ts b/tests/e2e/e2e-spec.ts index 7c3a6c1c0..9c6b36c5c 100644 --- a/tests/e2e/e2e-spec.ts +++ b/tests/e2e/e2e-spec.ts @@ -117,6 +117,8 @@ describe('Bridge E2E Tests', () => { expect(users).toHaveLength(1) + expect(users[0].toObject().activated).toBe(true) + // expect(dispatchSendGridMock).toHaveBeenCalled() }) @@ -164,6 +166,8 @@ describe('Bridge E2E Tests', () => { const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) + + expect(users[0].toObject().activated).toBe(true) }) it('should delete a user', async () => { From f0b26095b73e4f1f532a298a0b49fd24d4c2b138 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Fri, 5 Jan 2024 17:00:39 -0400 Subject: [PATCH 04/32] test(e2e): move tests inside tests/lib --- package.json | 2 +- tests/{ => lib}/e2e/e2e-spec.ts | 2 +- tests/{ => lib}/e2e/jest-e2e.json | 0 tests/{ => lib}/e2e/users.fixtures.ts | 2 +- tests/{ => lib}/e2e/utils.ts | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename tests/{ => lib}/e2e/e2e-spec.ts (99%) rename tests/{ => lib}/e2e/jest-e2e.json (100%) rename tests/{ => lib}/e2e/users.fixtures.ts (95%) rename tests/{ => lib}/e2e/utils.ts (100%) diff --git a/package.json b/package.json index 60119a793..88142bd3c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "build": "tsc", "test": "jest --testPathIgnorePatterns ./tests/lib/mongo", "test:cov": "jest --coverage", - "test:e2e": "NODE_ENV=test jest --config ./tests/e2e/jest-e2e.json --setupFiles dotenv/config", + "test:e2e": "NODE_ENV=test jest --config ./tests/lib/e2e/jest-e2e.json --setupFiles dotenv/config", "test-mongo-init": "ts-node ./tests/lib/mongo/init", "test-mongo": "jest ./tests/lib/mongo --testTimeout 30000 --runInBand", "clean": "ts-node ./bin/delete-objects", diff --git a/tests/e2e/e2e-spec.ts b/tests/lib/e2e/e2e-spec.ts similarity index 99% rename from tests/e2e/e2e-spec.ts rename to tests/lib/e2e/e2e-spec.ts index 9c6b36c5c..2e059a6bf 100644 --- a/tests/e2e/e2e-spec.ts +++ b/tests/lib/e2e/e2e-spec.ts @@ -2,7 +2,7 @@ import supertest from 'supertest' import { checkConnection, cleanDataBase, createTestUser, getAuth, testUser } from './utils' // Remove jest args so there is no conflict with storj-bridge process.argv = process.argv.slice(0, 2) -const engine = require('../../bin/storj-bridge') +const engine = require('../../../bin/storj-bridge') import sendGridMail from '@sendgrid/mail' diff --git a/tests/e2e/jest-e2e.json b/tests/lib/e2e/jest-e2e.json similarity index 100% rename from tests/e2e/jest-e2e.json rename to tests/lib/e2e/jest-e2e.json diff --git a/tests/e2e/users.fixtures.ts b/tests/lib/e2e/users.fixtures.ts similarity index 95% rename from tests/e2e/users.fixtures.ts rename to tests/lib/e2e/users.fixtures.ts index 60007a4e3..feec1e360 100644 --- a/tests/e2e/users.fixtures.ts +++ b/tests/lib/e2e/users.fixtures.ts @@ -1,4 +1,4 @@ -import { User } from '../../lib/core/users/User'; +import { User } from '../../../lib/core/users/User'; import { v4 } from 'uuid'; export type MongoUserModel = Required> & { diff --git a/tests/e2e/utils.ts b/tests/lib/e2e/utils.ts similarity index 100% rename from tests/e2e/utils.ts rename to tests/lib/e2e/utils.ts From b9cc7f5353050cd57da848654cbb9cf657658f2a Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Mon, 8 Jan 2024 09:17:53 -0400 Subject: [PATCH 05/32] fix(e2e): await clean up --- tests/lib/e2e/e2e-spec.ts | 8 ++++---- tests/lib/e2e/utils.ts | 24 +++++++++++------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/lib/e2e/e2e-spec.ts b/tests/lib/e2e/e2e-spec.ts index 2e059a6bf..8639148f0 100644 --- a/tests/lib/e2e/e2e-spec.ts +++ b/tests/lib/e2e/e2e-spec.ts @@ -24,12 +24,12 @@ describe('Bridge E2E Tests', () => { jest.clearAllMocks() }) - beforeAll(() => { - cleanDataBase(engine.storage) + beforeAll(async () => { + await cleanDataBase(engine.storage) }) - afterAll(() => { - cleanDataBase(engine.storage) + afterAll(async () => { + await cleanDataBase(engine.storage) }) describe('Buckets', () => { diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index e242e6185..717d44ba5 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -8,14 +8,14 @@ export const checkConnection = (storage: any) => { } } -export const createTestUser = async (storage: any): Promise => { - const user:MongoUserModel = await new Promise(resolve => storage.models.User.create({ +export const createTestUser = async (storage: any): Promise => { + const user: MongoUserModel = await new Promise(resolve => storage.models.User.create({ email: testUser.email, password: testUser.hashpass, maxSpaceBytes: testUser.maxSpaceBytes, uuid: testUser.uuid, }, (err: Error, user: MongoUserModel) => { - if(err) throw err + if (err) throw err resolve(user) })) @@ -32,29 +32,27 @@ export const createTestUser = async (storage: any): Promise => { return user } -export const deleteTestUser = async (storage: any): Promise => { +export const deleteTestUser = async (storage: any): Promise => { return await new Promise(resolve => storage.models.User.deleteOne({ email: testUser.email, }, (err: Error) => { - if(err) throw err + if (err) throw err resolve() })) } -export const getAuth = (user: { email: string , hashpass: string }) => { +export const getAuth = (user: { email: string, hashpass: string }) => { const credential = Buffer.from(`${user.email}:${user.hashpass}`).toString('base64'); return `Basic ${credential}`; } -export const cleanDataBase = (storage: any) => { +export const cleanDataBase = async (storage: any) => { checkConnection(storage) - storage.models.User.deleteMany({}, (err: Error) => { - if(err) throw err - }) - storage.models.Bucket.deleteMany({}, (err: Error) => { - if(err) throw err - }) + await Promise.all([ + storage.models.User.deleteMany({}), + storage.models.Bucket.deleteMany({}) + ]) } \ No newline at end of file From d4ba72a20283ff793dadeafca50dd461c107eb30 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Mon, 8 Jan 2024 11:01:02 -0400 Subject: [PATCH 06/32] refactor(e2e): use AAA pattern --- tests/lib/e2e/e2e-spec.ts | 254 +++++++++++++++++++++++++++----------- 1 file changed, 179 insertions(+), 75 deletions(-) diff --git a/tests/lib/e2e/e2e-spec.ts b/tests/lib/e2e/e2e-spec.ts index 8639148f0..c82578fe8 100644 --- a/tests/lib/e2e/e2e-spec.ts +++ b/tests/lib/e2e/e2e-spec.ts @@ -1,20 +1,26 @@ import supertest from 'supertest' import { checkConnection, cleanDataBase, createTestUser, getAuth, testUser } from './utils' -// Remove jest args so there is no conflict with storj-bridge -process.argv = process.argv.slice(0, 2) -const engine = require('../../../bin/storj-bridge') +// NB: Mock external dependencies import sendGridMail from '@sendgrid/mail' +// NB: Mock SendGrid jest.mock('@sendgrid/mail', () => ({ setApiKey: jest.fn(), send: jest.fn((_, __, done) => typeof done === 'function' ? done() : Promise.resolve()), })) -const dispatchSendGridMock = jest.fn((_, __, ___, cb) => { - sendGridMail.send(null as any, null as any, cb) -}) -engine.mailer.dispatchSendGrid = dispatchSendGridMock +// NB: Mock JWT verification +jest.mock('jsonwebtoken', () => ({ verify: jest.fn((_, __, ___, cb) => cb(null, {})) })) + + +// Remove jest args so there is no conflict with storj-bridge +process.argv = process.argv.slice(0, 2) +const engine = require('../../../bin/storj-bridge') + +engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) + + checkConnection(engine.storage) @@ -32,120 +38,158 @@ describe('Bridge E2E Tests', () => { await cleanDataBase(engine.storage) }) - describe('Buckets', () => { + describe('Buckets Management', () => { + beforeAll(async () => { await createTestUser(engine.storage) }) - it('should create a bucket', async () => { + describe('Bucket creation v1', () => { - const response = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name' - }) - .expect(201); + it('should create a bucket with name and pubkeys', async () => { - const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + // Act + const response = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name' + }) + .expect(201); - expect(buckets).toHaveLength(1) + // Assert + expect(response.body).toHaveProperty('id') - }) + const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + expect(buckets).toHaveLength(1) + }) + }) - it('should update a bucket', async () => { - const { body: bucket } = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-1' - }) - .expect(201); + describe('Bucket update v1', () => { - const response = await supertest(engine.server.app) - .patch(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) - .send({ pubkeys: [] }) - .expect(200); + it('should be able to update a bucket to empty pubkeys', async () => { + // Arrange + const { body: bucket } = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-1' + }) + .expect(201); + // Act + const response = await supertest(engine.server.app) + .patch(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .send({ pubkeys: [] }) + .expect(200); - const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) - expect(dbBucket.toObject().pubkeys).toEqual([]) + // Assert + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + expect(dbBucket.toObject().pubkeys).toEqual([]) + }) }) - it('should delete a bucket', async () => { - - const { body: bucket } = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-2' - }) - .expect(201); + describe('Bucket deletion v1', () => { + it('should be able to delete a bucket', async () => { + + // Arrange: Create a bucket + const { body: bucket } = await supertest(engine.server.app) + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-2' + }) + .expect(201); - await supertest(engine.server.app) - .delete(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) - .expect(204) + // Act: Delete the bucket + await supertest(engine.server.app) + .delete(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .expect(204) - const users = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) - expect(users).toBeNull() + // Assert + const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) + expect(buckets).toBeNull() + }) }) }) - describe('Users', () => { + describe('Users Management', () => { - describe('v1', () => { - it('should create a user', async () => { + describe('User creation v1', () => { + it('should create a user with email and password', async () => { + // Act const response = await supertest(engine.server.app) .post('/users') .send({ email: 'test' + testUser.email, password: testUser.hashpass }) .expect(201); + // Assert const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) - expect(users[0].toObject().activated).toBe(true) // expect(dispatchSendGridMock).toHaveBeenCalled() }) + }) - it('should delete a user', async () => { + describe('User deletion v1', () => { + it('should be able to request a user deactivation', async () => { - // Create User + // Arrange: Create User const { body: user } = await supertest(engine.server.app) .post('/users') - .send({ email: 'test3' + testUser.email, password: testUser.hashpass, }) + .send({ email: 'request_deactivation' + testUser.email, password: testUser.hashpass, }) .expect(201); - // Request Deactivation + // Act: Request Deactivation await supertest(engine.server.app) - .delete(`/users/${user.email}?deactivator=test-deactivator-token&redirect=/`) + .delete(`/users/${user.email}?deactivator=test-deactivator-token-request-deactivation&redirect=/`) .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) .expect(200) + // Assert const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUser).not.toBeNull() const token = dbUser.toObject().deactivator - expect(token).toBe('test-deactivator-token') + expect(token).toBe('test-deactivator-token-request-deactivation') expect(sendGridMail.send).toHaveBeenCalled() - // Confirm Deactivation + + }) + + it('should be able to confirm a user deactivation', async () => { + + // Arrange: Create User + const { body: user } = await supertest(engine.server.app) + .post('/users') + .send({ email: 'confirm_deactivation' + testUser.email, password: testUser.hashpass, }) + .expect(201); + + + // Arrange: Request Deactivation + const token = 'test-deactivator-token-confirm-deactivation' + await supertest(engine.server.app) + .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) + .expect(200) + + // Assert: Confirm Deactivation await supertest(engine.server.app) .get(`/deactivations/${token}`) .expect(200) @@ -153,27 +197,31 @@ describe('Bridge E2E Tests', () => { const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUserAfterDeactivation).toBeNull() }) - }) - describe('v2', () => { - it('should create a user', async () => { + + describe('User creation v2', () => { + it('should create a user with email and password', async () => { + // Act: Create a user const response = await supertest(engine.server.app) .post('/v2/users') .send({ email: 'test_v2' + testUser.email, password: testUser.hashpass }) // .expect(201); .expect(200); + // Assert const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) expect(users[0].toObject().activated).toBe(true) }) + }) + + describe('User deletion v2', () => { - it('should delete a user', async () => { + it('should be able to request a user deactivation', async () => { - const testEmail = 'test_v2_4' + testUser.email - // Create User + // Arrange: Create User + const testEmail = 'request_deactivation_v2' + testUser.email const { body: user } = await supertest(engine.server.app) .post('/v2/users') .send({ email: testEmail, password: testUser.hashpass, }) @@ -181,30 +229,86 @@ describe('Bridge E2E Tests', () => { .expect(200); - // Request Deactivation + // Act: Request Deactivation await supertest(engine.server.app) - .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token&redirect=/`) + .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token-request-deactivation-v2&redirect=/`) .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) .expect(200) + // Assert const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUser).not.toBeNull() const token = dbUser.toObject().deactivator - expect(token).toBe('test-deactivator-token') + expect(token).toBe('test-deactivator-token-request-deactivation-v2') expect(sendGridMail.send).toHaveBeenCalled() - // Confirm Deactivation + + }) + + it('should be able to confirm a user deactivation', async () => { + + // Arrange: Create User + const testEmail = 'confirm_deactivation_v2' + testUser.email + const { body: user } = await supertest(engine.server.app) + .post('/v2/users') + .send({ email: testEmail, password: testUser.hashpass, }) + // .expect(201) + .expect(200); + + + // Arrange: Request Deactivation + const token = 'test-deactivator-token-confirm-deactivation-v2' + await supertest(engine.server.app) + .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) + .expect(200) + + + // Act: Confirm Deactivation await supertest(engine.server.app) .delete(`/v2/users/confirm-deactivate/${token}`) .expect(200) + // Assert const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUserAfterDeactivation).toBeNull() + }) }) + + describe('User update v2', () => { + + it('should be able to update a user email via gateway', async () => { + + // Arrange: Create User + const testEmail = 'update_user_email_v2' + testUser.email + const { body: user } = await supertest(engine.server.app) + .post('/v2/users') + .send({ email: testEmail, password: testUser.hashpass, }) + // .expect(201) + .expect(200); + + // Arrange: Bypass JWT verification + + + // Act: Update User Email + const newEmail = 'new_email_v2' + testUser.email + await supertest(engine.server.app) + .patch(`/v2/gateway/users/${user.id}`) + .set('Authorization', `Bearer fake-token`) + .send({ email: newEmail }) + .expect(200) + + + // Assert + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser.toObject().email).toBe(newEmail) + + }) + }) }) afterAll(() => { process.emit('SIGINT') From a3ac93f2d5834e7800e8c83f4b38e87134550b8e Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Mon, 8 Jan 2024 11:13:09 -0400 Subject: [PATCH 07/32] chore(e2e): group asserts statements --- tests/lib/e2e/e2e-spec.ts | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/lib/e2e/e2e-spec.ts b/tests/lib/e2e/e2e-spec.ts index c82578fe8..93519e32f 100644 --- a/tests/lib/e2e/e2e-spec.ts +++ b/tests/lib/e2e/e2e-spec.ts @@ -57,9 +57,9 @@ describe('Bridge E2E Tests', () => { pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], name: 'test-bucket-name' }) - .expect(201); // Assert + expect(response.status).toBe(201) expect(response.body).toHaveProperty('id') const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) @@ -87,10 +87,11 @@ describe('Bridge E2E Tests', () => { .patch(`/buckets/${bucket.id}`) .set('Authorization', getAuth(testUser)) .send({ pubkeys: [] }) - .expect(200); // Assert + expect(response.status).toBe(200); + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) expect(dbBucket.toObject().pubkeys).toEqual([]) @@ -111,13 +112,13 @@ describe('Bridge E2E Tests', () => { .expect(201); // Act: Delete the bucket - await supertest(engine.server.app) + const response = await supertest(engine.server.app) .delete(`/buckets/${bucket.id}`) .set('Authorization', getAuth(testUser)) - .expect(204) // Assert + expect(response.status).toBe(204) const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) expect(buckets).toBeNull() @@ -134,9 +135,9 @@ describe('Bridge E2E Tests', () => { const response = await supertest(engine.server.app) .post('/users') .send({ email: 'test' + testUser.email, password: testUser.hashpass }) - .expect(201); // Assert + expect(response.status).toBe(201); const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) expect(users[0].toObject().activated).toBe(true) @@ -156,12 +157,12 @@ describe('Bridge E2E Tests', () => { .expect(201); // Act: Request Deactivation - await supertest(engine.server.app) + const response = await supertest(engine.server.app) .delete(`/users/${user.email}?deactivator=test-deactivator-token-request-deactivation&redirect=/`) .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) - .expect(200) // Assert + expect(response.status).toBe(200) const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUser).not.toBeNull() @@ -189,11 +190,11 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) .expect(200) - // Assert: Confirm Deactivation - await supertest(engine.server.app) - .get(`/deactivations/${token}`) - .expect(200) + // Act: Confirm Deactivation + const response = await supertest(engine.server.app).get(`/deactivations/${token}`) + // Assert + expect(response.status).toBe(200) const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUserAfterDeactivation).toBeNull() }) @@ -205,10 +206,10 @@ describe('Bridge E2E Tests', () => { const response = await supertest(engine.server.app) .post('/v2/users') .send({ email: 'test_v2' + testUser.email, password: testUser.hashpass }) - // .expect(201); - .expect(200); // Assert + expect(response.status).toBe(200); + // expect(response.status).toBe(201); const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) @@ -230,12 +231,13 @@ describe('Bridge E2E Tests', () => { // Act: Request Deactivation - await supertest(engine.server.app) + const response = await supertest(engine.server.app) .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token-request-deactivation-v2&redirect=/`) .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) - .expect(200) // Assert + expect(response.status).toBe(200); + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUser).not.toBeNull() @@ -267,11 +269,10 @@ describe('Bridge E2E Tests', () => { // Act: Confirm Deactivation - await supertest(engine.server.app) - .delete(`/v2/users/confirm-deactivate/${token}`) - .expect(200) + const response = await supertest(engine.server.app).delete(`/v2/users/confirm-deactivate/${token}`) // Assert + expect(response.status).toBe(200); const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUserAfterDeactivation).toBeNull() @@ -291,19 +292,18 @@ describe('Bridge E2E Tests', () => { // .expect(201) .expect(200); - // Arrange: Bypass JWT verification - // Act: Update User Email const newEmail = 'new_email_v2' + testUser.email - await supertest(engine.server.app) + const response = await supertest(engine.server.app) .patch(`/v2/gateway/users/${user.id}`) .set('Authorization', `Bearer fake-token`) .send({ email: newEmail }) - .expect(200) // Assert + expect(response.status).toBe(200); + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) expect(dbUser.toObject().email).toBe(newEmail) From b766a3271b0244b54e7c26ef491e312e46927abe Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Mon, 8 Jan 2024 14:06:26 -0400 Subject: [PATCH 08/32] refactor(e2e): split tests into separate files --- package.json | 2 +- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 115 ++++++++++++++ tests/lib/e2e/global-teardown.ts | 3 + tests/lib/e2e/jest-e2e.json | 1 + tests/lib/e2e/setup.ts | 14 ++ tests/lib/e2e/users.fixtures.ts | 1 + .../{e2e-spec.ts => users/users.e2e-spec.ts} | 149 +++--------------- tests/lib/e2e/utils.ts | 17 +- 8 files changed, 166 insertions(+), 136 deletions(-) create mode 100644 tests/lib/e2e/buckets/buckets.e2e-spec.ts create mode 100644 tests/lib/e2e/global-teardown.ts create mode 100644 tests/lib/e2e/setup.ts rename tests/lib/e2e/{e2e-spec.ts => users/users.e2e-spec.ts} (59%) diff --git a/package.json b/package.json index 88142bd3c..11b9f405e 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "make-docs": "./node_modules/.bin/jsdoc index.js lib -r -R README.md -u ./doc -c .jsdoc.json --verbose -d ./jsdoc", "publish-docs": "gh-pages -d jsdoc --repo git@github.com:internxt/bridge.git", "build": "tsc", - "test": "jest --testPathIgnorePatterns ./tests/lib/mongo", + "test": "jest --testPathIgnorePatterns ./tests/lib/mongo ./tests/lib/e2e", "test:cov": "jest --coverage", "test:e2e": "NODE_ENV=test jest --config ./tests/lib/e2e/jest-e2e.json --setupFiles dotenv/config", "test-mongo-init": "ts-node ./tests/lib/mongo/init", diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts new file mode 100644 index 000000000..6f2d6c27c --- /dev/null +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -0,0 +1,115 @@ +import { createTestUser, deleteTestUser, getAuth, testUser, } from '../utils' + +import { engine, testServer } from '../setup' + + + +describe('Bridge E2E Tests', () => { + + beforeAll(async () => { + await engine.storage.models.Bucket.deleteMany({}) + }) + + afterAll(async () => { + await engine.storage.models.Bucket.deleteMany({}) + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Buckets Management', () => { + + beforeAll(async () => { + await createTestUser(engine.storage) + }) + + afterAll(async () => { + await deleteTestUser(engine.storage) + }) + + describe('Bucket creation v1', () => { + + it('should create a bucket with name and pubkeys', async () => { + + // Act + const response = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name' + }) + + // Assert + expect(response.status).toBe(201) + expect(response.body).toHaveProperty('id') + + const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + expect(buckets).toHaveLength(1) + + }) + }) + + + describe('Bucket update v1', () => { + + it('should be able to update a bucket to empty pubkeys', async () => { + // Arrange + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-1' + }) + .expect(201); + + // Act + + + const response = await testServer + .patch(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .send({ pubkeys: [] }) + + + // Assert + expect(response.status).toBe(200); + + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + expect(dbBucket.toObject().pubkeys).toEqual([]) + + }) + }) + + describe('Bucket deletion v1', () => { + it('should be able to delete a bucket', async () => { + + // Arrange: Create a bucket + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name-2' + }) + .expect(201); + + // Act: Delete the bucket + const response = await testServer + .delete(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + + + // Assert + expect(response.status).toBe(204) + const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) + expect(buckets).toBeNull() + + }) + }) + }) + +}) + diff --git a/tests/lib/e2e/global-teardown.ts b/tests/lib/e2e/global-teardown.ts new file mode 100644 index 000000000..c2c603d74 --- /dev/null +++ b/tests/lib/e2e/global-teardown.ts @@ -0,0 +1,3 @@ +export default async () => { + process.emit('SIGINT') +} diff --git a/tests/lib/e2e/jest-e2e.json b/tests/lib/e2e/jest-e2e.json index e9d912f3e..5fddc0ec6 100644 --- a/tests/lib/e2e/jest-e2e.json +++ b/tests/lib/e2e/jest-e2e.json @@ -1,6 +1,7 @@ { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", + "globalTeardown": "./global-teardown.ts", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { diff --git a/tests/lib/e2e/setup.ts b/tests/lib/e2e/setup.ts new file mode 100644 index 000000000..d7d8fa45e --- /dev/null +++ b/tests/lib/e2e/setup.ts @@ -0,0 +1,14 @@ +import supertest from 'supertest'; +import { checkConnection } from './utils'; + +if (process.env.inxtbridge_server__port !== '0') { + console.warn('Warning: inxtbridge_server__port is not set to 0, this may cause conflicts with the test server'); +} +// Remove jest options from process.argv +process.argv = process.argv.slice(0, 2); +export const engine = require('../../../bin/storj-bridge.ts'); + +checkConnection(engine.storage); + +export const testServer = supertest(engine.server.app); + diff --git a/tests/lib/e2e/users.fixtures.ts b/tests/lib/e2e/users.fixtures.ts index feec1e360..fa14f2011 100644 --- a/tests/lib/e2e/users.fixtures.ts +++ b/tests/lib/e2e/users.fixtures.ts @@ -40,3 +40,4 @@ const usersTest: MongoUserModel[] = [ export const users: MongoUserModel[] = usersTest; export const userFixtures: User[] = usersTest.map(formatUser); +export const [testUser] = users diff --git a/tests/lib/e2e/e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts similarity index 59% rename from tests/lib/e2e/e2e-spec.ts rename to tests/lib/e2e/users/users.e2e-spec.ts index 93519e32f..6702da524 100644 --- a/tests/lib/e2e/e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -1,8 +1,8 @@ -import supertest from 'supertest' -import { checkConnection, cleanDataBase, createTestUser, getAuth, testUser } from './utils' +import { getAuth, testUser, } from '../utils' -// NB: Mock external dependencies import sendGridMail from '@sendgrid/mail' +import { engine, testServer } from '../setup' + // NB: Mock SendGrid jest.mock('@sendgrid/mail', () => ({ @@ -13,117 +13,18 @@ jest.mock('@sendgrid/mail', () => ({ // NB: Mock JWT verification jest.mock('jsonwebtoken', () => ({ verify: jest.fn((_, __, ___, cb) => cb(null, {})) })) - -// Remove jest args so there is no conflict with storj-bridge -process.argv = process.argv.slice(0, 2) -const engine = require('../../../bin/storj-bridge') - -engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) - - - -checkConnection(engine.storage) - describe('Bridge E2E Tests', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - beforeAll(async () => { - await cleanDataBase(engine.storage) + await engine.storage.models.User.deleteMany({}) }) afterAll(async () => { - await cleanDataBase(engine.storage) + await engine.storage.models.User.deleteMany({}) }) - describe('Buckets Management', () => { - - - beforeAll(async () => { - await createTestUser(engine.storage) - }) - - describe('Bucket creation v1', () => { - - it('should create a bucket with name and pubkeys', async () => { - - // Act - const response = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name' - }) - - // Assert - expect(response.status).toBe(201) - expect(response.body).toHaveProperty('id') - - const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) - expect(buckets).toHaveLength(1) - - }) - }) - - - describe('Bucket update v1', () => { - - it('should be able to update a bucket to empty pubkeys', async () => { - // Arrange - const { body: bucket } = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-1' - }) - .expect(201); - - // Act - const response = await supertest(engine.server.app) - .patch(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) - .send({ pubkeys: [] }) - - - // Assert - expect(response.status).toBe(200); - - const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) - expect(dbBucket.toObject().pubkeys).toEqual([]) - - }) - }) - - describe('Bucket deletion v1', () => { - it('should be able to delete a bucket', async () => { - - // Arrange: Create a bucket - const { body: bucket } = await supertest(engine.server.app) - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-2' - }) - .expect(201); - - // Act: Delete the bucket - const response = await supertest(engine.server.app) - .delete(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) - - - // Assert - expect(response.status).toBe(204) - const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) - expect(buckets).toBeNull() - - }) - }) + beforeEach(() => { + jest.clearAllMocks() }) describe('Users Management', () => { @@ -132,7 +33,7 @@ describe('Bridge E2E Tests', () => { it('should create a user with email and password', async () => { // Act - const response = await supertest(engine.server.app) + const response = await testServer .post('/users') .send({ email: 'test' + testUser.email, password: testUser.hashpass }) @@ -140,6 +41,7 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(201); const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) + expect(users[0].toObject().activated).toBe(true) // expect(dispatchSendGridMock).toHaveBeenCalled() @@ -150,14 +52,17 @@ describe('Bridge E2E Tests', () => { describe('User deletion v1', () => { it('should be able to request a user deactivation', async () => { + // Arrange: Mock SendGrid call + engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) + // Arrange: Create User - const { body: user } = await supertest(engine.server.app) + const { body: user } = await testServer .post('/users') .send({ email: 'request_deactivation' + testUser.email, password: testUser.hashpass, }) .expect(201); // Act: Request Deactivation - const response = await supertest(engine.server.app) + const response = await testServer .delete(`/users/${user.email}?deactivator=test-deactivator-token-request-deactivation&redirect=/`) .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) @@ -177,7 +82,7 @@ describe('Bridge E2E Tests', () => { it('should be able to confirm a user deactivation', async () => { // Arrange: Create User - const { body: user } = await supertest(engine.server.app) + const { body: user } = await testServer .post('/users') .send({ email: 'confirm_deactivation' + testUser.email, password: testUser.hashpass, }) .expect(201); @@ -185,13 +90,13 @@ describe('Bridge E2E Tests', () => { // Arrange: Request Deactivation const token = 'test-deactivator-token-confirm-deactivation' - await supertest(engine.server.app) + await testServer .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) .expect(200) // Act: Confirm Deactivation - const response = await supertest(engine.server.app).get(`/deactivations/${token}`) + const response = await testServer.get(`/deactivations/${token}`) // Assert expect(response.status).toBe(200) @@ -203,7 +108,7 @@ describe('Bridge E2E Tests', () => { describe('User creation v2', () => { it('should create a user with email and password', async () => { // Act: Create a user - const response = await supertest(engine.server.app) + const response = await testServer .post('/v2/users') .send({ email: 'test_v2' + testUser.email, password: testUser.hashpass }) @@ -223,7 +128,7 @@ describe('Bridge E2E Tests', () => { // Arrange: Create User const testEmail = 'request_deactivation_v2' + testUser.email - const { body: user } = await supertest(engine.server.app) + const { body: user } = await testServer .post('/v2/users') .send({ email: testEmail, password: testUser.hashpass, }) // .expect(201) @@ -231,7 +136,7 @@ describe('Bridge E2E Tests', () => { // Act: Request Deactivation - const response = await supertest(engine.server.app) + const response = await testServer .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token-request-deactivation-v2&redirect=/`) .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) @@ -253,7 +158,7 @@ describe('Bridge E2E Tests', () => { // Arrange: Create User const testEmail = 'confirm_deactivation_v2' + testUser.email - const { body: user } = await supertest(engine.server.app) + const { body: user } = await testServer .post('/v2/users') .send({ email: testEmail, password: testUser.hashpass, }) // .expect(201) @@ -262,14 +167,14 @@ describe('Bridge E2E Tests', () => { // Arrange: Request Deactivation const token = 'test-deactivator-token-confirm-deactivation-v2' - await supertest(engine.server.app) + await testServer .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) .expect(200) // Act: Confirm Deactivation - const response = await supertest(engine.server.app).delete(`/v2/users/confirm-deactivate/${token}`) + const response = await testServer.delete(`/v2/users/confirm-deactivate/${token}`) // Assert expect(response.status).toBe(200); @@ -286,7 +191,7 @@ describe('Bridge E2E Tests', () => { // Arrange: Create User const testEmail = 'update_user_email_v2' + testUser.email - const { body: user } = await supertest(engine.server.app) + const { body: user } = await testServer .post('/v2/users') .send({ email: testEmail, password: testUser.hashpass, }) // .expect(201) @@ -295,7 +200,7 @@ describe('Bridge E2E Tests', () => { // Act: Update User Email const newEmail = 'new_email_v2' + testUser.email - const response = await supertest(engine.server.app) + const response = await testServer .patch(`/v2/gateway/users/${user.id}`) .set('Authorization', `Bearer fake-token`) .send({ email: newEmail }) @@ -310,8 +215,6 @@ describe('Bridge E2E Tests', () => { }) }) }) - afterAll(() => { - process.emit('SIGINT') - }) + }) diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index 717d44ba5..fd790cc40 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -1,7 +1,9 @@ -import { MongoUserModel, users } from './users.fixtures' -export const [testUser] = users +import { type Test, type SuperTest } from 'supertest' +import { MongoUserModel, testUser, } from './users.fixtures' +export * from './users.fixtures' + export const checkConnection = (storage: any) => { if (!storage.connection.options.dbName.includes('test')) { throw new Error("For caution test database must include test in it's name"); @@ -42,17 +44,8 @@ export const deleteTestUser = async (storage: any): Promise => { } + export const getAuth = (user: { email: string, hashpass: string }) => { const credential = Buffer.from(`${user.email}:${user.hashpass}`).toString('base64'); return `Basic ${credential}`; } - - -export const cleanDataBase = async (storage: any) => { - checkConnection(storage) - - await Promise.all([ - storage.models.User.deleteMany({}), - storage.models.Bucket.deleteMany({}) - ]) -} \ No newline at end of file From 8624d8e2c65cade2a9b62b60c78dfc69815d1913 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 11:25:35 -0400 Subject: [PATCH 09/32] test(e2e): use `chance` to generate data --- package.json | 2 + tests/lib/e2e/buckets/buckets.e2e-spec.ts | 104 +++++++++++++++++++--- tests/lib/e2e/setup.ts | 5 +- tests/lib/e2e/users.fixtures.ts | 50 +++-------- tests/lib/e2e/users/users.e2e-spec.ts | 49 +++++----- tests/lib/e2e/utils.ts | 49 ++++------ yarn.lock | 10 +++ 7 files changed, 165 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index 11b9f405e..8ffb41620 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "homepage": "https://github.com/internxt/bridge#readme", "devDependencies": { "@types/async": "^3.2.13", + "@types/chance": "^1.1.6", "@types/express": "^4.17.13", "@types/jest": "^27.4.1", "@types/jsonwebtoken": "^8.5.8", @@ -66,6 +67,7 @@ "@types/supertest": "^2.0.12", "@types/uuid": "^8.3.4", "chai": "^4.2.0", + "chance": "^1.1.11", "coveralls": "^2.11.6", "dotenv": "^16.0.0", "eslint": "^7.10.0", diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index 6f2d6c27c..d4a1a273d 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -1,17 +1,23 @@ -import { createTestUser, deleteTestUser, getAuth, testUser, } from '../utils' - +// import crypto from 'crypto'; +import { createTestUser, deleteTestUser, getAuth, } from '../utils' import { engine, testServer } from '../setup' +// import axios, { type AxiosStatic } from 'axios' +import { type User } from '../users.fixtures'; +jest.mock('axios', () => ({ get: jest.fn() })) describe('Bridge E2E Tests', () => { + let testUser: User beforeAll(async () => { await engine.storage.models.Bucket.deleteMany({}) + testUser = await createTestUser() }) afterAll(async () => { await engine.storage.models.Bucket.deleteMany({}) + await deleteTestUser() }) beforeEach(() => { @@ -20,14 +26,6 @@ describe('Bridge E2E Tests', () => { describe('Buckets Management', () => { - beforeAll(async () => { - await createTestUser(engine.storage) - }) - - afterAll(async () => { - await deleteTestUser(engine.storage) - }) - describe('Bucket creation v1', () => { it('should create a bucket with name and pubkeys', async () => { @@ -51,7 +49,6 @@ describe('Bridge E2E Tests', () => { }) }) - describe('Bucket update v1', () => { it('should be able to update a bucket to empty pubkeys', async () => { @@ -111,5 +108,90 @@ describe('Bridge E2E Tests', () => { }) }) + // describe('File Management', () => { + + // describe('File upload v1', () => { + + // it('Uploads and finishes correctly', async () => { + + // const nodeID = engine._config.application.CLUSTER['0'] + + // const get = axios.get as jest.MockWithArgs + + // get.mockResolvedValue(Promise.resolve({ data: { result: 'http://fake-url' } } as any)) + + // await new Promise(resolve => engine.storage.models.Contact.record({ + // nodeID, + // protocol: "1.2.0-INXT", + // address: "72.132.43.2", // this ip address is an example + // port: 43758, + // lastSeen: new Date(), + // }, resolve)) + + // const { body: { id: bucketId } } = await testServer + // .post('/buckets') + // .set('Authorization', getAuth(testUser)) + + + // const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + // .set('Authorization', getAuth(testUser)) + // .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + + + // console.log({ body: { ...response.body } }); + + // const { uploads } = response.body; + + // for (const upload of uploads) { + // const { url, urls, index, uuid } = upload; + // expect(url).toBeDefined(); + // expect(url).toContain('http'); + // expect(url).toBe('http://fake-url') + // expect(urls).toBeNull(); + // expect(uuid).toBeDefined(); + // const file = crypto.randomBytes(50).toString('hex'); + // // await axios.put(url, file, { headers: { 'Content-Type': 'application/octet-stream', }, }); + // } + + // const index = crypto.randomBytes(32).toString('hex'); + // const responseComplete = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + // .set('Authorization', getAuth(testUser)) + // .send({ + // index, + // shards: [ + // { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, + // { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, + // ], + // }); + + // expect(responseComplete.status).toBe(200); + + // const { + // bucket, + // created, + // filename, + // id, + // index: indexResponse, + // mimetype, + // renewal, + // size, + // version, + // } = responseComplete.body; + + // expect(bucket).toEqual(bucketId); + // expect(created).toBeDefined(); + // expect(filename).toBeDefined(); + // expect(id).toBeDefined(); + // expect(indexResponse).toEqual(index); + // expect(mimetype).toBeDefined(); + // expect(renewal).toBeDefined(); + // expect(size).toBeGreaterThan(0); + // expect(typeof size).toBe('number'); + // expect(version).toBe(2); + // }); + // }) + + // }) + }) diff --git a/tests/lib/e2e/setup.ts b/tests/lib/e2e/setup.ts index d7d8fa45e..5f93a3cec 100644 --- a/tests/lib/e2e/setup.ts +++ b/tests/lib/e2e/setup.ts @@ -1,5 +1,4 @@ import supertest from 'supertest'; -import { checkConnection } from './utils'; if (process.env.inxtbridge_server__port !== '0') { console.warn('Warning: inxtbridge_server__port is not set to 0, this may cause conflicts with the test server'); @@ -8,7 +7,9 @@ if (process.env.inxtbridge_server__port !== '0') { process.argv = process.argv.slice(0, 2); export const engine = require('../../../bin/storj-bridge.ts'); -checkConnection(engine.storage); +if (!engine.storage.connection.options.dbName.includes('test')) { + throw new Error("For caution test database must include test in it's name"); +} export const testServer = supertest(engine.server.app); diff --git a/tests/lib/e2e/users.fixtures.ts b/tests/lib/e2e/users.fixtures.ts index fa14f2011..e78c362d5 100644 --- a/tests/lib/e2e/users.fixtures.ts +++ b/tests/lib/e2e/users.fixtures.ts @@ -1,43 +1,15 @@ -import { User } from '../../../lib/core/users/User'; -import { v4 } from 'uuid'; +import { Chance } from 'chance' -export type MongoUserModel = Required> & { - _id: string; -}; +export type { User } from '../../../lib/core/users/User'; -const formatUser = ({ _id, ...model }: MongoUserModel): User => ({ - ...model, - id: _id, -}); +export type TestUser = { email: string, password: string, maxSpaceBytes: number } + +export const dataGenerator = new Chance() + +export const testUser: TestUser = { + password: dataGenerator.hash({ length: 64 }), + email: dataGenerator.email(), + maxSpaceBytes: 2147483648 +} -const usersTest: MongoUserModel[] = [ - { - _id: v4(), - hashpass: - '4b796e7bbe57f7092d3627d3fa7e6f645b0e50e3411e1fafd61dbf27241d836d', - subscriptionPlan: { - isSubscribed: false, - }, - referralPartner: null, - email: 'fff@ff.com', - maxSpaceBytes: 2147483648.0, - totalUsedSpaceBytes: 14596520, - preferences: { - dnt: false, - }, - isFreeTier: true, - activated: true, - resetter: null, - deactivator: null, - activator: - '424061e79e2370266726a4792519a86a56fc5137674c4b617c0ab25f4979ea50', - created: new Date('2022-05-24T14:18:04.078Z'), - uuid: '569983d6-9ec5-43ae-ad87-fb3c1307d938', - password: 'xxxxx', - migrated: false, - }, -]; -export const users: MongoUserModel[] = usersTest; -export const userFixtures: User[] = usersTest.map(formatUser); -export const [testUser] = users diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 6702da524..199c45401 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -1,7 +1,8 @@ -import { getAuth, testUser, } from '../utils' +import { getAuth, } from '../utils' import sendGridMail from '@sendgrid/mail' import { engine, testServer } from '../setup' +import { dataGenerator } from '../users.fixtures' // NB: Mock SendGrid @@ -35,7 +36,7 @@ describe('Bridge E2E Tests', () => { // Act const response = await testServer .post('/users') - .send({ email: 'test' + testUser.email, password: testUser.hashpass }) + .send({ email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }) }) // Assert expect(response.status).toBe(201); @@ -56,15 +57,17 @@ describe('Bridge E2E Tests', () => { engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } const { body: user } = await testServer .post('/users') - .send({ email: 'request_deactivation' + testUser.email, password: testUser.hashpass, }) + .send(payload) .expect(201); // Act: Request Deactivation + const deactivatorHash = dataGenerator.hash() const response = await testServer - .delete(`/users/${user.email}?deactivator=test-deactivator-token-request-deactivation&redirect=/`) - .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) + .delete(`/users/${user.email}?deactivator=${deactivatorHash}&redirect=/`) + .set('Authorization', getAuth(payload)) // Assert expect(response.status).toBe(200) @@ -72,7 +75,7 @@ describe('Bridge E2E Tests', () => { expect(dbUser).not.toBeNull() const token = dbUser.toObject().deactivator - expect(token).toBe('test-deactivator-token-request-deactivation') + expect(token).toBe(deactivatorHash) expect(sendGridMail.send).toHaveBeenCalled() @@ -82,17 +85,18 @@ describe('Bridge E2E Tests', () => { it('should be able to confirm a user deactivation', async () => { // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } const { body: user } = await testServer .post('/users') - .send({ email: 'confirm_deactivation' + testUser.email, password: testUser.hashpass, }) + .send(payload) .expect(201); // Arrange: Request Deactivation - const token = 'test-deactivator-token-confirm-deactivation' + const token = dataGenerator.hash() await testServer .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) - .set('Authorization', getAuth({ email: user.email, hashpass: testUser.hashpass })) + .set('Authorization', getAuth(payload)) .expect(200) // Act: Confirm Deactivation @@ -110,7 +114,7 @@ describe('Bridge E2E Tests', () => { // Act: Create a user const response = await testServer .post('/v2/users') - .send({ email: 'test_v2' + testUser.email, password: testUser.hashpass }) + .send({ email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }) }) // Assert expect(response.status).toBe(200); @@ -127,18 +131,19 @@ describe('Bridge E2E Tests', () => { it('should be able to request a user deactivation', async () => { // Arrange: Create User - const testEmail = 'request_deactivation_v2' + testUser.email + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } const { body: user } = await testServer .post('/v2/users') - .send({ email: testEmail, password: testUser.hashpass, }) + .send(payload) // .expect(201) .expect(200); // Act: Request Deactivation + const deactivatorHash = dataGenerator.hash() const response = await testServer - .delete(`/v2/users/request-deactivate?deactivator=test-deactivator-token-request-deactivation-v2&redirect=/`) - .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) + .delete(`/v2/users/request-deactivate?deactivator=${deactivatorHash}&redirect=/`) + .set('Authorization', getAuth(payload)) // Assert expect(response.status).toBe(200); @@ -147,7 +152,7 @@ describe('Bridge E2E Tests', () => { expect(dbUser).not.toBeNull() const token = dbUser.toObject().deactivator - expect(token).toBe('test-deactivator-token-request-deactivation-v2') + expect(token).toBe(deactivatorHash) expect(sendGridMail.send).toHaveBeenCalled() @@ -157,19 +162,19 @@ describe('Bridge E2E Tests', () => { it('should be able to confirm a user deactivation', async () => { // Arrange: Create User - const testEmail = 'confirm_deactivation_v2' + testUser.email + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } const { body: user } = await testServer .post('/v2/users') - .send({ email: testEmail, password: testUser.hashpass, }) + .send(payload) // .expect(201) .expect(200); // Arrange: Request Deactivation - const token = 'test-deactivator-token-confirm-deactivation-v2' + const token = dataGenerator.hash() await testServer .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) - .set('Authorization', getAuth({ email: testEmail, hashpass: testUser.hashpass })) + .set('Authorization', getAuth(payload)) .expect(200) @@ -190,16 +195,16 @@ describe('Bridge E2E Tests', () => { it('should be able to update a user email via gateway', async () => { // Arrange: Create User - const testEmail = 'update_user_email_v2' + testUser.email + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } const { body: user } = await testServer .post('/v2/users') - .send({ email: testEmail, password: testUser.hashpass, }) + .send(payload) // .expect(201) .expect(200); // Act: Update User Email - const newEmail = 'new_email_v2' + testUser.email + const newEmail = dataGenerator.email() const response = await testServer .patch(`/v2/gateway/users/${user.id}`) .set('Authorization', `Bearer fake-token`) diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index fd790cc40..6e918b7e3 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -1,42 +1,31 @@ -import { type Test, type SuperTest } from 'supertest' -import { MongoUserModel, testUser, } from './users.fixtures' +import { engine } from './setup'; +import { TestUser, testUser, User } from './users.fixtures' +type Args = { storage: any, user: TestUser } -export * from './users.fixtures' +export const createTestUser = async (args: Args = { storage: engine.storage, user: testUser }): Promise => { + const { storage, user } = args -export const checkConnection = (storage: any) => { - if (!storage.connection.options.dbName.includes('test')) { - throw new Error("For caution test database must include test in it's name"); - } -} - -export const createTestUser = async (storage: any): Promise => { - const user: MongoUserModel = await new Promise(resolve => storage.models.User.create({ - email: testUser.email, - password: testUser.hashpass, - maxSpaceBytes: testUser.maxSpaceBytes, - uuid: testUser.uuid, - }, (err: Error, user: MongoUserModel) => { + const payload = { email: user.email, password: user.password } + const createdUser: User = await new Promise(resolve => storage.models.User.create(payload, (err: Error, user: any) => { if (err) throw err - resolve(user) + resolve(user.toObject()) })) await storage.models.User.updateOne( - { - _id: user._id, - }, - { - maxSpaceBytes: testUser.maxSpaceBytes, - activated: testUser.activated, - } + { _id: createdUser.uuid, }, + { maxSpaceBytes: user.maxSpaceBytes, activated: true, } ); - return user + createdUser.password = user.password + + return createdUser } -export const deleteTestUser = async (storage: any): Promise => { +export const deleteTestUser = async (args: Args = { storage: engine.storage, user: testUser }): Promise => { + const { storage, user } = args return await new Promise(resolve => storage.models.User.deleteOne({ - email: testUser.email, + email: user.email, }, (err: Error) => { if (err) throw err resolve() @@ -44,8 +33,8 @@ export const deleteTestUser = async (storage: any): Promise => { } - -export const getAuth = (user: { email: string, hashpass: string }) => { - const credential = Buffer.from(`${user.email}:${user.hashpass}`).toString('base64'); +export const getAuth = (user: Omit = testUser) => { + const credential = Buffer.from(`${user.email}:${user.password}`).toString('base64'); return `Basic ${credential}`; } + diff --git a/yarn.lock b/yarn.lock index 19f5b5e29..404609fa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -741,6 +741,11 @@ dependencies: "@types/node" "*" +"@types/chance@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.6.tgz#2fe3de58742629602c3fbab468093b27207f04ad" + integrity sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ== + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -1875,6 +1880,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chance@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.11.tgz#78e10e1f9220a5bbc60a83e3f28a5d8558d84d1b" + integrity sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" From e863441fb45810dbd883db4264066e138803a2f4 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 11:39:04 -0400 Subject: [PATCH 10/32] fix(users): use `_id` to destroy an user --- lib/core/users/usecase.ts | 6 +++--- lib/server/http/users/controller.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/core/users/usecase.ts b/lib/core/users/usecase.ts index 676d1d373..6859b6fdf 100644 --- a/lib/core/users/usecase.ts +++ b/lib/core/users/usecase.ts @@ -218,11 +218,11 @@ export class UsersUsecase { return user; } - async requestUserDestroy(userEmail: string, deactivator: string, redirect: string) { - const user = await this.usersRepository.findByEmail(userEmail); + async requestUserDestroy(userId: string, deactivator: string, redirect: string) { + const user = await this.usersRepository.findById(userId); if (!user) { - throw new UserNotFoundError(userEmail); + throw new UserNotFoundError(userId); } await this.usersRepository.updateById(user.id, { deactivator }); diff --git a/lib/server/http/users/controller.ts b/lib/server/http/users/controller.ts index 0f7ef3631..eb050b5c6 100644 --- a/lib/server/http/users/controller.ts +++ b/lib/server/http/users/controller.ts @@ -110,7 +110,7 @@ export class HTTPUsersController { res: Response ) { const { deactivator, redirect } = req.query; - const userEmail = (req as AuthorizedRequest).user.email; + const userId = (req as AuthorizedRequest).user._id; if (!deactivator || !redirect) { return res.status(400).send({ error: 'Missing required params' }); @@ -118,7 +118,7 @@ export class HTTPUsersController { try { const userRequestedToBeDestroyed = await this.usersUsecase.requestUserDestroy( - userEmail, + userId, deactivator, redirect ); From 5d13a9396fffa5546e46430ffadb5a0eb496ad35 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 12:12:52 -0400 Subject: [PATCH 11/32] test(users): update requestUserDestroy tests --- tests/lib/core/users/usecase.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/lib/core/users/usecase.test.ts b/tests/lib/core/users/usecase.test.ts index 3935b1847..64e6f9e7d 100644 --- a/tests/lib/core/users/usecase.test.ts +++ b/tests/lib/core/users/usecase.test.ts @@ -345,14 +345,14 @@ describe('Users usecases', () => { it('When requesting a user destroy, it should work if the user exists', async () => { const user = fixtures.getUser(); const redirect = 'redirect'; - const findUser = stub(usersRepository, 'findByEmail').resolves(user); + const findUser = stub(usersRepository, 'findById').resolves(user); const updateById = jest.spyOn(usersRepository, 'updateById').mockImplementation(); const eventBusEmitterSpy = jest.spyOn(eventBus, 'emit').mockImplementation(); - await usecase.requestUserDestroy(user.email, user.deactivator as string, redirect); + await usecase.requestUserDestroy(user.id, user.deactivator as string, redirect); expect(findUser.calledOnce).toBeTruthy(); - expect(findUser.firstCall.args).toStrictEqual([user.email]); + expect(findUser.firstCall.args).toStrictEqual([user.id]); expect(updateById).toBeCalledTimes(1); expect(updateById).toBeCalledWith(user.id, { deactivator: user.deactivator }); expect(eventBusEmitterSpy).toBeCalledTimes(1); @@ -368,15 +368,15 @@ describe('Users usecases', () => { it('When requesting a user destroy, it should fail if the user does not exist', async () => { const user = fixtures.getUser(); const redirect = 'redirect'; - const findUser = stub(usersRepository, 'findByEmail').resolves(null); + const findUser = stub(usersRepository, 'findById').resolves(null); try { - await usecase.requestUserDestroy(user.email, user.deactivator as string, redirect); + await usecase.requestUserDestroy(user.id, user.deactivator as string, redirect); expect(true).toBeFalsy(); } catch (err) { expect(err).toBeInstanceOf(UserNotFoundError); expect(findUser.calledOnce).toBeTruthy(); - expect(findUser.firstCall.args).toStrictEqual([user.email]); + expect(findUser.firstCall.args).toStrictEqual([user.id]); } }); }); From 72ca9305089907d81e2320dfcea5b51d4799fa5a Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 13:31:15 -0400 Subject: [PATCH 12/32] test(e2e): better naming for tests --- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 197 ++++++++--- tests/lib/e2e/users/users.e2e-spec.ts | 403 +++++++++++++++------- tests/lib/e2e/utils.ts | 10 +- 3 files changed, 416 insertions(+), 194 deletions(-) diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index d4a1a273d..c647867e1 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -1,3 +1,4 @@ +import { dataGenerator } from './../users.fixtures' // import crypto from 'crypto'; import { createTestUser, deleteTestUser, getAuth, } from '../utils' import { engine, testServer } from '../setup' @@ -26,84 +27,166 @@ describe('Bridge E2E Tests', () => { describe('Buckets Management', () => { - describe('Bucket creation v1', () => { + describe('Bucket Management v1', () => { - it('should create a bucket with name and pubkeys', async () => { + describe('Creating a bucket', () => { - // Act - const response = await testServer - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name' - }) + it('When you want create the root bucket, it should work without any arguments', async () => { - // Assert - expect(response.status).toBe(201) - expect(response.body).toHaveProperty('id') + // Act + const response = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) - const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) - expect(buckets).toHaveLength(1) + // Assert + expect(response.status).toBe(201) + expect(response.body).toHaveProperty('id') - }) - }) + const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + expect(buckets).toHaveLength(1) - describe('Bucket update v1', () => { + }) + it('When you want create a bucket with name and pubkeys, it should work with correctly formatted pubkeys', async () => { - it('should be able to update a bucket to empty pubkeys', async () => { - // Arrange - const { body: bucket } = await testServer - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-1' - }) - .expect(201); + // Act + const response = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: 'test-bucket-name' + }) - // Act + // Assert + expect(response.status).toBe(201) + expect(response.body).toHaveProperty('id') + const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) + expect(buckets).toHaveLength(1) - const response = await testServer - .patch(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) - .send({ pubkeys: [] }) + }) + it('When you want create a bucket with name and pubkeys, it should fail with incorrectly formatted pubkeys', async () => { + // Act + const response = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['invalid-pubkey'], + name: 'test-bucket-name' + }) - // Assert - expect(response.status).toBe(200); + // Assert + expect(response.status).toBe(400) - const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) - expect(dbBucket.toObject().pubkeys).toEqual([]) + }) + }) + describe('Updating a bucket', () => { + + it('Whe you want to update a bucket, it should work with empty pubkeys list', async () => { + // Arrange + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: dataGenerator.word({ length: 7 }) + }) + .expect(201); + + // Act + const response = await testServer + .patch(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .send({ pubkeys: [] }) + + + // Assert + expect(response.status).toBe(200); + + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + expect(dbBucket.toObject().pubkeys).toEqual([]) + + }) + it('Whe you want to update a bucket, it should fail with invalid pubkeys list', async () => { + // Arrange + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: dataGenerator.word({ length: 7 }) + }) + .expect(201); + + // Act + const response = await testServer + .patch(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + .send({ pubkeys: ['invalid-pubkey'] }) + + + // Assert + expect(response.status).toBe(400); + + const dbBucket = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) + expect(dbBucket.toObject().pubkeys).toEqual(['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9']) + + }) }) - }) - describe('Bucket deletion v1', () => { - it('should be able to delete a bucket', async () => { + describe('Deleting a bucket', () => { + it('When you want to delete a bucket it should work if is the owner', async () => { + + // Arrange: Create a bucket + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: dataGenerator.word({ length: 7 }) + }) + .expect(201); + + // Act: Delete the bucket + const response = await testServer + .delete(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) + + // Assert + expect(response.status).toBe(204) + const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) + expect(buckets).toBeNull() + + }) + + it('When you want to delete a bucket it should fail if is not the owner', async () => { + + // Arrange: Create a bucket + const owner = await createTestUser({ user: { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), maxSpaceBytes: 321312313 } }) + + const { body: bucket } = await testServer + .post('/buckets') + .set('Authorization', getAuth(owner)) + .send({ + pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], + name: dataGenerator.word({ length: 7 }) + }) + .expect(201); - // Arrange: Create a bucket - const { body: bucket } = await testServer - .post('/buckets') - .set('Authorization', getAuth(testUser)) - .send({ - pubkeys: ['031a259ee122414f57a63bbd6887ee17960e9106b0adcf89a298cdad2108adf4d9'], - name: 'test-bucket-name-2' - }) - .expect(201); + // Act: Delete the bucket + const response = await testServer + .delete(`/buckets/${bucket.id}`) + .set('Authorization', getAuth(testUser)) - // Act: Delete the bucket - const response = await testServer - .delete(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) + // Assert + expect(response.status).toBe(404) - // Assert - expect(response.status).toBe(204) - const buckets = await engine.storage.models.Bucket.findOne({ _id: bucket.id }) - expect(buckets).toBeNull() + await deleteTestUser({ user: owner }) + }) }) }) }) diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 199c45401..1e941a578 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -30,195 +30,334 @@ describe('Bridge E2E Tests', () => { describe('Users Management', () => { - describe('User creation v1', () => { - it('should create a user with email and password', async () => { - // Act - const response = await testServer - .post('/users') - .send({ email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }) }) + describe('User Management v1', () => { - // Assert - expect(response.status).toBe(201); - const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) + describe('Creating a new user', () => { - expect(users[0].toObject().activated).toBe(true) + it('When creating a user, it should work if email is not in use', async () => { - // expect(dispatchSendGridMock).toHaveBeenCalled() + // Arrange + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - }) - }) + // Act + const response = await testServer + .post('/users') + .send(payload) - describe('User deletion v1', () => { - it('should be able to request a user deactivation', async () => { + // Assert + expect(response.status).toBe(201); + const users = await engine.storage.models.User.find({ _id: response.body.id }) + expect(users).toHaveLength(1) - // Arrange: Mock SendGrid call - engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) + expect(users[0].toObject().activated).toBe(true) - // Arrange: Create User - const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - const { body: user } = await testServer - .post('/users') - .send(payload) - .expect(201); + // expect(dispatchSendGridMock).toHaveBeenCalled() - // Act: Request Deactivation - const deactivatorHash = dataGenerator.hash() - const response = await testServer - .delete(`/users/${user.email}?deactivator=${deactivatorHash}&redirect=/`) - .set('Authorization', getAuth(payload)) + }) + it('When creating a user, it should fail if email is in use', async () => { - // Assert - expect(response.status).toBe(200) - const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) - expect(dbUser).not.toBeNull() + // Arrange + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + await testServer + .post('/users') + .send(payload) + .expect(201); - const token = dbUser.toObject().deactivator - expect(token).toBe(deactivatorHash) + // Act + const response = await testServer + .post('/users') + .send(payload) - expect(sendGridMail.send).toHaveBeenCalled() + // Assert + expect(response.status).toBe(400); + }) }) - it('should be able to confirm a user deactivation', async () => { + describe('Deleting an existing user', () => { - // Arrange: Create User - const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - const { body: user } = await testServer - .post('/users') - .send(payload) - .expect(201); + it('When requesting user deactivation, it should work for authorized user email', async () => { + // Arrange: Mock SendGrid call + engine.mailer.dispatchSendGrid = jest.fn((_, __, ___, cb) => { sendGridMail.send(null as any, null as any, cb) }) + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/users') + .send(payload) + .expect(201); - // Arrange: Request Deactivation - const token = dataGenerator.hash() - await testServer - .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) - .set('Authorization', getAuth(payload)) - .expect(200) + // Act: Request Deactivation + const deactivatorHash = dataGenerator.hash() + const response = await testServer + .delete(`/users/${user.email}?deactivator=${deactivatorHash}&redirect=/`) + .set('Authorization', getAuth(payload)) - // Act: Confirm Deactivation - const response = await testServer.get(`/deactivations/${token}`) + // Assert + expect(response.status).toBe(200) + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser).not.toBeNull() - // Assert - expect(response.status).toBe(200) - const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) - expect(dbUserAfterDeactivation).toBeNull() - }) - }) + const token = dbUser.toObject().deactivator + expect(token).toBe(deactivatorHash) + + expect(sendGridMail.send).toHaveBeenCalled() + + + }) + + it('When requesting user deactivation, it should fail for an email different than authorized user email', async () => { + + // Arrange: Create User + const payload1 = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/users') + .send(payload1) + .expect(201); + + const payload2 = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + await testServer + .post('/users') + .send(payload2) + .expect(201); + + // Act: Request Deactivation + const deactivatorHash = dataGenerator.hash() + const response = await testServer + .delete(`/users/${payload2.email}?deactivator=${deactivatorHash}&redirect=/`) + .set('Authorization', getAuth(payload1)) + + // Assert + expect(response.status).toBe(401) + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser).not.toBeNull() + + const token = dbUser.toObject().deactivator + expect(token).toBeNull() + + }) + + it('When confirming user deactivation, it should work with the correct deactivator', async () => { + + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/users') + .send(payload) + .expect(201); + + + // Arrange: Request Deactivation + const token = dataGenerator.hash() + await testServer + .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth(payload)) + .expect(200) + + // Act: Confirm Deactivation + const response = await testServer.get(`/deactivations/${token}`) - describe('User creation v2', () => { - it('should create a user with email and password', async () => { - // Act: Create a user - const response = await testServer - .post('/v2/users') - .send({ email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }) }) + // Assert + expect(response.status).toBe(200) + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).toBeNull() + }) - // Assert - expect(response.status).toBe(200); - // expect(response.status).toBe(201); - const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) + it('When confirming user deactivation, it should fail with an incorrect deactivator', async () => { - expect(users[0].toObject().activated).toBe(true) + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/users') + .send(payload) + .expect(201); + + + // Arrange: Request Deactivation + const token = dataGenerator.hash() + await testServer + .delete(`/users/${user.email}?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth(payload)) + .expect(200) + + // Act: Confirm Deactivation + const response = await testServer.get(`/deactivations/${dataGenerator.hash()}`) + + // Assert + expect(response.status).toBe(404) + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).not.toBeNull() + }) }) }) - describe('User deletion v2', () => { - it('should be able to request a user deactivation', async () => { + describe('User Management v2', () => { + + describe('Creating a new user', () => { + it('When creating a user, it should work if email is not in use', async () => { + + // Arrange + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + // Act: Create a user + const response = await testServer + .post('/v2/users') + .send(payload) + + // Assert + expect(response.status).toBe(200); + // expect(response.status).toBe(201); + const users = await engine.storage.models.User.find({ _id: response.body.id }) + expect(users).toHaveLength(1) + + expect(users[0].toObject().activated).toBe(true) + }) + it('When creating a user, it should fail if email is in use', async () => { + + // Arrange + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + await testServer + .post('/v2/users') + .send(payload) + .expect(200); + + // Act: Create a user + const response = await testServer + .post('/v2/users') + .send(payload) + + // Assert + expect(response.status).toBe(400); + + }) + }) - // Arrange: Create User - const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - const { body: user } = await testServer - .post('/v2/users') - .send(payload) - // .expect(201) - .expect(200); + describe('Deleting an existing user', () => { + it('When requesting user deactivation, it should work for authorized user', async () => { - // Act: Request Deactivation - const deactivatorHash = dataGenerator.hash() - const response = await testServer - .delete(`/v2/users/request-deactivate?deactivator=${deactivatorHash}&redirect=/`) - .set('Authorization', getAuth(payload)) + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/v2/users') + .send(payload) + // .expect(201) + .expect(200); - // Assert - expect(response.status).toBe(200); - const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) - expect(dbUser).not.toBeNull() + // Act: Request Deactivation + const deactivatorHash = dataGenerator.hash() + const response = await testServer + .delete(`/v2/users/request-deactivate?deactivator=${deactivatorHash}&redirect=/`) + .set('Authorization', getAuth(payload)) - const token = dbUser.toObject().deactivator - expect(token).toBe(deactivatorHash) + // Assert + expect(response.status).toBe(200); - expect(sendGridMail.send).toHaveBeenCalled() + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser).not.toBeNull() + const token = dbUser.toObject().deactivator + expect(token).toBe(deactivatorHash) - }) + expect(sendGridMail.send).toHaveBeenCalled() - it('should be able to confirm a user deactivation', async () => { + }) - // Arrange: Create User - const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - const { body: user } = await testServer - .post('/v2/users') - .send(payload) - // .expect(201) - .expect(200); + it('When confirming user deactivation, it should work with the correct deactivator', async () => { + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/v2/users') + .send(payload) + // .expect(201) + .expect(200); - // Arrange: Request Deactivation - const token = dataGenerator.hash() - await testServer - .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) - .set('Authorization', getAuth(payload)) - .expect(200) + // Arrange: Request Deactivation + const token = dataGenerator.hash() + await testServer + .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth(payload)) + .expect(200) - // Act: Confirm Deactivation - const response = await testServer.delete(`/v2/users/confirm-deactivate/${token}`) - // Assert - expect(response.status).toBe(200); - const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) - expect(dbUserAfterDeactivation).toBeNull() + // Act: Confirm Deactivation + const response = await testServer.delete(`/v2/users/confirm-deactivate/${token}`) - }) + // Assert + expect(response.status).toBe(200); + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).toBeNull() - }) + }) + it('When confirming user deactivation, it should fail with an incorrect deactivator', async () => { + + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/v2/users') + .send(payload) + // .expect(201) + .expect(200); + + + // Arrange: Request Deactivation + const token = dataGenerator.hash() + await testServer + .delete(`/v2/users/request-deactivate?deactivator=${token}&redirect=/`) + .set('Authorization', getAuth(payload)) + .expect(200) - describe('User update v2', () => { - it('should be able to update a user email via gateway', async () => { + // Act: Confirm Deactivation + const response = await testServer.delete(`/v2/users/confirm-deactivate/${dataGenerator.hash()}`) - // Arrange: Create User - const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } - const { body: user } = await testServer - .post('/v2/users') - .send(payload) - // .expect(201) - .expect(200); + // Assert + expect(response.status).toBe(404); + const dbUserAfterDeactivation = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUserAfterDeactivation).not.toBeNull() + }) + + }) + + describe('User update v2', () => { + + it('should be able to update a user email via gateway', async () => { + + // Arrange: Create User + const payload = { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), } + const { body: user } = await testServer + .post('/v2/users') + .send(payload) + // .expect(201) + .expect(200); - // Act: Update User Email - const newEmail = dataGenerator.email() - const response = await testServer - .patch(`/v2/gateway/users/${user.id}`) - .set('Authorization', `Bearer fake-token`) - .send({ email: newEmail }) + // Act: Update User Email + const newEmail = dataGenerator.email() + const response = await testServer + .patch(`/v2/gateway/users/${user.id}`) + .set('Authorization', `Bearer fake-token`) + .send({ email: newEmail }) - // Assert - expect(response.status).toBe(200); - const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) - expect(dbUser.toObject().email).toBe(newEmail) + // Assert + expect(response.status).toBe(200); + const dbUser = await engine.storage.models.User.findOne({ _id: user.id }) + expect(dbUser.toObject().email).toBe(newEmail) + + }) }) + }) + + }) }) diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index 6e918b7e3..e9451accd 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -1,10 +1,10 @@ import { engine } from './setup'; import { TestUser, testUser, User } from './users.fixtures' -type Args = { storage: any, user: TestUser } +type Args = { storage?: any, user?: TestUser } -export const createTestUser = async (args: Args = { storage: engine.storage, user: testUser }): Promise => { - const { storage, user } = args +export const createTestUser = async (args: Args = {}): Promise => { + const { storage = engine.storage, user = testUser } = args const payload = { email: user.email, password: user.password } const createdUser: User = await new Promise(resolve => storage.models.User.create(payload, (err: Error, user: any) => { @@ -22,8 +22,8 @@ export const createTestUser = async (args: Args = { storage: engine.storage, use return createdUser } -export const deleteTestUser = async (args: Args = { storage: engine.storage, user: testUser }): Promise => { - const { storage, user } = args +export const deleteTestUser = async (args: Args = { }): Promise => { + const { storage = engine.storage, user = testUser } = args return await new Promise(resolve => storage.models.User.deleteOne({ email: user.email, }, (err: Error) => { From 13db2b775afff333355a9633798ca4e64f2af020 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 13:56:17 -0400 Subject: [PATCH 13/32] test(e2e): move cleanup to global setup & teardown --- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 2 -- tests/lib/e2e/global-setup.ts | 22 ++++++++++++++++++++++ tests/lib/e2e/global-teardown.ts | 14 ++++++++++++++ tests/lib/e2e/jest-e2e.json | 1 + tests/lib/e2e/users/users.e2e-spec.ts | 10 +--------- 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 tests/lib/e2e/global-setup.ts diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index c647867e1..455bf501b 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -12,12 +12,10 @@ describe('Bridge E2E Tests', () => { let testUser: User beforeAll(async () => { - await engine.storage.models.Bucket.deleteMany({}) testUser = await createTestUser() }) afterAll(async () => { - await engine.storage.models.Bucket.deleteMany({}) await deleteTestUser() }) diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts new file mode 100644 index 000000000..363fcfe78 --- /dev/null +++ b/tests/lib/e2e/global-setup.ts @@ -0,0 +1,22 @@ +import { config as loadEnv } from 'dotenv' +import { MongoClient } from 'mongodb'; +declare var globalThis: any + +export default async () => { + loadEnv(); + const url = process.env.inxtbridge_storage__mongoUrl; + if (!url) throw new Error('Missing mongo url'); + if (!url.includes('test')) { + throw new Error("For caution test database must include test in it's name"); + } + const client = new MongoClient(url); + await client.connect(); + + const db = client.db(); + await Promise.all([ + db.collection('users').deleteMany({}), + db.collection('buckets').deleteMany({}) + ]); + + globalThis.mongoClient = client; +} \ No newline at end of file diff --git a/tests/lib/e2e/global-teardown.ts b/tests/lib/e2e/global-teardown.ts index c2c603d74..a4938b267 100644 --- a/tests/lib/e2e/global-teardown.ts +++ b/tests/lib/e2e/global-teardown.ts @@ -1,3 +1,17 @@ +import { type MongoClient } from 'mongodb' +declare var globalThis: any + export default async () => { + + const client = globalThis.mongoClient as MongoClient; + const db = client.db(); + + await Promise.all([ + db.collection('users').deleteMany({}), + db.collection('buckets').deleteMany({}) + ]); + + await client.close(); + process.emit('SIGINT') } diff --git a/tests/lib/e2e/jest-e2e.json b/tests/lib/e2e/jest-e2e.json index 5fddc0ec6..b0a43159d 100644 --- a/tests/lib/e2e/jest-e2e.json +++ b/tests/lib/e2e/jest-e2e.json @@ -1,6 +1,7 @@ { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", + "globalSetup": "./global-setup.ts", "globalTeardown": "./global-teardown.ts", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 1e941a578..785d301df 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -16,14 +16,6 @@ jest.mock('jsonwebtoken', () => ({ verify: jest.fn((_, __, ___, cb) => cb(null, describe('Bridge E2E Tests', () => { - beforeAll(async () => { - await engine.storage.models.User.deleteMany({}) - }) - - afterAll(async () => { - await engine.storage.models.User.deleteMany({}) - }) - beforeEach(() => { jest.clearAllMocks() }) @@ -50,7 +42,7 @@ describe('Bridge E2E Tests', () => { const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) - expect(users[0].toObject().activated).toBe(true) + // expect(users[0].toObject().activated).toBe(true) // expect(dispatchSendGridMock).toHaveBeenCalled() From ef3d1e56b14e81957c605020530fe0869785a08b Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 14:01:57 -0400 Subject: [PATCH 14/32] fix(users): await `user activated` update --- lib/server/routes/users.js | 30 ++++++++++++++++----------- tests/lib/e2e/users/users.e2e-spec.ts | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/server/routes/users.js b/lib/server/routes/users.js index 470becc6d..48c5070ef 100644 --- a/lib/server/routes/users.js +++ b/lib/server/routes/users.js @@ -181,23 +181,29 @@ UsersRouter.prototype.createUser = function (req, res, next) { trackUserActivated(user.uuid, req.body.email); - user.save(); + user.save((err) => { - /* - analytics.identify(req.headers.dnt, { - userId: user.uuid, - traits: { - activated: false + if (err) { + return next(err); } - }); + /* + analytics.identify(req.headers.dnt, { + userId: user.uuid, + traits: { + activated: false + } + }); - analytics.track(req.headers.dnt, { - userId: user.uuid, - event: 'User Created' + analytics.track(req.headers.dnt, { + userId: user.uuid, + event: 'User Created' + }); + */ + + self._dispatchAndCreatePubKey(user, req, res, next); }); - */ - self._dispatchAndCreatePubKey(user, req, res, next); + }); } }; diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 785d301df..592864553 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -42,7 +42,7 @@ describe('Bridge E2E Tests', () => { const users = await engine.storage.models.User.find({ _id: response.body.id }) expect(users).toHaveLength(1) - // expect(users[0].toObject().activated).toBe(true) + expect(users[0].toObject().activated).toBe(true) // expect(dispatchSendGridMock).toHaveBeenCalled() From 5c408f38e6f04adf6deaed5ec2c9de66f8be5786 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 18:28:40 -0400 Subject: [PATCH 15/32] test(e2e): test file upload --- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 89 +-------------- tests/lib/e2e/buckets/files.e2e-spec.ts | 126 ++++++++++++++++++++++ tests/lib/e2e/global-setup.ts | 3 +- tests/lib/e2e/global-teardown.ts | 3 +- 4 files changed, 131 insertions(+), 90 deletions(-) create mode 100644 tests/lib/e2e/buckets/files.e2e-spec.ts diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index 455bf501b..e7992d283 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -1,13 +1,9 @@ import { dataGenerator } from './../users.fixtures' -// import crypto from 'crypto'; import { createTestUser, deleteTestUser, getAuth, } from '../utils' import { engine, testServer } from '../setup' -// import axios, { type AxiosStatic } from 'axios' import { type User } from '../users.fixtures'; -jest.mock('axios', () => ({ get: jest.fn() })) - describe('Bridge E2E Tests', () => { let testUser: User @@ -189,90 +185,7 @@ describe('Bridge E2E Tests', () => { }) }) - // describe('File Management', () => { - - // describe('File upload v1', () => { - - // it('Uploads and finishes correctly', async () => { - - // const nodeID = engine._config.application.CLUSTER['0'] - - // const get = axios.get as jest.MockWithArgs - - // get.mockResolvedValue(Promise.resolve({ data: { result: 'http://fake-url' } } as any)) - - // await new Promise(resolve => engine.storage.models.Contact.record({ - // nodeID, - // protocol: "1.2.0-INXT", - // address: "72.132.43.2", // this ip address is an example - // port: 43758, - // lastSeen: new Date(), - // }, resolve)) - - // const { body: { id: bucketId } } = await testServer - // .post('/buckets') - // .set('Authorization', getAuth(testUser)) - - - // const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) - // .set('Authorization', getAuth(testUser)) - // .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) - - - // console.log({ body: { ...response.body } }); - - // const { uploads } = response.body; - - // for (const upload of uploads) { - // const { url, urls, index, uuid } = upload; - // expect(url).toBeDefined(); - // expect(url).toContain('http'); - // expect(url).toBe('http://fake-url') - // expect(urls).toBeNull(); - // expect(uuid).toBeDefined(); - // const file = crypto.randomBytes(50).toString('hex'); - // // await axios.put(url, file, { headers: { 'Content-Type': 'application/octet-stream', }, }); - // } - - // const index = crypto.randomBytes(32).toString('hex'); - // const responseComplete = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) - // .set('Authorization', getAuth(testUser)) - // .send({ - // index, - // shards: [ - // { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, - // { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, - // ], - // }); - - // expect(responseComplete.status).toBe(200); - - // const { - // bucket, - // created, - // filename, - // id, - // index: indexResponse, - // mimetype, - // renewal, - // size, - // version, - // } = responseComplete.body; - - // expect(bucket).toEqual(bucketId); - // expect(created).toBeDefined(); - // expect(filename).toBeDefined(); - // expect(id).toBeDefined(); - // expect(indexResponse).toEqual(index); - // expect(mimetype).toBeDefined(); - // expect(renewal).toBeDefined(); - // expect(size).toBeGreaterThan(0); - // expect(typeof size).toBe('number'); - // expect(version).toBe(2); - // }); - // }) - - // }) + }) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts new file mode 100644 index 000000000..23f1e02e7 --- /dev/null +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -0,0 +1,126 @@ +import { ObjectId } from 'mongodb' +import crypto from 'crypto' +import axios from 'axios'; +import { engine, testServer } from '../setup'; +import { type User } from '../users.fixtures'; +import { createTestUser, getAuth } from '../utils'; + +jest.mock('axios', () => ({ get: jest.fn(), post: jest.fn() })) + +describe('Bridge E2E Tests', () => { + + let testUser: User + beforeAll(async () => { + testUser = await createTestUser() + + // Create a contact + const nodeID = engine._config.application.CLUSTER['0'] + const payload = { nodeID, protocol: "1.2.0-INXT", address: "72.132.43.2", port: 43758, lastSeen: new Date(), } + await new Promise(resolve => engine.storage.models.Contact.record(payload, resolve)) + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('File Management v1', () => { + + + describe('Uploading a file', () => { + + it('When a user wants to upload a file, it should work for owned buckets and get a list of upload links one per each file part', async () => { + // Arrange: Mock http calls + const get = axios.get as jest.MockedFunction; + get.mockImplementation(async () => ({ data: { result: 'http://fake-url' } })) + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .expect(201) + + // Act: start the upload + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + + // Assert + expect(response.status).toBe(200); + const { uploads } = response.body; + + expect(uploads).toHaveLength(2); + + const upload1 = uploads[0]; + expect(upload1.url).toBe('http://fake-url') + expect(upload1.urls).toBeNull(); + expect(upload1.uuid).toBeDefined(); + + const upload2 = uploads[1]; + expect(upload2.url).toBeDefined(); + expect(upload2.url).toBe('http://fake-url') + expect(upload2.urls).toBeNull(); + expect(upload2.uuid).toBeDefined(); + + }) + + + it('When a user finishes to upload a file, the user can finish the upload with a hash per each part uploaded', async () => { + + const get = axios.get as jest.MockedFunction; + get.mockImplementation(async (url: string) => { + // Mock the upload link + if (url.includes('/v2/upload/link')) return { data: { result: 'http://fake-url' } } + // Mock the exists check + if (url.includes('/exists')) return { status: 200 } + // Fail for any other request + throw new Error('Not implemented') + }) + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + + const { uploads } = response.body; + + // Act: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: [ + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, + ], + }); + + // Assert + expect(responseComplete.status).toBe(200); + + const body = responseComplete.body; + + expect(body.bucket).toEqual(bucketId); + expect(body.created).toBeDefined(); + expect(body.filename).toBeDefined(); + expect(body.id).toBeDefined(); + expect(body.index).toEqual(index); + expect(body.mimetype).toBeDefined(); + expect(body.renewal).toBeDefined(); + expect(body.size).toBeGreaterThan(0); + expect(typeof body.size).toBe('number'); + expect(body.version).toBe(2); + }); + + }) + + }) + + + +}); \ No newline at end of file diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts index 363fcfe78..cc149a1e7 100644 --- a/tests/lib/e2e/global-setup.ts +++ b/tests/lib/e2e/global-setup.ts @@ -15,7 +15,8 @@ export default async () => { const db = client.db(); await Promise.all([ db.collection('users').deleteMany({}), - db.collection('buckets').deleteMany({}) + db.collection('buckets').deleteMany({}), + db.collection('contacts').deleteMany({}), ]); globalThis.mongoClient = client; diff --git a/tests/lib/e2e/global-teardown.ts b/tests/lib/e2e/global-teardown.ts index a4938b267..439fdea0e 100644 --- a/tests/lib/e2e/global-teardown.ts +++ b/tests/lib/e2e/global-teardown.ts @@ -8,7 +8,8 @@ export default async () => { await Promise.all([ db.collection('users').deleteMany({}), - db.collection('buckets').deleteMany({}) + db.collection('buckets').deleteMany({}), + db.collection('contacts').deleteMany({}), ]); await client.close(); From b19916e3385f05fc1a6d3f1ca425e5ee5ac30039 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 9 Jan 2024 18:48:21 -0400 Subject: [PATCH 16/32] test(e2e): test file download --- lib/server/routes/buckets.js | 2 +- tests/lib/e2e/buckets/files.e2e-spec.ts | 76 +++++++++++++++++++------ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/lib/server/routes/buckets.js b/lib/server/routes/buckets.js index fc7585fc2..1dd49363b 100644 --- a/lib/server/routes/buckets.js +++ b/lib/server/routes/buckets.js @@ -20,7 +20,7 @@ const { randomBytes } = require('crypto'); const DELETING_FILE_MESSAGE = require('../queues/messageTypes').DELETING_FILE_MESSAGE; const { v4: uuidv4, validate: uuidValidate } = require('uuid'); const { isHexString } = require('../middleware/farmer-auth'); -const { default: axios } = require('axios'); +const axios = require('axios'); const { MongoDBBucketEntriesRepository } = require('../../core/bucketEntries/MongoDBBucketEntriesRepository'); const { BucketsUsecase, BucketEntryNotFoundError, BucketEntryFrameNotFoundError, BucketNotFoundError, BucketForbiddenError, MissingUploadsError, MaxSpaceUsedError, InvalidUploadIndexes, InvalidMultiPartValueError, NoNodeFoundError, EmptyMirrorsError } = require('../../core/buckets/usecase'); const { BucketEntriesUsecase, BucketEntryVersionNotFoundError } = require('../../core/bucketEntries/usecase'); diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 23f1e02e7..70122277e 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -1,4 +1,3 @@ -import { ObjectId } from 'mongodb' import crypto from 'crypto' import axios from 'axios'; import { engine, testServer } from '../setup'; @@ -21,18 +20,26 @@ describe('Bridge E2E Tests', () => { beforeEach(() => { jest.clearAllMocks() + + const get = axios.get as jest.MockedFunction; + get.mockImplementation(async (url: string) => { + // Mock the upload request + if (url.includes('/v2/upload/link')) return { data: { result: 'http://fake-url' } } + // Mock the download request + if (url.includes('/v2/download/link')) return { data: { result: 'http://fake-url' } } + // Mock the exists check + if (url.includes('/exists')) return { status: 200 } + // Fail for any other request + throw new Error('Not implemented') + }) }) - describe('File Management v1', () => { + describe('File Management v2', () => { describe('Uploading a file', () => { it('When a user wants to upload a file, it should work for owned buckets and get a list of upload links one per each file part', async () => { - // Arrange: Mock http calls - const get = axios.get as jest.MockedFunction; - get.mockImplementation(async () => ({ data: { result: 'http://fake-url' } })) - // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer .post('/buckets') @@ -66,16 +73,6 @@ describe('Bridge E2E Tests', () => { it('When a user finishes to upload a file, the user can finish the upload with a hash per each part uploaded', async () => { - const get = axios.get as jest.MockedFunction; - get.mockImplementation(async (url: string) => { - // Mock the upload link - if (url.includes('/v2/upload/link')) return { data: { result: 'http://fake-url' } } - // Mock the exists check - if (url.includes('/exists')) return { status: 200 } - // Fail for any other request - throw new Error('Not implemented') - }) - // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer .post('/buckets') @@ -119,6 +116,53 @@ describe('Bridge E2E Tests', () => { }) + describe('Downloading a file', () => { + + it('When a user wants to download a file, it should get a list of links for each file part', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + + // Arrange: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: [ + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, + ], + }); + + + // Act: download the file + const response = await testServer.get(`/v2/buckets/${bucketId}/files/${file.id}/mirrors`) + .set('Authorization', getAuth(testUser)) + + // Assert + expect(response.status).toBe(200); + const body = response.body; + expect(body.bucket).toBe(bucketId) + expect(body.created).toBeDefined() + expect(body.index).toBe(index) + expect(body.shards).toHaveLength(2) + expect(body.shards[0].hash).toBeDefined() + expect(body.shards[0].url).toBeDefined() + expect(body.shards[1].hash).toBeDefined() + expect(body.shards[1].url).toBeDefined() + + }) + + + }) + }) From f3830b4689ca83d9201a1303b669dc6b9032cf9c Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Wed, 10 Jan 2024 10:01:21 -0400 Subject: [PATCH 17/32] test(e2e): delete a file --- tests/lib/e2e/buckets/files.e2e-spec.ts | 76 ++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 70122277e..76760d8d3 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -1,3 +1,4 @@ +import { ObjectId } from 'mongodb' import crypto from 'crypto' import axios from 'axios'; import { engine, testServer } from '../setup'; @@ -12,10 +13,15 @@ describe('Bridge E2E Tests', () => { beforeAll(async () => { testUser = await createTestUser() - // Create a contact - const nodeID = engine._config.application.CLUSTER['0'] - const payload = { nodeID, protocol: "1.2.0-INXT", address: "72.132.43.2", port: 43758, lastSeen: new Date(), } - await new Promise(resolve => engine.storage.models.Contact.record(payload, resolve)) + // Create fake contact per each node + const nodeIDs = Object.values(engine._config.application.CLUSTER) + await Promise.all( + nodeIDs.map((nodeID, index) => { + const payload = { nodeID, protocol: "1.2.0-INXT", address: `72.132.43.${index}`, port: 43758 + index, lastSeen: new Date(), } + return new Promise(resolve => engine.storage.models.Contact.record(payload, resolve)) + }) + ) + }) beforeEach(() => { @@ -36,7 +42,6 @@ describe('Bridge E2E Tests', () => { describe('File Management v2', () => { - describe('Uploading a file', () => { it('When a user wants to upload a file, it should work for owned buckets and get a list of upload links one per each file part', async () => { @@ -140,8 +145,8 @@ describe('Bridge E2E Tests', () => { { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, ], }); - - + + // Act: download the file const response = await testServer.get(`/v2/buckets/${bucketId}/files/${file.id}/mirrors`) .set('Authorization', getAuth(testUser)) @@ -157,14 +162,67 @@ describe('Bridge E2E Tests', () => { expect(body.shards[0].url).toBeDefined() expect(body.shards[1].hash).toBeDefined() expect(body.shards[1].url).toBeDefined() - - }) + }) }) }) + describe('File Management v1', () => { + describe('Deleting a file', () => { + + it('When a user wants to delete a file, it should work if the file and the bucket exist', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + + // Arrange: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: [ + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, + { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, + ], + }); + + + // Act: remove the file + const response = await testServer.delete(`/buckets/${bucketId}/files/${file.id}`) + .set('Authorization', getAuth(testUser)) + + // Assert + expect(response.status).toBe(204); + + }) + + it('When a user wants to delete a file, it should work if the file does not exist', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Act: remove the file + const fakeFileId = new ObjectId() + const response = await testServer.delete(`/buckets/${bucketId}/files/${fakeFileId}`) + .set('Authorization', getAuth(testUser)) + + // Assert + expect(response.status).toBe(404); + + }) + }) + }) + }); \ No newline at end of file From 504b001cdc3fdc8655096f3b326b5fbb8011ed05 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Wed, 10 Jan 2024 13:16:10 -0400 Subject: [PATCH 18/32] test(e2e): shutdown engine manually --- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 5 +++-- tests/lib/e2e/buckets/files.e2e-spec.ts | 19 ++++++++++++++++++- tests/lib/e2e/setup.ts | 13 +++++++++++++ tests/lib/e2e/users/users.e2e-spec.ts | 6 +++++- tests/lib/e2e/utils.ts | 14 +++++++++++++- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index e7992d283..a71f6cc18 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -1,5 +1,5 @@ import { dataGenerator } from './../users.fixtures' -import { createTestUser, deleteTestUser, getAuth, } from '../utils' +import { createTestUser, deleteTestUser, getAuth, shutdownEngine, } from '../utils' import { engine, testServer } from '../setup' import { type User } from '../users.fixtures'; @@ -13,6 +13,7 @@ describe('Bridge E2E Tests', () => { afterAll(async () => { await deleteTestUser() + await shutdownEngine(engine) }) beforeEach(() => { @@ -185,7 +186,7 @@ describe('Bridge E2E Tests', () => { }) }) - + }) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 76760d8d3..0cfbac152 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -3,7 +3,7 @@ import crypto from 'crypto' import axios from 'axios'; import { engine, testServer } from '../setup'; import { type User } from '../users.fixtures'; -import { createTestUser, getAuth } from '../utils'; +import { createTestUser, getAuth, shutdownEngine } from '../utils'; jest.mock('axios', () => ({ get: jest.fn(), post: jest.fn() })) @@ -24,6 +24,10 @@ describe('Bridge E2E Tests', () => { }) + afterAll(async() => { + await shutdownEngine(engine) + }) + beforeEach(() => { jest.clearAllMocks() @@ -221,6 +225,19 @@ describe('Bridge E2E Tests', () => { }) }) + + describe('Sharing a file', () => { + it('When a user wants to share a file, it should be able to create a token for the bucket', async () => { + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Act + const response = await testServer.post(`/buckets/${bucketId}/tokens`) + }) + }) }) diff --git a/tests/lib/e2e/setup.ts b/tests/lib/e2e/setup.ts index 5f93a3cec..684ff2b6a 100644 --- a/tests/lib/e2e/setup.ts +++ b/tests/lib/e2e/setup.ts @@ -1,5 +1,18 @@ import supertest from 'supertest'; +declare var globalThis: any; + +export const intervalRefs: NodeJS.Timer[] = []; + +const realSetInterval = globalThis.setInterval; + +globalThis.setInterval = jest.fn((...args: any[]) => { + const ref = realSetInterval(...args); + intervalRefs.push(ref); + return ref; +}) + + if (process.env.inxtbridge_server__port !== '0') { console.warn('Warning: inxtbridge_server__port is not set to 0, this may cause conflicts with the test server'); } diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 592864553..6c4595db4 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -1,4 +1,4 @@ -import { getAuth, } from '../utils' +import { getAuth, shutdownEngine, } from '../utils' import sendGridMail from '@sendgrid/mail' import { engine, testServer } from '../setup' @@ -20,6 +20,10 @@ describe('Bridge E2E Tests', () => { jest.clearAllMocks() }) + afterAll(async() => { + await shutdownEngine(engine) + }) + describe('Users Management', () => { diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index e9451accd..17a515fcb 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -1,4 +1,4 @@ -import { engine } from './setup'; +import { engine, intervalRefs } from './setup'; import { TestUser, testUser, User } from './users.fixtures' type Args = { storage?: any, user?: TestUser } @@ -38,3 +38,15 @@ export const getAuth = (user: Omit = testUser) => { return `Basic ${credential}`; } + +export const shutdownEngine = async (engine: any) => { + + await Promise.all([ + engine.storage.connection.close(), + engine.networkQueue.close(), + engine.redis.quit(), + engine.server.server.close(), + ]) + intervalRefs.forEach(ref => clearInterval(ref)) + +} \ No newline at end of file From 04188ee7e927c5daebb63cf67cf32d0226773856 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Wed, 10 Jan 2024 16:14:34 -0400 Subject: [PATCH 19/32] test(e2e): add test for sharing files * test create a bucket token * test get the file info This to endpoint are called from the `drive-server` --- tests/lib/e2e/buckets/files.e2e-spec.ts | 132 +++++++++++++++++++++--- tests/lib/e2e/global-setup.ts | 7 +- tests/lib/e2e/global-teardown.ts | 8 +- 3 files changed, 121 insertions(+), 26 deletions(-) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 0cfbac152..93ca8f7e0 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -4,12 +4,13 @@ import axios from 'axios'; import { engine, testServer } from '../setup'; import { type User } from '../users.fixtures'; import { createTestUser, getAuth, shutdownEngine } from '../utils'; +import sinon from 'sinon'; -jest.mock('axios', () => ({ get: jest.fn(), post: jest.fn() })) describe('Bridge E2E Tests', () => { let testUser: User + let axiosGetStub: sinon.SinonStub beforeAll(async () => { testUser = await createTestUser() @@ -24,26 +25,27 @@ describe('Bridge E2E Tests', () => { }) - afterAll(async() => { + afterAll(async () => { await shutdownEngine(engine) }) + + beforeEach(() => { - jest.clearAllMocks() - const get = axios.get as jest.MockedFunction; - get.mockImplementation(async (url: string) => { - // Mock the upload request + axiosGetStub = sinon.stub(axios, 'get') + jest.clearAllMocks() + axiosGetStub.callsFake(async (url: string) => { if (url.includes('/v2/upload/link')) return { data: { result: 'http://fake-url' } } - // Mock the download request if (url.includes('/v2/download/link')) return { data: { result: 'http://fake-url' } } - // Mock the exists check - if (url.includes('/exists')) return { status: 200 } - // Fail for any other request - throw new Error('Not implemented') + if (url.includes('exists')) return { status: 200 } }) }) + afterEach(() => { + axiosGetStub.restore() + }) + describe('File Management v2', () => { describe('Uploading a file', () => { @@ -173,7 +175,7 @@ describe('Bridge E2E Tests', () => { }) - describe('File Management v1', () => { + describe('File Management v1', () => { describe('Deleting a file', () => { it('When a user wants to delete a file, it should work if the file and the bucket exist', async () => { @@ -225,17 +227,117 @@ describe('Bridge E2E Tests', () => { }) }) - + describe('Sharing a file', () => { - it('When a user wants to share a file, it should be able to create a token for the bucket', async () => { + it('When a user wants to share a file, it should be able to create a token for the bucket passing PULL as operation', async () => { + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Act + const response = await testServer.post(`/buckets/${bucketId}/tokens`) + .set('Authorization', getAuth(testUser)) + .send({ operation: 'PULL', }) + + // Assert + expect(response.status).toBe(201); + + const { body } = response + + expect(body.bucket).toBeDefined() + expect(body.operation).toBeDefined() + expect(body.expires).toBeDefined() + expect(body.token).toBeDefined() + expect(body.encryptionKey).toBeDefined() + expect(body.id).toBeDefined() + }) + it('When a user wants to share a file, it should be able to create a token for the bucket passing PULL as operation and an existing file', async () => { // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer .post('/buckets') .set('Authorization', getAuth(testUser)) - + + // Arrange: start the upload + const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }], }) + + // Arrange: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }], + }); + // Act const response = await testServer.post(`/buckets/${bucketId}/tokens`) + .set('Authorization', getAuth(testUser)) + .send({ operation: 'PULL', file: file.id }) + + // Assert + expect(response.status).toBe(201); + + const { body } = response + + expect(body.bucket).toBeDefined() + expect(body.operation).toBeDefined() + expect(body.expires).toBeDefined() + expect(body.token).toBeDefined() + expect(body.id).toBeDefined() + expect(body.encryptionKey).toBeDefined() + expect(body.mimetype).toBeDefined() + expect(body.size).toBeDefined() + }) + + it('When a user wants to share a file, it should be able to get the file info', async () => { + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: 1000, }], }) + + // Arrange: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }], + }); + + // Act + const response = await testServer.get(`/buckets/${bucketId}/files/${file.id}/info`) + .set('Authorization', getAuth(testUser)) + + // Assert + expect(response.status).toBe(200); + + const { body: fileInfo } = response + expect(fileInfo.bucket).toBeDefined() + expect(fileInfo.index).toBeDefined() + expect(fileInfo.size).toBeDefined() + expect(fileInfo.version).toBeDefined() + expect(fileInfo.created).toBeDefined() + expect(fileInfo.renewal).toBeDefined() + expect(fileInfo.mimetype).toBeDefined() + expect(fileInfo.filename).toBeDefined() + expect(fileInfo.id).toBeDefined() + expect(fileInfo.shards).toBeDefined() + expect(fileInfo.shards).toHaveLength(1) + expect(fileInfo.shards[0].index).toBeDefined() + expect(fileInfo.shards[0].hash).toBeDefined() + expect(fileInfo.shards[0].url).toBeDefined() + }) }) }) diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts index cc149a1e7..af89fddb8 100644 --- a/tests/lib/e2e/global-setup.ts +++ b/tests/lib/e2e/global-setup.ts @@ -13,11 +13,8 @@ export default async () => { await client.connect(); const db = client.db(); - await Promise.all([ - db.collection('users').deleteMany({}), - db.collection('buckets').deleteMany({}), - db.collection('contacts').deleteMany({}), - ]); + const collections = await db.collections(); + await Promise.all(collections.map(collection => collection.deleteMany({}))); globalThis.mongoClient = client; } \ No newline at end of file diff --git a/tests/lib/e2e/global-teardown.ts b/tests/lib/e2e/global-teardown.ts index 439fdea0e..3efe4d0d1 100644 --- a/tests/lib/e2e/global-teardown.ts +++ b/tests/lib/e2e/global-teardown.ts @@ -6,12 +6,8 @@ export default async () => { const client = globalThis.mongoClient as MongoClient; const db = client.db(); - await Promise.all([ - db.collection('users').deleteMany({}), - db.collection('buckets').deleteMany({}), - db.collection('contacts').deleteMany({}), - ]); - + const collections = await db.collections(); + await Promise.all(collections.map(collection => collection.deleteMany({}))); await client.close(); process.emit('SIGINT') From 6d65a8c9fddd1ee21d5fdb630a7c461903301642 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 10:33:08 -0400 Subject: [PATCH 20/32] test(e2e): multiparts and single parts upload --- lib/core/buckets/usecase.ts | 2 +- tests/lib/e2e/buckets/files.e2e-spec.ts | 133 +++++++++++++++++------- 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/lib/core/buckets/usecase.ts b/lib/core/buckets/usecase.ts index accb8dff8..b98ed3b11 100644 --- a/lib/core/buckets/usecase.ts +++ b/lib/core/buckets/usecase.ts @@ -85,7 +85,7 @@ export class InvalidUploadIndexes extends Error { } export class InvalidMultiPartValueError extends Error { constructor() { - super('Multipart is not allowed for files smaller than 500MB'); + super('Multipart is not allowed for files smaller than 100MB'); Object.setPrototypeOf(this, InvalidMultiPartValueError.prototype); } diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 93ca8f7e0..12242f0dd 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -6,6 +6,8 @@ import { type User } from '../users.fixtures'; import { createTestUser, getAuth, shutdownEngine } from '../utils'; import sinon from 'sinon'; +const FAKE_UPLOAD_URL = 'http://fake-upload-url' +const FAKE_DOWNLOAD_URL = 'http://fake-download-url' describe('Bridge E2E Tests', () => { @@ -14,7 +16,7 @@ describe('Bridge E2E Tests', () => { beforeAll(async () => { testUser = await createTestUser() - // Create fake contact per each node + // Arrange: Create fake contact per each node const nodeIDs = Object.values(engine._config.application.CLUSTER) await Promise.all( nodeIDs.map((nodeID, index) => { @@ -26,7 +28,7 @@ describe('Bridge E2E Tests', () => { }) afterAll(async () => { - await shutdownEngine(engine) + await shutdownEngine() }) @@ -36,8 +38,12 @@ describe('Bridge E2E Tests', () => { axiosGetStub = sinon.stub(axios, 'get') jest.clearAllMocks() axiosGetStub.callsFake(async (url: string) => { - if (url.includes('/v2/upload/link')) return { data: { result: 'http://fake-url' } } - if (url.includes('/v2/download/link')) return { data: { result: 'http://fake-url' } } + if (url.includes('/v2/upload/link')) return { data: { result: FAKE_UPLOAD_URL } } + if (url.includes('/v2/upload-multipart/link')) { + const parts = new URL(url).searchParams.get('parts') + return { data: { result: new Array(parts).fill(FAKE_UPLOAD_URL), UploadId: 'fake-id' } } + } + if (url.includes('/v2/download/link')) return { data: { result: FAKE_DOWNLOAD_URL } } if (url.includes('exists')) return { status: 200 } }) }) @@ -50,7 +56,7 @@ describe('Bridge E2E Tests', () => { describe('Uploading a file', () => { - it('When a user wants to upload a file, it should work for owned buckets and get a list of upload links one per each file part', async () => { + it('When a user wants to upload a file with just one part, it should work for owned buckets and get an upload link', async () => { // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer .post('/buckets') @@ -60,7 +66,35 @@ describe('Bridge E2E Tests', () => { // Act: start the upload const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) .set('Authorization', getAuth(testUser)) - .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + .send({ uploads: [{ index: 0, size: 1000, }], }) + + // Assert + expect(response.status).toBe(200); + const { uploads } = response.body; + + expect(uploads).toHaveLength(1); + + const [upload] = uploads; + + expect(upload.url).toBe(FAKE_UPLOAD_URL) + expect(upload.urls).toBeNull(); + expect(upload.uuid).toBeDefined(); + + }) + + it('When a user wants to upload a file over 100MB with multiple parts into a owned bucket, it should work and get a list of upload links one per each file part', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .expect(201) + + // Act: start the upload + const MB100 = 100 * 1024 * 1024 + const fileParts = [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, },] + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=${fileParts.length}`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: fileParts, }) // Assert expect(response.status).toBe(200); @@ -68,16 +102,38 @@ describe('Bridge E2E Tests', () => { expect(uploads).toHaveLength(2); - const upload1 = uploads[0]; - expect(upload1.url).toBe('http://fake-url') - expect(upload1.urls).toBeNull(); - expect(upload1.uuid).toBeDefined(); + const [firstUpload, secondUpload] = uploads; + + expect(firstUpload.url).toBeNull(); + expect(firstUpload.urls).toBeDefined(); + expect(firstUpload.uuid).toBeDefined(); + expect(firstUpload.UploadId).toBeDefined(); - const upload2 = uploads[1]; - expect(upload2.url).toBeDefined(); - expect(upload2.url).toBe('http://fake-url') - expect(upload2.urls).toBeNull(); - expect(upload2.uuid).toBeDefined(); + expect(secondUpload.url).toBeDefined(); + expect(secondUpload.url).toBeNull(); + expect(secondUpload.urls).toBeDefined(); + expect(secondUpload.uuid).toBeDefined(); + expect(secondUpload.UploadId).toBeDefined(); + + }) + + it('When a user wants to upload a file with multiple parts, it should fail if is under 100MB', async () => { + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + .expect(201) + + // Act: start the upload + const MB100 = 99 * 1024 * 1024 + const fileParts = [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, },] + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=${fileParts.length}`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: fileParts, }) + + // Assert + expect(response.status).toBe(400) + expect(response.body.error).toBe('Multipart is not allowed for files smaller than 100MB') }) @@ -92,9 +148,9 @@ describe('Bridge E2E Tests', () => { // Arrange: start the upload const response = await testServer.post(`/v2/buckets/${bucketId}/files/start`) .set('Authorization', getAuth(testUser)) - .send({ uploads: [{ index: 0, size: 1000, }, { index: 1, size: 10000, },], }) + .send({ uploads: [{ index: 0, size: 1000, }] }) - const { uploads } = response.body; + const { uploads: [upload] } = response.body; // Act: finish the upload const index = crypto.randomBytes(32).toString('hex'); @@ -102,10 +158,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) .send({ index, - shards: [ - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, - ], + shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, }], }); // Assert @@ -146,10 +199,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) .send({ index, - shards: [ - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, - ], + shards: (uploads as any[]).map(upload => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })), }); @@ -160,14 +210,18 @@ describe('Bridge E2E Tests', () => { // Assert expect(response.status).toBe(200); const body = response.body; + expect(body.bucket).toBe(bucketId) expect(body.created).toBeDefined() expect(body.index).toBe(index) expect(body.shards).toHaveLength(2) - expect(body.shards[0].hash).toBeDefined() - expect(body.shards[0].url).toBeDefined() - expect(body.shards[1].hash).toBeDefined() - expect(body.shards[1].url).toBeDefined() + + const [firstShard, secondShard] = body.shards + + expect(firstShard.hash).toBeDefined() + expect(firstShard.url).toBeDefined() + expect(secondShard.hash).toBeDefined() + expect(secondShard.url).toBeDefined() }) @@ -195,10 +249,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) .send({ index, - shards: [ - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }, - { hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[1].uuid, }, - ], + shards: (uploads as any[]).map(upload => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })), }); @@ -261,7 +312,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) // Arrange: start the upload - const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + const { body: { uploads: [upload] } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) .set('Authorization', getAuth(testUser)) .send({ uploads: [{ index: 0, size: 1000, }], }) @@ -271,7 +322,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) .send({ index, - shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }], + shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, }], }); // Act @@ -302,7 +353,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) // Arrange: start the upload - const { body: { uploads } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) + const { body: { uploads: [upload] } } = await testServer.post(`/v2/buckets/${bucketId}/files/start`) .set('Authorization', getAuth(testUser)) .send({ uploads: [{ index: 0, size: 1000, }], }) @@ -312,7 +363,7 @@ describe('Bridge E2E Tests', () => { .set('Authorization', getAuth(testUser)) .send({ index, - shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: uploads[0].uuid, }], + shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, }], }); // Act @@ -334,9 +385,11 @@ describe('Bridge E2E Tests', () => { expect(fileInfo.id).toBeDefined() expect(fileInfo.shards).toBeDefined() expect(fileInfo.shards).toHaveLength(1) - expect(fileInfo.shards[0].index).toBeDefined() - expect(fileInfo.shards[0].hash).toBeDefined() - expect(fileInfo.shards[0].url).toBeDefined() + + const [shard] = fileInfo.shards + expect(shard.index).toBeDefined() + expect(shard.hash).toBeDefined() + expect(shard.url).toBeDefined() }) }) From d4fe7e674205cc848876affa8ae1f36d8a8ff920 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 10:41:40 -0400 Subject: [PATCH 21/32] chore: cleanup all test users --- tests/lib/e2e/buckets/buckets.e2e-spec.ts | 29 +++++++++++------------ tests/lib/e2e/users/users.e2e-spec.ts | 26 ++++++++------------ tests/lib/e2e/utils.ts | 29 ++++++++++++++--------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/lib/e2e/buckets/buckets.e2e-spec.ts b/tests/lib/e2e/buckets/buckets.e2e-spec.ts index a71f6cc18..05745eaaa 100644 --- a/tests/lib/e2e/buckets/buckets.e2e-spec.ts +++ b/tests/lib/e2e/buckets/buckets.e2e-spec.ts @@ -1,5 +1,5 @@ import { dataGenerator } from './../users.fixtures' -import { createTestUser, deleteTestUser, getAuth, shutdownEngine, } from '../utils' +import { cleanUpTestUsers, createTestUser, getAuth, shutdownEngine, } from '../utils' import { engine, testServer } from '../setup' import { type User } from '../users.fixtures'; @@ -12,8 +12,8 @@ describe('Bridge E2E Tests', () => { }) afterAll(async () => { - await deleteTestUser() - await shutdownEngine(engine) + await cleanUpTestUsers() + await shutdownEngine() }) beforeEach(() => { @@ -26,7 +26,7 @@ describe('Bridge E2E Tests', () => { describe('Creating a bucket', () => { - it('When you want create the root bucket, it should work without any arguments', async () => { + it('When you want to create the root bucket, it should work without any arguments', async () => { // Act const response = await testServer @@ -37,11 +37,11 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(201) expect(response.body).toHaveProperty('id') - const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) - expect(buckets).toHaveLength(1) + const bucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + expect(bucket).not.toBeNull() }) - it('When you want create a bucket with name and pubkeys, it should work with correctly formatted pubkeys', async () => { + it('When you want to create a bucket with name and pubkeys, it should work with correctly formatted pubkeys', async () => { // Act const response = await testServer @@ -56,11 +56,11 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(201) expect(response.body).toHaveProperty('id') - const buckets = await engine.storage.models.Bucket.find({ _id: response.body.id }) - expect(buckets).toHaveLength(1) + const bucket = await engine.storage.models.Bucket.findOne({ _id: response.body.id }) + expect(bucket).not.toBeNull() }) - it('When you want create a bucket with name and pubkeys, it should fail with incorrectly formatted pubkeys', async () => { + it('When you want to create a bucket with name and pubkeys, it should fail with incorrectly formatted pubkeys', async () => { // Act const response = await testServer @@ -79,7 +79,7 @@ describe('Bridge E2E Tests', () => { describe('Updating a bucket', () => { - it('Whe you want to update a bucket, it should work with empty pubkeys list', async () => { + it('When you want to update a bucket, it should work with empty pubkeys list', async () => { // Arrange const { body: bucket } = await testServer .post('/buckets') @@ -104,7 +104,7 @@ describe('Bridge E2E Tests', () => { expect(dbBucket.toObject().pubkeys).toEqual([]) }) - it('Whe you want to update a bucket, it should fail with invalid pubkeys list', async () => { + it('When you want to update a bucket, it should fail with invalid pubkeys list', async () => { // Arrange const { body: bucket } = await testServer .post('/buckets') @@ -160,6 +160,7 @@ describe('Bridge E2E Tests', () => { // Arrange: Create a bucket const owner = await createTestUser({ user: { email: dataGenerator.email(), password: dataGenerator.hash({ length: 64 }), maxSpaceBytes: 321312313 } }) + const notTheOwner = testUser const { body: bucket } = await testServer .post('/buckets') @@ -173,14 +174,12 @@ describe('Bridge E2E Tests', () => { // Act: Delete the bucket const response = await testServer .delete(`/buckets/${bucket.id}`) - .set('Authorization', getAuth(testUser)) + .set('Authorization', getAuth(notTheOwner)) // Assert expect(response.status).toBe(404) - await deleteTestUser({ user: owner }) - }) }) }) diff --git a/tests/lib/e2e/users/users.e2e-spec.ts b/tests/lib/e2e/users/users.e2e-spec.ts index 6c4595db4..e7887097a 100644 --- a/tests/lib/e2e/users/users.e2e-spec.ts +++ b/tests/lib/e2e/users/users.e2e-spec.ts @@ -5,13 +5,13 @@ import { engine, testServer } from '../setup' import { dataGenerator } from '../users.fixtures' -// NB: Mock SendGrid +// Mock SendGrid jest.mock('@sendgrid/mail', () => ({ setApiKey: jest.fn(), send: jest.fn((_, __, done) => typeof done === 'function' ? done() : Promise.resolve()), })) -// NB: Mock JWT verification +// Mock JWT verification jest.mock('jsonwebtoken', () => ({ verify: jest.fn((_, __, ___, cb) => cb(null, {})) })) describe('Bridge E2E Tests', () => { @@ -21,7 +21,7 @@ describe('Bridge E2E Tests', () => { }) afterAll(async() => { - await shutdownEngine(engine) + await shutdownEngine() }) describe('Users Management', () => { @@ -43,12 +43,10 @@ describe('Bridge E2E Tests', () => { // Assert expect(response.status).toBe(201); - const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) + const user = await engine.storage.models.User.findOne({ _id: response.body.id }) + expect(user).not.toBeNull() - expect(users[0].toObject().activated).toBe(true) - - // expect(dispatchSendGridMock).toHaveBeenCalled() + expect(user.toObject().activated).toBe(true) }) it('When creating a user, it should fail if email is in use', async () => { @@ -204,11 +202,11 @@ describe('Bridge E2E Tests', () => { // Assert expect(response.status).toBe(200); - // expect(response.status).toBe(201); - const users = await engine.storage.models.User.find({ _id: response.body.id }) - expect(users).toHaveLength(1) - expect(users[0].toObject().activated).toBe(true) + const user = await engine.storage.models.User.findOne({ _id: response.body.id }) + expect(user).not.toBeNull() + + expect(user.toObject().activated).toBe(true) }) it('When creating a user, it should fail if email is in use', async () => { @@ -239,7 +237,6 @@ describe('Bridge E2E Tests', () => { const { body: user } = await testServer .post('/v2/users') .send(payload) - // .expect(201) .expect(200); @@ -269,7 +266,6 @@ describe('Bridge E2E Tests', () => { const { body: user } = await testServer .post('/v2/users') .send(payload) - // .expect(201) .expect(200); @@ -297,7 +293,6 @@ describe('Bridge E2E Tests', () => { const { body: user } = await testServer .post('/v2/users') .send(payload) - // .expect(201) .expect(200); @@ -330,7 +325,6 @@ describe('Bridge E2E Tests', () => { const { body: user } = await testServer .post('/v2/users') .send(payload) - // .expect(201) .expect(200); diff --git a/tests/lib/e2e/utils.ts b/tests/lib/e2e/utils.ts index 17a515fcb..eb4ccb002 100644 --- a/tests/lib/e2e/utils.ts +++ b/tests/lib/e2e/utils.ts @@ -3,13 +3,13 @@ import { TestUser, testUser, User } from './users.fixtures' type Args = { storage?: any, user?: TestUser } +const createdUsers: User[] = [] export const createTestUser = async (args: Args = {}): Promise => { const { storage = engine.storage, user = testUser } = args const payload = { email: user.email, password: user.password } - const createdUser: User = await new Promise(resolve => storage.models.User.create(payload, (err: Error, user: any) => { - if (err) throw err - resolve(user.toObject()) + const createdUser: User = await new Promise((resolve, reject) => storage.models.User.create(payload, (err: Error, user: any) => { + err ? reject(err) : resolve(user.toObject()) })) await storage.models.User.updateOne( @@ -19,18 +19,25 @@ export const createTestUser = async (args: Args = {}): Promise => { createdUser.password = user.password + createdUsers.push(createdUser) + return createdUser } -export const deleteTestUser = async (args: Args = { }): Promise => { +export const cleanUpTestUsers = (): Promise => { + return new Promise((resolve, reject) => { + engine.storage.models.User.deleteMany({ email: { $in: [createdUsers.map(user => user.email)] } }, (err: Error) => { + err ? reject(err) : resolve() + }) + }) + +} + +export const deleteTestUser = (args: Args = {}): Promise => { const { storage = engine.storage, user = testUser } = args - return await new Promise(resolve => storage.models.User.deleteOne({ - email: user.email, - }, (err: Error) => { - if (err) throw err - resolve() + return new Promise((resolve, reject) => storage.models.User.deleteOne({ email: user.email, }, (err: Error) => { + err ? reject(err) : resolve() })) - } export const getAuth = (user: Omit = testUser) => { @@ -39,7 +46,7 @@ export const getAuth = (user: Omit = testUser) => { } -export const shutdownEngine = async (engine: any) => { +export const shutdownEngine = async () => { await Promise.all([ engine.storage.connection.close(), From 880f7e47526407061d9847081b39779b8cb6ac11 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 15:45:00 -0400 Subject: [PATCH 22/32] chore(e2e): create db collections and user at global setup --- tests/lib/e2e/global-setup.ts | 40 ++++++++++++++++++++++++++++---- tests/lib/e2e/global-teardown.ts | 3 ++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts index af89fddb8..3e3b24339 100644 --- a/tests/lib/e2e/global-setup.ts +++ b/tests/lib/e2e/global-setup.ts @@ -3,18 +3,50 @@ import { MongoClient } from 'mongodb'; declare var globalThis: any export default async () => { + const collections = [ + 'bucketentries', + 'buckets', + 'contacts', + 'exchangereports', + 'frames', + 'fullaudits', + 'marketings', + 'mirrors', + 'partners', + 'pointers', + 'publickeys', + 'referrals', + 'shards', + 'storageevents', + 'tokens', + 'usernonces', + 'users', + ] + loadEnv(); const url = process.env.inxtbridge_storage__mongoUrl; + const user = process.env.inxtbridge_storage__mongoOpts__user; + const password = process.env.inxtbridge_storage__mongoOpts__pass; if (!url) throw new Error('Missing mongo url'); + if (!user) throw new Error('Missing mongo user'); + if (!password) throw new Error('Missing mongo password'); + if (!url.includes('test')) { throw new Error("For caution test database must include test in it's name"); } - const client = new MongoClient(url); + + const urlParts = url.split('/'); + const dbName = urlParts.pop(); + const client = new MongoClient(urlParts.join('/')); + await client.connect(); - const db = client.db(); - const collections = await db.collections(); - await Promise.all(collections.map(collection => collection.deleteMany({}))); + const db = client.db(dbName); + + await db.addUser(user, password, { roles: ['dbOwner'] }).catch(error => console.log(error.message)); + + await Promise.all(collections.map(collection => db.collection(collection).deleteMany({}))); globalThis.mongoClient = client; + globalThis.dbName = dbName; } \ No newline at end of file diff --git a/tests/lib/e2e/global-teardown.ts b/tests/lib/e2e/global-teardown.ts index 3efe4d0d1..465ff50c4 100644 --- a/tests/lib/e2e/global-teardown.ts +++ b/tests/lib/e2e/global-teardown.ts @@ -4,7 +4,8 @@ declare var globalThis: any export default async () => { const client = globalThis.mongoClient as MongoClient; - const db = client.db(); + const dbName = globalThis.dbName as string; + const db = client.db(dbName); const collections = await db.collections(); await Promise.all(collections.map(collection => collection.deleteMany({}))); From aae7c6ced6a4a2090b14e5c511d281fc06bfb7ee Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 15:49:09 -0400 Subject: [PATCH 23/32] test(e2e): add step to run e2e --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e36e23db..053a69655 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,16 +17,25 @@ jobs: node-version: [16.x] env: DATABASE_URI: ${{ secrets.DATABASE_URI }} + inxtbridge_storage__mongoUrl: mongodb://admin:password@127.0.0.1:27017/bridge-test + inxtbridge_storage__mongoOpts__user: admin + inxtbridge_storage__mongoOpts__pass: password steps: - name: Start MongoDB - run: docker run -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=bridge-test -d mongo:5 + run: docker run -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=bridge-test -d mongo:4.4 - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 - run: yarn --ignore-engines + name: Install dependencies + - run: yarn run test + name: Run Tests + + - run: yarn run test:e2e + name: Run E2E Tests # - run: yarn run test-mongo-init # - run: yarn run test-mongo From 7bd137adcb1dc26b2a4b2c90b07c3e7f90d4dca6 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 16:00:01 -0400 Subject: [PATCH 24/32] chore(e2e): test action --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 053a69655..0f7eee0ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,17 +25,24 @@ jobs: run: docker run -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=bridge-test -d mongo:4.4 - uses: actions/checkout@v2 + # - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://npm.pkg.github.com/ - run: yarn --ignore-engines name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - run: yarn run test name: Run Tests - run: yarn run test:e2e name: Run E2E Tests + # # - run: yarn run test-mongo-init # - run: yarn run test-mongo From 9fe05bcdc19a3f9e0188e09c2219c18f214196c3 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 16:20:54 -0400 Subject: [PATCH 25/32] chore(e2e): add needed envs --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f7eee0ff..0f04130cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,46 @@ jobs: inxtbridge_storage__mongoUrl: mongodb://admin:password@127.0.0.1:27017/bridge-test inxtbridge_storage__mongoOpts__user: admin inxtbridge_storage__mongoOpts__pass: password + inxtbridge_server__port: 0 + + inxtbridge_api_keys__segment_test: inxtbridge_api_keys__segment_test + inxtbridge_application__CLUSTER__0: inxtbridge_application__CLUSTER__0 + inxtbridge_logger__level: 5 + inxtbridge_storage__mongoOpts__authSource: inxtbridge_storage__mongoOpts__authSource + inxtbridge_stripe__PK_LIVE: inxtbridge_stripe__PK_LIVE + inxtbridge_api_keys__segment: inxtbridge_api_keys__segment + inxtbridge_stripe__SK_LIVE: inxtbridge_stripe__SK_LIVE + inxtbridge_redis__port: 6379 + inxtbridge_server__ssl__redirect: 443 + inxtbridge_redis__password: + inxtbridge_complex__rpcUser: inxtbridge_complex__rpcUser + inxtbridge_stripe__SIG_TEST: inxtbridge_stripe__SIG_TEST + inxtbridge_gateway__username: inxtbridge_gateway__username + inxtbridge_mailer__auth__pass: inxtbridge_mailer__auth__pass + inxtbridge_stripe__PK_TEST: inxtbridge_stripe__PK_TEST + inxtbridge_complex__rpcPassword: inxtbridge_complex__rpcPassword + inxtbridge_mailer__host: inxtbridge_mailer__host + inxtbridge_server__public__host: inxtbridge_server__public__host + inxtbridge_stripe__SK_TEST: inxtbridge_stripe__SK_TEST + inxtbridge_complex__rpcUrl: inxtbridge_complex__rpcUrl + inxtbridge_mailer__auth__user: inxtbridge_mailer__auth__user + inxtbridge_mailer__sendgrid__api_key: inxtbridge_mailer__sendgrid__api_key + inxtbridge_storage__mongoOpts__server__poolSize: inxtbridge_storage__mongoOpts__server__poolSize + inxtbridge_stripe__SIG: inxtbridge_stripe__SIG + inxtbridge_drive__api: inxtbridge_drive__api + inxtbridge_server__public__port: 443 + inxtbridge_mailer__port: 465 + inxtbridge_gateway__password: inxtbridge_gateway__password + inxtbridge_gateway__JWT_SECRET: inxtbridge_gateway__JWT_SECRET + inxtbridge_QUEUE_HOST: inxtbridge_QUEUE_HOST + inxtbridge_QUEUE_USERNAME: inxtbridge_QUEUE_USERNAME + inxtbridge_QUEUE_PASSWORD: inxtbridge_QUEUE_PASSWORD + steps: - name: Start MongoDB run: docker run -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=bridge-test -d mongo:4.4 - uses: actions/checkout@v2 - # - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: From a7680ed9b1810acc28434f20674de4f7488a7741 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 16:21:22 -0400 Subject: [PATCH 26/32] chore(e2e): remove duplicated check --- tests/lib/e2e/setup.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/lib/e2e/setup.ts b/tests/lib/e2e/setup.ts index 684ff2b6a..808c84be7 100644 --- a/tests/lib/e2e/setup.ts +++ b/tests/lib/e2e/setup.ts @@ -20,9 +20,5 @@ if (process.env.inxtbridge_server__port !== '0') { process.argv = process.argv.slice(0, 2); export const engine = require('../../../bin/storj-bridge.ts'); -if (!engine.storage.connection.options.dbName.includes('test')) { - throw new Error("For caution test database must include test in it's name"); -} - export const testServer = supertest(engine.server.app); From 3cc09114fdc5038a47537d589980923959132bfb Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 11 Jan 2024 16:26:16 -0400 Subject: [PATCH 27/32] chore(e2e): trigger action --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f04130cf..fae9ec5c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: inxtbridge_storage__mongoOpts__user: admin inxtbridge_storage__mongoOpts__pass: password inxtbridge_server__port: 0 - inxtbridge_api_keys__segment_test: inxtbridge_api_keys__segment_test inxtbridge_application__CLUSTER__0: inxtbridge_application__CLUSTER__0 inxtbridge_logger__level: 5 From 9605502c68e476734b01332a5021ff6997bedaf6 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 16 Jan 2024 10:49:26 -0400 Subject: [PATCH 28/32] refactor(e2e): enhance use `expect.toMatchObject` --- tests/lib/e2e/buckets/files.e2e-spec.ts | 199 +++++++++++++++--------- 1 file changed, 125 insertions(+), 74 deletions(-) diff --git a/tests/lib/e2e/buckets/files.e2e-spec.ts b/tests/lib/e2e/buckets/files.e2e-spec.ts index 12242f0dd..1533e2f76 100644 --- a/tests/lib/e2e/buckets/files.e2e-spec.ts +++ b/tests/lib/e2e/buckets/files.e2e-spec.ts @@ -40,7 +40,7 @@ describe('Bridge E2E Tests', () => { axiosGetStub.callsFake(async (url: string) => { if (url.includes('/v2/upload/link')) return { data: { result: FAKE_UPLOAD_URL } } if (url.includes('/v2/upload-multipart/link')) { - const parts = new URL(url).searchParams.get('parts') + const parts = Number(new URL(url).searchParams.get('parts')) return { data: { result: new Array(parts).fill(FAKE_UPLOAD_URL), UploadId: 'fake-id' } } } if (url.includes('/v2/download/link')) return { data: { result: FAKE_DOWNLOAD_URL } } @@ -76,9 +76,11 @@ describe('Bridge E2E Tests', () => { const [upload] = uploads; - expect(upload.url).toBe(FAKE_UPLOAD_URL) - expect(upload.urls).toBeNull(); - expect(upload.uuid).toBeDefined(); + expect(upload).toMatchObject({ + url: FAKE_UPLOAD_URL, + urls: null, + uuid: expect.any(String), + }) }) @@ -104,16 +106,19 @@ describe('Bridge E2E Tests', () => { const [firstUpload, secondUpload] = uploads; - expect(firstUpload.url).toBeNull(); - expect(firstUpload.urls).toBeDefined(); - expect(firstUpload.uuid).toBeDefined(); - expect(firstUpload.UploadId).toBeDefined(); + expect(firstUpload).toMatchObject({ + url: null, + urls: [FAKE_UPLOAD_URL, FAKE_UPLOAD_URL], + uuid: expect.any(String), + UploadId: expect.any(String), + }) - expect(secondUpload.url).toBeDefined(); - expect(secondUpload.url).toBeNull(); - expect(secondUpload.urls).toBeDefined(); - expect(secondUpload.uuid).toBeDefined(); - expect(secondUpload.UploadId).toBeDefined(); + expect(secondUpload).toMatchObject({ + url: null, + urls: [FAKE_UPLOAD_URL, FAKE_UPLOAD_URL], + uuid: expect.any(String), + UploadId: expect.any(String), + }) }) @@ -125,8 +130,8 @@ describe('Bridge E2E Tests', () => { .expect(201) // Act: start the upload - const MB100 = 99 * 1024 * 1024 - const fileParts = [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, },] + const MB99 = 99 * 1024 * 1024 + const fileParts = [{ index: 0, size: MB99 / 2, }, { index: 1, size: MB99 / 2, },] const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=${fileParts.length}`) .set('Authorization', getAuth(testUser)) .send({ uploads: fileParts, }) @@ -138,7 +143,7 @@ describe('Bridge E2E Tests', () => { }) - it('When a user finishes to upload a file, the user can finish the upload with a hash per each part uploaded', async () => { + it('When a user finishes to upload a file with single part upload, the user can finish the upload with a hash for the file', async () => { // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer @@ -166,16 +171,61 @@ describe('Bridge E2E Tests', () => { const body = responseComplete.body; - expect(body.bucket).toEqual(bucketId); - expect(body.created).toBeDefined(); - expect(body.filename).toBeDefined(); - expect(body.id).toBeDefined(); - expect(body.index).toEqual(index); - expect(body.mimetype).toBeDefined(); - expect(body.renewal).toBeDefined(); - expect(body.size).toBeGreaterThan(0); - expect(typeof body.size).toBe('number'); - expect(body.version).toBe(2); + expect(body).toMatchObject({ + bucket: bucketId, + created: expect.any(String), + filename: expect.any(String), + index: index, + id: expect.any(String), + mimetype: 'application/octet-stream', + renewal: expect.any(String), + size: 1000, + version: 2, + }) + + }); + + it('When a user finishes to upload a file with multipart upload, the user can finish the upload with a hash per each part uploaded', async () => { + + // Arrange: Create a bucket + const { body: { id: bucketId } } = await testServer + .post('/buckets') + .set('Authorization', getAuth(testUser)) + + // Arrange: start the upload + const MB100 = 100 * 1024 * 1024 + const response = await testServer.post(`/v2/buckets/${bucketId}/files/start?multiparts=2`) + .set('Authorization', getAuth(testUser)) + .send({ uploads: [{ index: 0, size: MB100 / 2, }, { index: 1, size: MB100 / 2, }] }) + + const { uploads } = response.body; + + // Act: finish the upload + const index = crypto.randomBytes(32).toString('hex'); + const responseComplete = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) + .set('Authorization', getAuth(testUser)) + .send({ + index, + shards: (uploads as any[]).map((upload) => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })), + }); + + // Assert + expect(responseComplete.status).toBe(200); + + const body = responseComplete.body; + + expect(body).toMatchObject({ + bucket: bucketId, + created: expect.any(String), + filename: expect.any(String), + index: index, + id: expect.any(String), + mimetype: 'application/octet-stream', + renewal: expect.any(String), + size: MB100, + version: 2, + }) + }); }) @@ -195,12 +245,10 @@ describe('Bridge E2E Tests', () => { // Arrange: finish the upload const index = crypto.randomBytes(32).toString('hex'); + const uploadedShards = (uploads as any[]).map(upload => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })) const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) .set('Authorization', getAuth(testUser)) - .send({ - index, - shards: (uploads as any[]).map(upload => ({ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, })), - }); + .send({ index, shards: uploadedShards, }); // Act: download the file @@ -211,17 +259,16 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(200); const body = response.body; - expect(body.bucket).toBe(bucketId) - expect(body.created).toBeDefined() - expect(body.index).toBe(index) - expect(body.shards).toHaveLength(2) - - const [firstShard, secondShard] = body.shards - - expect(firstShard.hash).toBeDefined() - expect(firstShard.url).toBeDefined() - expect(secondShard.hash).toBeDefined() - expect(secondShard.url).toBeDefined() + const [firstShard, secondShard] = uploadedShards + expect(body).toMatchObject({ + bucket: bucketId, + created: expect.any(String), + index, + shards: expect.arrayContaining([ + { hash: firstShard.hash, url: expect.any(String), }, + { hash: secondShard.hash, url: expect.any(String), }, + ]) + }) }) @@ -262,7 +309,7 @@ describe('Bridge E2E Tests', () => { }) - it('When a user wants to delete a file, it should work if the file does not exist', async () => { + it('When a user wants to delete a file, it should fail if the file does not exist', async () => { // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer .post('/buckets') @@ -297,12 +344,15 @@ describe('Bridge E2E Tests', () => { const { body } = response - expect(body.bucket).toBeDefined() - expect(body.operation).toBeDefined() - expect(body.expires).toBeDefined() - expect(body.token).toBeDefined() - expect(body.encryptionKey).toBeDefined() - expect(body.id).toBeDefined() + expect(body).toMatchObject({ + bucket: bucketId, + operation: 'PULL', + expires: expect.any(String), + token: expect.any(String), + id: expect.any(String), + encryptionKey: expect.any(String), + }) + }) it('When a user wants to share a file, it should be able to create a token for the bucket passing PULL as operation and an existing file', async () => { @@ -335,17 +385,20 @@ describe('Bridge E2E Tests', () => { const { body } = response - expect(body.bucket).toBeDefined() - expect(body.operation).toBeDefined() - expect(body.expires).toBeDefined() - expect(body.token).toBeDefined() - expect(body.id).toBeDefined() - expect(body.encryptionKey).toBeDefined() - expect(body.mimetype).toBeDefined() - expect(body.size).toBeDefined() + expect(body).toMatchObject({ + bucket: bucketId, + operation: 'PULL', + expires: expect.any(String), + token: expect.any(String), + id: expect.any(String), + encryptionKey: expect.any(String), + mimetype: 'application/octet-stream', + size: 1000, + }) + }) - it('When a user wants to share a file, it should be able to get the file info', async () => { + it('When a user uploads a file, it should able to see the metadata of a file once is uploaded', async () => { // Arrange: Create a bucket const { body: { id: bucketId } } = await testServer @@ -359,11 +412,12 @@ describe('Bridge E2E Tests', () => { // Arrange: finish the upload const index = crypto.randomBytes(32).toString('hex'); + const fileHash = crypto.randomBytes(20).toString('hex') const { body: file } = await testServer.post(`/v2/buckets/${bucketId}/files/finish`) .set('Authorization', getAuth(testUser)) .send({ index, - shards: [{ hash: crypto.randomBytes(20).toString('hex'), uuid: upload.uuid, }], + shards: [{ hash: fileHash, uuid: upload.uuid, }], }); // Act @@ -374,22 +428,19 @@ describe('Bridge E2E Tests', () => { expect(response.status).toBe(200); const { body: fileInfo } = response - expect(fileInfo.bucket).toBeDefined() - expect(fileInfo.index).toBeDefined() - expect(fileInfo.size).toBeDefined() - expect(fileInfo.version).toBeDefined() - expect(fileInfo.created).toBeDefined() - expect(fileInfo.renewal).toBeDefined() - expect(fileInfo.mimetype).toBeDefined() - expect(fileInfo.filename).toBeDefined() - expect(fileInfo.id).toBeDefined() - expect(fileInfo.shards).toBeDefined() - expect(fileInfo.shards).toHaveLength(1) - - const [shard] = fileInfo.shards - expect(shard.index).toBeDefined() - expect(shard.hash).toBeDefined() - expect(shard.url).toBeDefined() + + expect(fileInfo).toMatchObject({ + bucket: bucketId, + created: expect.any(String), + filename: expect.any(String), + index, + id: expect.any(String), + mimetype: 'application/octet-stream', + renewal: expect.any(String), + size: 1000, + version: 2, + shards: [{ index: 0, hash: fileHash, url: expect.any(String), }] + }) }) }) From 3ef2de16670f5314546bdcd7fdcf177edf821878 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Tue, 16 Jan 2024 10:56:47 -0400 Subject: [PATCH 29/32] chore(users): restore `AuthorizedRequest` type --- lib/server/http/users/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server/http/users/controller.ts b/lib/server/http/users/controller.ts index eb050b5c6..1575906e5 100644 --- a/lib/server/http/users/controller.ts +++ b/lib/server/http/users/controller.ts @@ -8,7 +8,7 @@ import { UsersUsecase } from '../../../core'; -type AuthorizedRequest = Request & { user: { _id: string, email: string } }; +type AuthorizedRequest = Request & { user: { _id: string } }; export class HTTPUsersController { constructor( From 0324cedd70f96d168bac3a67f9bd90276604a776 Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 18 Jan 2024 14:05:39 -0400 Subject: [PATCH 30/32] chore(e2e): use env dbName instead of url --- tests/lib/e2e/global-setup.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts index 3e3b24339..64202c74e 100644 --- a/tests/lib/e2e/global-setup.ts +++ b/tests/lib/e2e/global-setup.ts @@ -27,17 +27,17 @@ export default async () => { const url = process.env.inxtbridge_storage__mongoUrl; const user = process.env.inxtbridge_storage__mongoOpts__user; const password = process.env.inxtbridge_storage__mongoOpts__pass; + const dbName = process.env.inxtbridge_storage__mongoOpts__dbName; if (!url) throw new Error('Missing mongo url'); if (!user) throw new Error('Missing mongo user'); if (!password) throw new Error('Missing mongo password'); + if (!dbName) throw new Error('Missing mongo dbName'); - if (!url.includes('test')) { + if (!url.includes('test') || !dbName.includes('test')) { throw new Error("For caution test database must include test in it's name"); } - const urlParts = url.split('/'); - const dbName = urlParts.pop(); - const client = new MongoClient(urlParts.join('/')); + const client = new MongoClient(url); await client.connect(); From 90b9a860eb727392cb079e62ea2b59d0a8ebb6ab Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 18 Jan 2024 14:41:33 -0400 Subject: [PATCH 31/32] fix(e2e): ci actions --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fae9ec5c6..bbed5391e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: inxtbridge_storage__mongoUrl: mongodb://admin:password@127.0.0.1:27017/bridge-test inxtbridge_storage__mongoOpts__user: admin inxtbridge_storage__mongoOpts__pass: password + inxtbridge_storage__mongoOpts__dbName: bridge-test inxtbridge_server__port: 0 inxtbridge_api_keys__segment_test: inxtbridge_api_keys__segment_test inxtbridge_application__CLUSTER__0: inxtbridge_application__CLUSTER__0 From 4b7c79186fa724f4df123df8b7748a8dec187e9a Mon Sep 17 00:00:00 2001 From: sw-wayner Date: Thu, 18 Jan 2024 14:52:00 -0400 Subject: [PATCH 32/32] fix(e2e): global setup --- tests/lib/e2e/global-setup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/lib/e2e/global-setup.ts b/tests/lib/e2e/global-setup.ts index 64202c74e..2e56796c9 100644 --- a/tests/lib/e2e/global-setup.ts +++ b/tests/lib/e2e/global-setup.ts @@ -37,7 +37,8 @@ export default async () => { throw new Error("For caution test database must include test in it's name"); } - const client = new MongoClient(url); + const baseUrl = new URL(url); + const client = new MongoClient(baseUrl.toString().replace(baseUrl.pathname, '')); await client.connect();