diff --git a/.eslintrc.js b/.eslintrc.js index 8f5aedb71..af1ad33a1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js'], rules: { + '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': "^_" }], '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/.github/workflows/run-tests-e2e.yml b/.github/workflows/run-tests-e2e.yml index 5e24323bf..6df343e87 100644 --- a/.github/workflows/run-tests-e2e.yml +++ b/.github/workflows/run-tests-e2e.yml @@ -27,7 +27,7 @@ jobs: APP_SEGMENT_KEY: ${{ secrets.APP_SEGMENT_KEY }} strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - name: Checkout repo uses: actions/checkout@v3 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2cb424929..aa5a38637 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,61 +1,49 @@ -# name: run-tests -# on: -# push: -# branches: -# - 'master' -# - 'develop' -# pull_request: -# branches: -# - 'master' -# - 'develop' -# jobs: -# run-tests: -# environment: test -# runs-on: ubuntu-latest -# strategy: -# matrix: -# node-version: [16.x] -# services: -# postgres: -# image: postgres:latest -# env: -# POSTGRES_DB: xCloud_test -# POSTGRES_PASSWORD: example -# POSTGRES_USER: postgres -# ports: -# - 5432:5432 -# options: >- -# --health-cmd pg_isready -# --health-interval 10s -# --health-timeout 5s -# --health-retries 5 +name: Run tests +on: + push: + branches: + - 'master' + - 'develop' + pull_request: + branches: + - 'master' + - 'develop' +jobs: + run-tests: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + environment: + name: development + strategy: + matrix: + node-version: [20.x] + steps: + - name: Checkout repo + uses: actions/checkout@v3 -# steps: -# - uses: actions/checkout@v2 -# - name: Use Node.js ${{ matrix.node-version }} -# uses: actions/setup-node@v1 -# with: -# node-version: ${{ matrix.node-version }} -# registry-url: 'https://npm.pkg.github.com' + - 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: echo "registry=https://registry.yarnpkg.com/" > .npmrc -# - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc -# # You cannot read packages from other private repos with GITHUB_TOKEN -# # You have to use a PAT instead https://github.com/actions/setup-node/issues/49 -# - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc -# - run: echo "always-auth=true" >> .npmrc + # We should see why permissions field is not doing the job. This should be removed + - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc + - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc + # You cannot read packages from other private repos with GITHUB_TOKEN + # You have to use a PAT instead https://github.com/actions/setup-node/issues/49 + - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc + - run: echo "always-auth=true" >> .npmrc -# - name: Lint & Test -# run: yarn && yarn run test + - name: Setup environment + uses: isbang/compose-action@v1.4.1 + with: + compose-file: './infrastructure/docker-compose.yml' -# - name: Test E2E -# env: -# RDS_HOSTNAME: localhost -# RDS_DBNAME: xCloud_test -# RDS_USERNAME: postgres -# RDS_PASSWORD: example -# RDS_PORT: 5432 -# SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }} -# SENDGRID_MODE_SANDBOX: true -# APP_SEGMENT_KEY: ${{ secrets.APP_SEGMENT_KEY }} -# run: yarn migrate:test && yarn run test:e2e + - name: Install dependencies + run: yarn + + - name: Run unit tests + run: yarn test diff --git a/infrastructure/development.Dockerfile b/infrastructure/development.Dockerfile index 9be034678..e548b235d 100644 --- a/infrastructure/development.Dockerfile +++ b/infrastructure/development.Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine +FROM node:20.10-alpine WORKDIR /usr/app diff --git a/seeders/20230308180046-test-users.js b/seeders/20230308180046-test-users.js index 32ef283d3..f9914523e 100644 --- a/seeders/20230308180046-test-users.js +++ b/seeders/20230308180046-test-users.js @@ -42,7 +42,7 @@ const referredTestUser = { }; module.exports = { - async up(queryInterface, Sequelize) { + async up(queryInterface) { /** * Add seed commands here. * @@ -69,7 +69,7 @@ module.exports = { } }, - async down(queryInterface, Sequelize) { + async down(queryInterface) { /** * Add commands to revert seed here. * diff --git a/seeders/20230607000000-create-folders.js b/seeders/20230607000000-create-folders.js index cf553a753..fcca78647 100644 --- a/seeders/20230607000000-create-folders.js +++ b/seeders/20230607000000-create-folders.js @@ -3,7 +3,14 @@ const { v4 } = require('uuid'); const { Op, Sequelize } = require('sequelize'); -let folderOneUUID, folderTwoUUID; +const foldersNames = [ + 'FolderOne', + 'FolderTwo', + 'FolderFather', + 'RemovedFolder', + 'DeletedFolder', + 'NormalFolder', +]; module.exports = { async up(queryInterface) { @@ -22,14 +29,18 @@ module.exports = { const existingFolders = await queryInterface.sequelize.query( `SELECT * FROM folders WHERE name IN (:names)`, { - replacements: { names: ['FolderOne', 'FolderTwo'] }, + replacements: { + names: foldersNames, + }, type: Sequelize.QueryTypes.SELECT, }, ); - if (existingFolders.length > 0) { + if (existingFolders.length === foldersNames.length) { console.log( - 'Folders with the names "FolderOne" and/or "FolderTwo" already exist. Skipping creation.', + `Folders with the names ${foldersNames.join( + ',', + )} already exist. Skipping creation.`, ); return; } @@ -48,8 +59,6 @@ module.exports = { updated_at: new Date(), }; - folderOneUUID = folderOne.uuid; - const folderTwo = { parent_id: null, name: 'FolderTwo', @@ -64,18 +73,118 @@ module.exports = { updated_at: new Date(), }; - folderTwoUUID = folderTwo.uuid; + const folderFather = createFolderObject( + 'FolderFather', + 'bucketFather', + users[0].id, + null, + ); + + await queryInterface.bulkInsert('folders', [folderFather]); - await queryInterface.bulkInsert('folders', [folderOne, folderTwo]); + const fatherFolderSaved = await queryInterface.sequelize.query( + `SELECT * FROM folders WHERE uuid = :uuid`, + { + replacements: { uuid: folderFather.uuid }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + const deletedFolder = createDeletedFolderObject( + 'DeletedFolder', + 'bucketThree', + users[0].id, + fatherFolderSaved[0].id, + fatherFolderSaved[0].uuid, + ); + + const removedFolder = createRemovedFolderObject( + 'RemovedFolder', + 'bucketFour', + users[0].id, + fatherFolderSaved[0].id, + fatherFolderSaved[0].uuid, + ); + + const normalFolder = createFolderObject( + 'NormalFolder', + 'bucketFive', + users[0].id, + fatherFolderSaved[0].id, + fatherFolderSaved[0].uuid, + ); + + await queryInterface.bulkInsert('folders', [ + folderOne, + folderTwo, + removedFolder, + deletedFolder, + normalFolder, + ]); }, - async down(queryInterface, Sequelize) { + async down(queryInterface) { await queryInterface.bulkDelete( 'folders', { - uuid: { [Op.in]: [folderOneUUID, folderTwoUUID] }, + name: { + [Op.in]: foldersNames, + }, }, {}, ); }, }; + +function createFolderObject(name, bucket, userId, parentId, parentUUID) { + return { + parent_id: parentId || null, + parent_uuid: parentUUID || null, + name, + bucket, + user_id: userId, + uuid: v4(), + plain_name: name, + encrypt_version: '1.0', + deleted: false, + removed: false, + created_at: new Date(), + updated_at: new Date(), + }; +} + +function createDeletedFolderObject(name, bucket, userId, parentId, parentUUID) { + return { + parent_id: parentId, + parent_uuid: parentUUID, + name, + bucket, + user_id: userId, + uuid: v4(), + plain_name: name, + encrypt_version: '1.0', + deleted: true, + removed: false, + deleted_at: new Date(), + created_at: new Date(), + updated_at: new Date(), + }; +} + +function createRemovedFolderObject(name, bucket, userId, parentId, parentUUID) { + return { + parent_id: parentId, + parent_uuid: parentUUID, + name, + bucket, + user_id: userId, + uuid: v4(), + plain_name: name, + encrypt_version: '1.0', + deleted: false, + removed: true, + removed_at: new Date(), + created_at: new Date(), + updated_at: new Date(), + }; +} diff --git a/seeders/20230614000000-create-share.js b/seeders/20230614000000-create-share.js index f29bb92cb..2f3ec0fed 100644 --- a/seeders/20230614000000-create-share.js +++ b/seeders/20230614000000-create-share.js @@ -1,7 +1,6 @@ 'use strict'; const { v4 } = require('uuid'); -const { Op } = require('sequelize'); module.exports = { async up(queryInterface, Sequelize) { @@ -14,9 +13,17 @@ module.exports = { }, ); + const folders = await queryInterface.sequelize.query( + 'SELECT * FROM folders WHERE user_id IN (:usersIds)', + { + replacements: { usersIds: [users[0].id, users[1].id] }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + const shareOne = { id: 1, - folder_id: 1, + folder_id: folders[0].id, user_id: users[0].id, bucket: 'bucketOne', file_token: v4(), @@ -38,6 +45,6 @@ module.exports = { }, async down(queryInterface) { - await queryInterface.bulkDelete('shares', null, {}); + await queryInterface.bulkDelete('shares', { id: 1 }, {}); }, }; diff --git a/seeders/20230618000000-create-private-folder.js b/seeders/20230618000000-create-private-folder.js index 3b820b726..72fa691fd 100644 --- a/seeders/20230618000000-create-private-folder.js +++ b/seeders/20230618000000-create-private-folder.js @@ -1,9 +1,9 @@ 'use strict'; -const { v4 } = require('uuid'); -const { Op, Sequelize } = require('sequelize'); +const { Sequelize, Op } = require('sequelize'); -let sharingFolderOneId, sharingFolderTwoId; +const uuid = 'd290f1ee-6c54-4b01-90e6-d701748f0851'; +const uuid2 = 'd290f1ee-6c54-4b01-90e6-d701748f0852'; module.exports = { async up(queryInterface) { @@ -31,7 +31,7 @@ module.exports = { const folderTwo = folders.find((f) => f.name === 'FolderTwo'); const sharingFolderOne = { - id: v4(), + id: uuid, folder_id: folderOne.uuid, owner_id: users[0].uuid, shared_with: users[1].uuid, @@ -40,10 +40,8 @@ module.exports = { updated_at: new Date(), }; - sharingFolderOneId = sharingFolderOne.id; - const sharingFolderTwo = { - id: v4(), + id: uuid2, folder_id: folderTwo.uuid, owner_id: users[1].uuid, shared_with: users[0].uuid, @@ -52,8 +50,6 @@ module.exports = { updated_at: new Date(), }; - sharingFolderTwoId = sharingFolderTwo.id; - await queryInterface.bulkInsert('private_sharing_folder', [ sharingFolderOne, sharingFolderTwo, @@ -64,7 +60,9 @@ module.exports = { await queryInterface.bulkDelete( 'private_sharing_folder', { - id: { [Op.in]: [sharingFolderOneId, sharingFolderTwoId] }, + id: { + [Op.in]: [uuid, uuid2], + }, }, {}, ); diff --git a/seeders/20230620032601-roles.js b/seeders/20230620032601-roles.js index 91a072c5e..9540e2f8b 100644 --- a/seeders/20230620032601-roles.js +++ b/seeders/20230620032601-roles.js @@ -1,4 +1,7 @@ -const uuidv4 = require('uuid').v4; +const { Op } = require('sequelize'); + +const uuid = 'd290f1ee-6c54-4b01-90e6-d801748f0851'; +const uuid2 = 'd290f1ee-6c54-4b01-90e6-d801748f0852'; module.exports = { up: async (queryInterface) => { @@ -9,13 +12,13 @@ module.exports = { if (roles.length === 0) { await queryInterface.bulkInsert('roles', [ { - id: uuidv4(), + id: uuid, name: 'role_1', created_at: new Date(), updated_at: new Date(), }, { - id: uuidv4(), + id: uuid2, name: 'role_2', created_at: new Date(), updated_at: new Date(), @@ -25,6 +28,14 @@ module.exports = { }, down: async (queryInterface) => { - await queryInterface.bulkDelete('roles', null, {}); + await queryInterface.bulkDelete( + 'roles', + { + id: { + [Op.in]: [uuid, uuid2], + }, + }, + {}, + ); }, -}; \ No newline at end of file +}; diff --git a/seeders/20230620032717-permissions.js b/seeders/20230620032717-permissions.js index 6303dda73..df3dee4ba 100644 --- a/seeders/20230620032717-permissions.js +++ b/seeders/20230620032717-permissions.js @@ -10,9 +10,12 @@ module.exports = { throw new Error('No roles found'); } - const existingPermissions = await queryInterface.sequelize.query(`SELECT * FROM permissions`, { - type: queryInterface.sequelize.QueryTypes.SELECT, - }); + const existingPermissions = await queryInterface.sequelize.query( + `SELECT * FROM permissions`, + { + type: queryInterface.sequelize.QueryTypes.SELECT, + }, + ); if (existingPermissions.length === 0) { const permissions = []; @@ -36,4 +39,4 @@ module.exports = { down: async (queryInterface) => { await queryInterface.bulkDelete('permissions', null, {}); }, -}; \ No newline at end of file +}; diff --git a/seeders/20231010210008-create-files.js b/seeders/20231010210008-create-files.js new file mode 100644 index 000000000..79772a381 --- /dev/null +++ b/seeders/20231010210008-create-files.js @@ -0,0 +1,70 @@ +'use strict'; + +const { v4 } = require('uuid'); +const { Op, Sequelize } = require('sequelize'); + +module.exports = { + async up(queryInterface) { + const users = await queryInterface.sequelize.query( + `SELECT * FROM users WHERE email IN (:emails)`, + { + replacements: { emails: ['john@doe.com', 'johnTwo@doe.com'] }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + const folders = await queryInterface.sequelize.query( + `SELECT * FROM folders WHERE name = :name`, + { + replacements: { + name: ['NormalFolder'], + }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + const folder = folders[0]; + + if (!users || users.length !== 2) { + throw new Error('No users found'); + } + + const filesData = [ + { + uuid: v4(), + file_id: 'file1', + name: 'File 1', + plain_name: 'file1.txt', + type: 'text/plain', + size: 1024, + bucket: 'bucket1', + folder_id: folder.id, + folder_uuid: folder.uuid, + encrypt_version: '1.0', + user_id: users[0].id, + modification_time: new Date(), + created_at: new Date(), + updated_at: new Date(), + removed: false, + removed_at: null, + deleted: false, + deleted_at: null, + status: 'EXISTS', + }, + ]; + + await queryInterface.bulkInsert('files', filesData); + }, + + async down(queryInterface) { + await queryInterface.bulkDelete( + 'files', + { + file_id: { + [Op.in]: ['file1'], + }, + }, + {}, + ); + }, +}; diff --git a/src/externals/bridge/bridge.service.spec.ts b/src/externals/bridge/bridge.service.spec.ts index aaedac07d..3d3371314 100644 --- a/src/externals/bridge/bridge.service.spec.ts +++ b/src/externals/bridge/bridge.service.spec.ts @@ -5,7 +5,7 @@ import { CryptoService } from '../crypto/crypto.service'; import { HttpClientModule } from '../http/http.module'; import { HttpClient } from '../http/http.service'; import { BridgeService } from './bridge.service'; -import { AxiosResponse } from 'axios'; +import { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import { CryptoModule } from '../crypto/crypto.module'; describe('Bridge Service', () => { @@ -63,7 +63,7 @@ describe('Bridge Service', () => { data: null, status: 200, headers: {}, - config: {}, + config: {} as InternalAxiosRequestConfig, statusText: 'OK', }; jest.spyOn(configService, 'get').mockReturnValue(testUrl); diff --git a/src/externals/notifications/events/event.ts b/src/externals/notifications/events/event.ts index 314ef2295..fba46e63e 100644 --- a/src/externals/notifications/events/event.ts +++ b/src/externals/notifications/events/event.ts @@ -1,6 +1,9 @@ export class Event { private createdAt: Date; - constructor(public name: string, public payload: Record) { + constructor( + public name: string, + public payload: Record, + ) { this.createdAt = new Date(); } } diff --git a/src/modules/app-sumo/app-sumo.usecase.spec.ts b/src/modules/app-sumo/app-sumo.usecase.spec.ts index 60417d56e..b70dbffb9 100644 --- a/src/modules/app-sumo/app-sumo.usecase.spec.ts +++ b/src/modules/app-sumo/app-sumo.usecase.spec.ts @@ -49,17 +49,20 @@ describe('AppSumoUseCase', () => { it('should not set appSumo.planId to "unlimited" when a plan is found', async () => { const userId = 1; - planRepository.getOneBy.mockRejectedValueOnce(PlanNotFoundException); - - const result = await useCase.getByUserId(userId); - - expect(result.planId).not.toEqual('unlimited'); + planRepository.getOneBy.mockRejectedValueOnce(new PlanNotFoundException()); + try { + await useCase.getByUserId(userId); + } catch (error) { + expect(error).toBeInstanceOf(PlanNotFoundException); + } }); it('should log an error message and rethrow when PlanNotFoundException is caught', async () => { const userId = 1; - planRepository.getOneBy.mockRejectedValueOnce(PlanNotFoundException); + planRepository.getOneBy.mockRejectedValueOnce( + new PlanNotFoundException('Plan not found'), + ); const errorLogSpy = jest.spyOn(useCase['logger'], 'log'); diff --git a/src/modules/file/file.controller.ts b/src/modules/file/file.controller.ts index 94ff7e76d..36af5170b 100644 --- a/src/modules/file/file.controller.ts +++ b/src/modules/file/file.controller.ts @@ -112,7 +112,7 @@ export class FileController { @UserDecorator() user: User, @Query('limit') limit: number, @Query('offset') offset: number, - @Query('status') status: typeof filesStatuses[number], + @Query('status') status: (typeof filesStatuses)[number], @Query('sort') sort?: string, @Query('order') order?: 'ASC' | 'DESC', @Query('updatedAt') updatedAt?: string, diff --git a/src/modules/file/file.usecase.spec.ts b/src/modules/file/file.usecase.spec.ts index def4df881..b66dc32c6 100644 --- a/src/modules/file/file.usecase.spec.ts +++ b/src/modules/file/file.usecase.spec.ts @@ -487,15 +487,15 @@ describe('FileUseCases', () => { folderId, }); - const { user, ...expectedFiles } = fileAttributes; + delete fileAttributes['user']; const result = service.decrypFileName(file); expect(result).toStrictEqual({ - ...expectedFiles, - name: fileAttributes['name'], + ...fileAttributes, shares: undefined, thumbnails: undefined, + sharings: undefined, folderId, }); }); diff --git a/src/modules/folder/folder.controller.ts b/src/modules/folder/folder.controller.ts index 81d8ae4ac..b585408c3 100644 --- a/src/modules/folder/folder.controller.ts +++ b/src/modules/folder/folder.controller.ts @@ -255,7 +255,7 @@ export class FolderController { @UserDecorator() user: User, @Query('limit') limit: number, @Query('offset') offset: number, - @Query('status') status: typeof foldersStatuses[number], + @Query('status') status: (typeof foldersStatuses)[number], @Query('updatedAt') updatedAt?: string, ) { if (!status) { diff --git a/src/modules/folder/folder.e2e.ts b/src/modules/folder/folder.e2e.ts index 459df8786..584faa6c8 100644 --- a/src/modules/folder/folder.e2e.ts +++ b/src/modules/folder/folder.e2e.ts @@ -10,46 +10,17 @@ import { BadRequestInvalidOffsetException, BadRequestOutOfRangeLimitException, } from './folder.controller'; -import { User } from '../user/user.domain'; -import { v4 } from 'uuid'; -import { generateJWT } from '../../lib/jwt'; import getEnv from '../../config/configuration'; - -const user = new User({ - id: 1, - email: '', - password: '', - name: '', - lastname: '', - username: '', - bridgeUser: '', - mnemonic: '', - rootFolderId: 1, - hKey: Buffer.from(''), - userId: '', - secret_2FA: '', - errorLoginCount: 0, - isEmailActivitySended: 0, - referralCode: '', - referrer: '', - syncDate: new Date(), - uuid: v4(), - lastResend: new Date(), - credit: 0, - welcomePack: false, - registerCompleted: false, - backupsBucket: '', - sharedWorkspace: false, - tempKey: '', - avatar: '', -}); +import { sequelizeTest } from '../../../test/__e2e__/sequelize'; +import { UserTestRepository } from '../../../test/__e2e__/repositories/users-test.repository'; +import { FolderTestRepository } from '../../../test/__e2e__/repositories/folders-test.repository'; +import { v4 } from 'uuid'; const wrongFolderIdException = new BadRequestWrongFolderIdException(); const wrongOffsetOrLimitException = new BadRequestWrongOffsetOrLimitException(); const invalidOffsetException = new BadRequestInvalidOffsetException(); const outOfRangeLimitException = new BadRequestOutOfRangeLimitException(); -const existentFolderId = 2; const invalidFolderId = 0; const notAFolderId = 'invalidFolderId'; const invalidLimit = 51; @@ -58,32 +29,319 @@ const validOffset = 0; describe('Folder module', () => { let app: INestApplication; - - function getToken(): string { - return generateJWT(user.toJSON(), '5m', getEnv().secrets.jwt); - } + let updatedAfter: string; + let user: any; + let userToken: string; + let folders: any[]; + let existentFolderId: number; + let existentFolderUUID: string; + let folderTestRepository: FolderTestRepository; + let userTestRerpository: UserTestRepository; beforeAll(async () => { jest.resetModules(); + + folderTestRepository = new FolderTestRepository(sequelizeTest); + userTestRerpository = new UserTestRepository(sequelizeTest); + + await sequelizeTest.authenticate(); + await sequelizeTest.sync(); + user = await userTestRerpository.getPrincipalUser(); + userToken = userTestRerpository.generateToken(user, getEnv().secrets.jwt); + + folders = await folderTestRepository.getFoldersByUserId(user.id); + existentFolderId = folders[0].id; + existentFolderUUID = folders[0].uuid; + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe()); app.useGlobalInterceptors(new TransformInterceptor()); + + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + updatedAfter = date.toISOString(); + await app.init(); }); afterAll(async () => { await app.close(); + await sequelizeTest.close(); + }); + + describe('GET /folders - Get Folders', () => { + it('should get all folders when the status is ALL', async () => { + const res = await request(app.getHttpServer()) + .get(`/folders?status=ALL&limit=10&offset=0&updatedAt=${updatedAfter}`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + expect(Array.isArray(res.body)).toBeTruthy(); + }); + + it('should get all exists folders when the status is EXISTS', async () => { + const res = await request(app.getHttpServer()) + .get( + `/folders?status=EXISTS&limit=10&offset=0&updatedAt=${updatedAfter}`, + ) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + const statuses = res.body.map((f) => f.status) as string[]; + const everyStatusAreExists = statuses.every( + (status) => status === 'EXISTS', + ); + + if (!res.body.length) { + throw Error('No folders found'); + } + + expect(everyStatusAreExists).toBeTruthy(); + }); + + it('should get all trashed folders when the status is TRASHED', async () => { + const res = await request(app.getHttpServer()) + .get( + `/folders?status=TRASHED&limit=10&offset=0&updatedAt=${updatedAfter}`, + ) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + const statuses = res.body.map((f) => f.status) as string[]; + const everyStatusAreTrashed = statuses.every( + (status) => status === 'TRASHED', + ); + + if (!res.body.length) { + throw Error('No folders found'); + } + + expect(everyStatusAreTrashed).toBeTruthy(); + }); + + it('should get all deleted folders when the status is DELETED', async () => { + const res = await request(app.getHttpServer()) + .get( + `/folders?status=DELETED&limit=10&offset=0&updatedAt=${updatedAfter}`, + ) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + const statuses = res.body.map((f) => f.status) as string[]; + const everyStatusAreDeleted = statuses.every( + (status) => status === 'DELETED', + ); + + if (!res.body.length) { + throw Error('No folders found'); + } + + expect(everyStatusAreDeleted).toBeTruthy(); + }); + + describe('Exceptions', () => { + it('should be a bad request when the status is not ALL, EXISTS, TRASHED or DELETED', async () => { + const response = await request(app.getHttpServer()) + .get('/folders?status=INVALID_STATUS&limit=10&offset=1') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(`Unknown status "INVALID_STATUS"`); + }); + + it('should be a bad request when the offset is negative', async () => { + const offset = -1; + const response = await request(app.getHttpServer()) + .get(`/folders?status=ALL&limit=10&offset=${offset}`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(invalidOffsetException.message); + }); + + it('should be a bad request when the limit is less than 1', async () => { + const limit = 0; + const response = await request(app.getHttpServer()) + .get(`/folders?status=ALL&offset=1&limit=${limit}`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(outOfRangeLimitException.message); + }); + + it('should be a bad request when the limit is greater than 50', async () => { + const response = await request(app.getHttpServer()) + .get(`/folders?status=ALL&offset=1&limit=51`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(outOfRangeLimitException.message); + }); + + it('should be a bad request when the status is not provided', async () => { + const response = await request(app.getHttpServer()) + .get('/folders?limit=10&offset=1') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Missing "status" query param'); + }); + + it('should be a bad request when the limit is not provided', async () => { + const response = await request(app.getHttpServer()) + .get('/folders?status=ALL&offset=1') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(`Missing "offset" or "limit" param`); + }); + + it('should be a bad request when the offset is not provided', async () => { + const response = await request(app.getHttpServer()) + .get('/folders?status=ALL&limit=10') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe(`Missing "offset" or "limit" param`); + }); + }); + }); + + describe('Delete /folders - Delete Folders', () => { + // TODO: fix this test in the pipeline, in local it runs fine + it.skip('should delete all orphan folders', async () => { + const uuid = v4(); + + await folderTestRepository.createOrphan(uuid, user.id); + + await request(app.getHttpServer()) + .delete(`/folders?status=orphan`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + const folder = await folderTestRepository.getBy('uuid', uuid); + + expect(folder).toBeUndefined(); + }); + + describe('Exceptions', () => { + it('should be a not implemented exception', async () => { + const response = await request(app.getHttpServer()) + .delete(`/folders?status=trashed`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.NOT_IMPLEMENTED); + + expect(response.body.message).toBe('Not Implemented'); + }); + + it('should be a bad request when the status is not provided', async () => { + const response = await request(app.getHttpServer()) + .delete('/folders') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Bad Request'); + }); + + it('should be a bad request when the status is not valid', async () => { + const response = await request(app.getHttpServer()) + .delete('/folders?status=invalid-status') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Bad Request'); + }); + }); + }); + + describe('GET /folders/:id/metadata - Get Folder by id', () => { + it('should get the folder', async () => { + const res = await request(app.getHttpServer()) + .get(`/folders/${existentFolderId}/metadata`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + expect(res.body.id).toBe(existentFolderId); + }); + + describe('Exceptions', () => { + it('should be a bad request when the folder id is lower than 1', async () => { + const response = await request(app.getHttpServer()) + .get(`/folders/${-1}/metadata`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Invalid id provided'); + }); + }); + }); + + describe('GET /folders/:uuid/meta - Get Folder by uuid', () => { + it('should get the folder', async () => { + const res = await request(app.getHttpServer()) + .get(`/folders/${existentFolderUUID}/meta`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + expect(res.body.id).toBe(existentFolderId); + }); + + describe('Exceptions', () => { + it('should be a bad request when the folder uuid is invalid', async () => { + const response = await request(app.getHttpServer()) + .get(`/folders/invalid_uuid/meta`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Invalid UUID provided'); + }); + + it('should be a not found when the folder uuid does not exist', async () => { + const response = await request(app.getHttpServer()) + .get(`/folders/00000000-0000-0000-0000-000000000000/meta`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.NOT_FOUND); + + expect(response.body.message).toBe('Not Found'); + }); + }); + }); + + describe('GET /folders/count - Get Folders count', () => { + describe('Exceptions', () => { + it('should be a bad request when the status is not orphan, trashed', async () => { + const response = await request(app.getHttpServer()) + .get('/folders/count?status=INVALID_STATUS') + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.BAD_REQUEST); + + expect(response.body.message).toBe('Bad Request'); + }); + }); }); describe('GET /folders/:folderId/files - Gets folder files', () => { - describe('Fails with invalid query params', () => { + it('should get the files by folder id', async () => { + const normalFolder = folders.find( + (folder) => folder.name === 'NormalFolder', + ); + + const response = await request(app.getHttpServer()) + .get(`/folders/${normalFolder.id}/files?limit=10&offset=0`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + expect(response.body.result.length).toBeGreaterThan(0); + }); + + describe('Exceptions', () => { it('When folder id is lower than 1', async () => { const response = await request(app.getHttpServer()) .get(`/folders/${invalidFolderId}/files`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -93,7 +351,7 @@ describe('Folder module', () => { it('When folder id is not a number', async () => { const response = await request(app.getHttpServer()) .get(`/folders/${notAFolderId}/files`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -104,7 +362,7 @@ describe('Folder module', () => { const limit = 'nonValidLimit'; const response = await request(app.getHttpServer()) .get(`/folders/${existentFolderId}/files?limit=${limit}`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -117,7 +375,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/files?limit=${validLimit}&offset=${offset}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -130,7 +388,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/files?limit=${validLimit}&offset=${offset}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -143,7 +401,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/files?offset=${validOffset}&limit=${limit}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -155,7 +413,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/files?offset=${validOffset}&limit=${invalidLimit}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -165,11 +423,22 @@ describe('Folder module', () => { }); describe('GET /folders/:folderId/folders - Gets folder children folders', () => { - describe('Fails with invalid query params', () => { + it('should get the folder children folders', async () => { + const folderFather = folders.find((item) => item.name === 'FolderFather'); + + const response = await request(app.getHttpServer()) + .get(`/folders/${folderFather.id}/folders?limit=10&offset=0`) + .set('Authorization', 'Bearer ' + userToken) + .expect(HttpStatus.OK); + + expect(response.body.result.length).toBeGreaterThan(0); + }); + + describe('Exceptions', () => { it('When folder id is lower than 1', async () => { const response = await request(app.getHttpServer()) .get(`/folders/${invalidFolderId}/folders`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -179,7 +448,7 @@ describe('Folder module', () => { it('When folder id is not a number', async () => { const response = await request(app.getHttpServer()) .get(`/folders/${notAFolderId}/folders`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -190,7 +459,7 @@ describe('Folder module', () => { const limit = 'nonValidLimit'; const response = await request(app.getHttpServer()) .get(`/folders/${existentFolderId}/folders?limit=${limit}`) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -203,7 +472,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/folders?limit=${validLimit}&offset=${offset}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -216,7 +485,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/folders?limit=${validLimit}&offset=${offset}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -229,7 +498,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/folders?offset=${validOffset}&limit=${limit}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); @@ -241,7 +510,7 @@ describe('Folder module', () => { .get( `/folders/${existentFolderId}/folders?offset=${validOffset}&limit=${invalidLimit}`, ) - .set('Authorization', 'bearer ' + getToken()) + .set('Authorization', 'Bearer ' + userToken) .expect(HttpStatus.BAD_REQUEST); expect(response.status).toBe(HttpStatus.BAD_REQUEST); diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts index 825a4d058..db8a4602c 100644 --- a/src/modules/folder/folder.repository.ts +++ b/src/modules/folder/folder.repository.ts @@ -439,7 +439,7 @@ export class SequelizeFolderRepository implements FolderRepository { [Op.not]: null, }, }, - order: additionalOrders, + order: process.env.NODE_ENV === 'test' ? null : additionalOrders, limit, offset, }); diff --git a/src/modules/folder/folder.usecase.spec.ts b/src/modules/folder/folder.usecase.spec.ts index b379dff1e..24ddb79f4 100644 --- a/src/modules/folder/folder.usecase.spec.ts +++ b/src/modules/folder/folder.usecase.spec.ts @@ -133,7 +133,13 @@ describe('FolderUseCases', () => { removed: false, removedAt: null, }); - jest.spyOn(folderRepository, 'findById').mockResolvedValue(mockFolder); + jest + .spyOn(folderRepository, 'findById') + .mockResolvedValueOnce(mockFolder); + jest + .spyOn(cryptoService, 'decryptName') + .mockResolvedValueOnce('a descrypt name' as never); + const findDeletedFolders: FolderOptions['deleted'] = false; const result = await service.getFolder(folderId, { deleted: findDeletedFolders, @@ -466,7 +472,7 @@ describe('FolderUseCases', () => { }; delete expectedResult.parentId; - expect(result).toStrictEqual(expectedResult); + expect(result).toStrictEqual({ ...expectedResult, sharings: undefined }); }); it('fails when the folder name is not encrypted', () => { diff --git a/src/modules/folder/folder.usecase.ts b/src/modules/folder/folder.usecase.ts index fc5568b22..4ce9d8219 100644 --- a/src/modules/folder/folder.usecase.ts +++ b/src/modules/folder/folder.usecase.ts @@ -498,9 +498,8 @@ export class FolderUseCases { } async deleteOrphansFolders(userId: UserAttributes['id']): Promise { - let remainingFolders = await this.folderRepository.clearOrphansFolders( - userId, - ); + let remainingFolders = + await this.folderRepository.clearOrphansFolders(userId); if (remainingFolders > 0) { remainingFolders += await this.deleteOrphansFolders(userId); diff --git a/src/modules/fuzzy-search/look-up.domain.ts b/src/modules/fuzzy-search/look-up.domain.ts index be87ecc25..d35857c33 100644 --- a/src/modules/fuzzy-search/look-up.domain.ts +++ b/src/modules/fuzzy-search/look-up.domain.ts @@ -4,7 +4,7 @@ import { UserModel } from '../user/user.model'; export const itemTypes = ['file', 'folder'] as const; -export type ItemType = typeof itemTypes[number]; +export type ItemType = (typeof itemTypes)[number]; export interface LookUpAttributes { id: string; diff --git a/src/modules/keyserver/key-server.usecase.spec.ts b/src/modules/keyserver/key-server.usecase.spec.ts index 24bed0cae..53c2e3545 100644 --- a/src/modules/keyserver/key-server.usecase.spec.ts +++ b/src/modules/keyserver/key-server.usecase.spec.ts @@ -1,33 +1,17 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Keys, KeyServer } from './key-server.domain'; import { SequelizeKeyServerRepository } from './key-server.repository'; import { KeyServerUseCases } from './key-server.usecase'; describe('Key Server Use Cases', () => { let service: KeyServerUseCases; - let keyServerRepository: SequelizeKeyServerRepository; + let keyServerRepository: DeepMocked; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - KeyServerUseCases, - SequelizeKeyServerRepository, - { - provide: SequelizeKeyServerRepository, - useValue: { - findUserKeysOrCreate: () => { - return {}; - }, - }, - }, - ], - }).compile(); + keyServerRepository = createMock(); + keyServerRepository.findUserKeysOrCreate.mockResolvedValue({} as any); - service = module.get(KeyServerUseCases); - - keyServerRepository = module.get( - SequelizeKeyServerRepository, - ); + service = new KeyServerUseCases(keyServerRepository); }); it('is be defined', () => { diff --git a/src/modules/send/send.usecase.spec.ts b/src/modules/send/send.usecase.spec.ts index 6dd5fe11c..dd0a1d924 100644 --- a/src/modules/send/send.usecase.spec.ts +++ b/src/modules/send/send.usecase.spec.ts @@ -16,8 +16,6 @@ import { SequelizeSendRepository, } from './send-link.repository'; import { SendUseCases } from './send.usecase'; -import { FileModel } from '../file/file.model'; -import { ThumbnailModel } from '../thumbnail/thumbnail.model'; describe('Send Use Cases', () => { let service: SendUseCases, notificationService, sendRepository; @@ -64,7 +62,9 @@ describe('Send Use Cases', () => { }, { provide: getModelToken(SendLinkModel), - useValue: jest.fn(), + useValue: { + count: jest.fn(), + }, }, { provide: getModelToken(SendLinkItemModel), @@ -166,6 +166,8 @@ describe('Send Use Cases', () => { .spyOn(sendRepository, 'createSendLinkWithItems') .mockResolvedValue(undefined); jest.spyOn(sendRepository, 'findById').mockResolvedValue(undefined); + jest.spyOn(sendRepository, 'countBySendersToday').mockResolvedValue(2); + const sendLink = await service.createSendLinks( null, [], @@ -179,7 +181,6 @@ describe('Send Use Cases', () => { ); expect(sendRepository.createSendLinkWithItems).toHaveBeenCalledTimes(1); expect(notificationService.add).toHaveBeenCalledTimes(1); - expect(sendRepository.findById).toHaveBeenCalledTimes(1); expect(sendLink).toMatchObject({ user: null, code: 'code', @@ -195,6 +196,8 @@ describe('Send Use Cases', () => { .spyOn(sendRepository, 'createSendLinkWithItems') .mockResolvedValue(undefined); jest.spyOn(sendRepository, 'findById').mockResolvedValue(undefined); + jest.spyOn(sendRepository, 'countBySendersToday').mockResolvedValue(2); + const sendLink = await service.createSendLinks( userMock, [], @@ -208,7 +211,6 @@ describe('Send Use Cases', () => { ); expect(sendRepository.createSendLinkWithItems).toHaveBeenCalledTimes(1); expect(notificationService.add).toHaveBeenCalledTimes(1); - expect(sendRepository.findById).toHaveBeenCalledTimes(1); expect(sendLink).toMatchObject({ user: userMock, code: 'code', diff --git a/src/modules/share/share.repository.ts b/src/modules/share/share.repository.ts index e05fb996b..ddd87df15 100644 --- a/src/modules/share/share.repository.ts +++ b/src/modules/share/share.repository.ts @@ -133,12 +133,6 @@ export class SequelizeShareRepository implements ShareRepository { constructor( @InjectModel(ShareModel) private shareModel: typeof ShareModel, - @InjectModel(FileModel) - private fileModel: typeof FileModel, - @InjectModel(FolderModel) - private folderModel: typeof FolderModel, - @InjectModel(UserModel) - private userModel: typeof UserModel, ) {} // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -150,16 +144,16 @@ export class SequelizeShareRepository implements ShareRepository { async findById(id: ShareAttributes['id']) { const share = await this.shareModel.findByPk(id, { include: [ - this.userModel, + UserModel, { - model: this.fileModel, + model: FileModel, where: { deleted: false, }, required: false, }, { - model: this.folderModel, + model: FolderModel, where: { deleted: false, }, @@ -177,9 +171,9 @@ export class SequelizeShareRepository implements ShareRepository { const share = await this.shareModel.findOne({ where: { fileId, userId }, include: [ - this.userModel, + UserModel, { - model: this.fileModel, + model: FileModel, where: { deleted: false, }, @@ -196,9 +190,9 @@ export class SequelizeShareRepository implements ShareRepository { const share = await this.shareModel.findOne({ where: { folderId, userId }, include: [ - this.userModel, + UserModel, { - model: this.folderModel, + model: FolderModel, where: { deleted: false, }, @@ -267,16 +261,16 @@ export class SequelizeShareRepository implements ShareRepository { }, }, include: [ - this.userModel, + UserModel, { - model: this.fileModel, + model: FileModel, where: { deleted: false, }, required: false, }, { - model: this.folderModel, + model: FolderModel, where: { deleted: false, }, diff --git a/src/modules/share/share.usecase.spec.ts b/src/modules/share/share.usecase.spec.ts index 884b66f91..651a7ff7a 100644 --- a/src/modules/share/share.usecase.spec.ts +++ b/src/modules/share/share.usecase.spec.ts @@ -50,6 +50,21 @@ import { FileModel } from '../file/file.model'; import { ThumbnailModel } from '../thumbnail/thumbnail.model'; import { SequelizeKeyServerRepository } from '../keyserver/key-server.repository'; import { AvatarService } from '../../externals/avatar/avatar.service'; +import { SequelizePreCreatedUsersRepository } from '../user/pre-created-users.repository'; +import { PreCreatedUserModel } from '../user/pre-created-users.model'; +import { SequelizeSharingRepository } from '../sharing/sharing.repository'; +import { + PermissionModel, + RoleModel, + SharingInviteModel, + SharingModel, +} from '../sharing/models'; +import { SharingRolesModel } from '../sharing/models/sharing-roles.model'; +import { AppSumoUseCase } from '../app-sumo/app-sumo.usecase'; +import { AppSumoModel } from '../app-sumo/app-sumo.model'; +import { SequelizeAppSumoRepository } from '../app-sumo/app-sumo.repository'; +import { SequelizePlanRepository } from '../plan/plan.repository'; +import { PlanModel } from '../plan/plan.model'; describe('Share Use Cases', () => { let service: ShareUseCases; @@ -209,6 +224,11 @@ describe('Share Use Cases', () => { FolderUseCases, UserUseCases, FileUseCases, + SequelizePreCreatedUsersRepository, + SequelizeSharingRepository, + SequelizeAppSumoRepository, + AppSumoUseCase, + SequelizePlanRepository, SequelizeShareRepository, SequelizeFileRepository, SequelizeFolderRepository, @@ -253,6 +273,38 @@ describe('Share Use Cases', () => { provide: getModelToken(UserModel), useValue: jest.fn(), }, + { + provide: getModelToken(PreCreatedUserModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(SharingModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(PermissionModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(RoleModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(SharingRolesModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(SharingInviteModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(AppSumoModel), + useValue: jest.fn(), + }, + { + provide: getModelToken(PlanModel), + useValue: jest.fn(), + }, { provide: getModelToken(ReferralModel), useValue: jest.fn(), diff --git a/src/modules/sharing/models/index.ts b/src/modules/sharing/models/index.ts index 3bdf4e5d2..50318853d 100644 --- a/src/modules/sharing/models/index.ts +++ b/src/modules/sharing/models/index.ts @@ -16,8 +16,8 @@ import { } from '../sharing.domain'; import { UserModel } from '../../user/user.model'; import { FolderModel } from '../../folder/folder.model'; -import { FileModel } from '../../../modules/file/file.model'; import { PreCreatedUserModel } from '../../../modules/user/pre-created-users.model'; +import { FileModel } from '../../file/file.model'; @Table({ underscored: true, diff --git a/src/modules/sharing/sharing.service.spec.ts b/src/modules/sharing/sharing.service.spec.ts index 0cf8d4d5d..664ddb54d 100644 --- a/src/modules/sharing/sharing.service.spec.ts +++ b/src/modules/sharing/sharing.service.spec.ts @@ -1,6 +1,5 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigModule } from '@nestjs/config'; -import { createMock } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { ConflictException, ForbiddenException } from '@nestjs/common'; import { v4 } from 'uuid'; @@ -10,71 +9,38 @@ import { newSharingRole, newUser, } from '../../../test/fixtures'; -import configuration from '../../config/configuration'; import { SharingService } from './sharing.service'; import { SequelizeSharingRepository } from './sharing.repository'; import { FolderUseCases } from '../folder/folder.usecase'; -import { SequelizeFolderRepository } from '../folder/folder.repository'; import { FileUseCases } from '../file/file.usecase'; import { UserUseCases } from '../user/user.usecase'; -import { SequelizeUserRepository } from '../user/user.repository'; +import { SequelizeUserReferralsRepository } from '../user/user-referrals.repository'; describe('Sharing Use Cases', () => { let sharingService: SharingService; - let sharingRepository: SequelizeSharingRepository; - let folderUseCases: FolderUseCases; - let fileUseCases: FileUseCases; - let userUseCases: UserUseCases; - - const userRepositoryMock = { - findByUuid: jest.fn(), - }; + let sharingRepository: DeepMocked; + let folderUseCases: DeepMocked; + let fileUsecases: DeepMocked; + let usersUsecases: DeepMocked; + let userReferralsRepository: DeepMocked; + let config: DeepMocked; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - envFilePath: [`.env.${process.env.NODE_ENV}`], - load: [configuration], - isGlobal: true, - }), - ], - providers: [ - SharingService, - { - provide: SequelizeSharingRepository, - useValue: createMock(), - }, - { - provide: SequelizeUserRepository, - useValue: userRepositoryMock, - }, - { - provide: SequelizeFolderRepository, - useValue: createMock(), - }, - { - provide: FolderUseCases, - useValue: createMock(), - }, - { - provide: FileUseCases, - useValue: createMock(), - }, - { - provide: UserUseCases, - useValue: createMock(), - }, - ], - }).compile(); - - sharingService = module.get(SharingService); - sharingRepository = module.get( - SequelizeSharingRepository, + sharingRepository = createMock(); + folderUseCases = createMock(); + fileUsecases = createMock(); + usersUsecases = createMock(); + userReferralsRepository = createMock(); + config = createMock(); + + sharingService = new SharingService( + sharingRepository, + fileUsecases, + folderUseCases, + usersUsecases, + config, + userReferralsRepository, ); - folderUseCases = module.get(FolderUseCases); - fileUseCases = module.get(FileUseCases); - userUseCases = module.get(UserUseCases); }); it('should be defined', () => { @@ -97,33 +63,21 @@ describe('Sharing Use Cases', () => { roleId: v4(), }); - jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); - jest - .spyOn(sharingRepository, 'findSharingByItemAndSharedWith') - .mockResolvedValue(sharing); - jest - .spyOn(sharingRepository, 'findSharingRole') - .mockResolvedValue(sharingRole); - - const removeFolderRoleMock = jest.spyOn( - sharingRepository, - 'deleteSharingRole', - ); - const removeSharedWithMock = jest.spyOn( - sharingRepository, - 'deleteSharing', - ); + folderUseCases.getByUuid.mockResolvedValue(folder); + sharingRepository.findOneSharing.mockResolvedValue(sharing); + sharingRepository.findSharingRole.mockResolvedValue(sharingRole); - const response = await sharingService.removeSharedWith( + await sharingService.removeSharedWith( folder.uuid, + 'folder', otherUser.uuid, otherUser, ); - expect(removeFolderRoleMock).toHaveBeenCalled(); - expect(removeSharedWithMock).toHaveBeenCalled(); - - expect(response).toEqual({ message: 'User removed from shared folder' }); + expect(sharingRepository.deleteSharing).toHaveBeenCalledWith(sharing.id); + expect(sharingRepository.deleteSharingRole).toHaveBeenCalledWith( + sharingRole, + ); }); it('When the user is not the owner and is requesting the removal of other user then, it fails', async () => { @@ -137,51 +91,33 @@ describe('Sharing Use Cases', () => { sharedWith: otherUser, }); - jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); - jest - .spyOn(sharingRepository, 'findSharingByItemAndSharedWith') - .mockResolvedValue(sharing); + folderUseCases.getByUuid.mockResolvedValue(folder); + sharingRepository.findOneSharing.mockResolvedValue(sharing); await expect( sharingService.removeSharedWith( folder.uuid, - otherUser.uuid, + 'folder', + owner.uuid, notTheOwner, ), ).rejects.toThrowError(ForbiddenException); }); - it('When the owner tries to remove its own sharing, then it should not be allowed to remove itself', async () => { - const owner = newUser(); - const folder = newFolder({ owner }); - const sharing = newSharing({ - owner, - item: folder, - sharedWith: owner, - }); - - jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); - jest - .spyOn(sharingRepository, 'findSharingByItemAndSharedWith') - .mockResolvedValue(sharing); - - await expect( - sharingService.removeSharedWith(folder.uuid, owner.uuid, owner), - ).rejects.toThrowError(ConflictException); - }); - it('When the owner tries to remove a user that is not invited to the folder then, it fails', async () => { const owner = newUser(); - const otherUser = newUser(); const folder = newFolder({ owner }); - jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); - jest - .spyOn(sharingRepository, 'findSharingByItemAndSharedWith') - .mockResolvedValue(null); + folderUseCases.getByUuid.mockResolvedValue(folder); + sharingRepository.findOneSharing.mockResolvedValue(null); await expect( - sharingService.removeSharedWith(folder.uuid, otherUser.uuid, owner), + sharingService.removeSharedWith( + folder.uuid, + folder.type as any, + 'not invited user uudi', + owner, + ), ).rejects.toThrowError(ConflictException); }); @@ -199,33 +135,21 @@ describe('Sharing Use Cases', () => { roleId: v4(), }); - jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValue(folder); - jest - .spyOn(sharingRepository, 'findSharingByItemAndSharedWith') - .mockResolvedValue(sharing); - jest - .spyOn(sharingRepository, 'findSharingRole') - .mockResolvedValue(sharingRole); - - const removeFolderRoleMock = jest.spyOn( - sharingRepository, - 'deleteSharingRole', - ); - const removeSharedWithMock = jest.spyOn( - sharingRepository, - 'deleteSharing', - ); + folderUseCases.getByUuid.mockResolvedValue(folder); + sharingRepository.findOneSharing.mockResolvedValue(sharing); + sharingRepository.findSharingRole.mockResolvedValue(sharingRole); - const response = await sharingService.removeSharedWith( + await sharingService.removeSharedWith( folder.uuid, + 'folder', otherUser.uuid, owner, ); - expect(removeFolderRoleMock).toHaveBeenCalled(); - expect(removeSharedWithMock).toHaveBeenCalled(); - - expect(response).toEqual({ message: 'User removed from shared folder' }); + expect(sharingRepository.deleteSharing).toHaveBeenCalledWith(sharing.id); + expect(sharingRepository.deleteSharingRole).toHaveBeenCalledWith( + sharingRole, + ); }); }); }); diff --git a/src/modules/sharing/sharing.service.ts b/src/modules/sharing/sharing.service.ts index 8a6c7e32f..5afd55a2f 100644 --- a/src/modules/sharing/sharing.service.ts +++ b/src/modules/sharing/sharing.service.ts @@ -1191,9 +1191,8 @@ export class SharingService { return sharing; } - const sharingCreated = await this.sharingRepository.createSharing( - newSharing, - ); + const sharingCreated = + await this.sharingRepository.createSharing(newSharing); this.userReferralsRepository .applyUserReferral(user.id, ReferralKey.ShareFile) @@ -1428,9 +1427,8 @@ export class SharingService { requester: User, sharingRoleId: SharingRole['id'], ): Promise { - const sharingRole = await this.sharingRepository.findSharingRole( - sharingRoleId, - ); + const sharingRole = + await this.sharingRepository.findSharingRole(sharingRoleId); if (!sharingRole) { throw new NotFoundException('Sharing role not found'); } diff --git a/src/modules/user/user.usecase.spec.ts b/src/modules/user/user.usecase.spec.ts index 72f40090c..62bb51aa8 100644 --- a/src/modules/user/user.usecase.spec.ts +++ b/src/modules/user/user.usecase.spec.ts @@ -20,6 +20,8 @@ import { SequelizeKeyServerRepository } from '../keyserver/key-server.repository import { Folder, FolderAttributes } from '../folder/folder.domain'; import { File, FileAttributes } from '../file/file.domain'; import { AvatarService } from '../../externals/avatar/avatar.service'; +import { SequelizePreCreatedUsersRepository } from './pre-created-users.repository'; +import { SequelizeSharingRepository } from '../sharing/sharing.repository'; describe('User use cases', () => { let userUseCases: UserUseCases; @@ -222,6 +224,14 @@ const createTestingModule = (): Promise => { provide: SequelizeUserRepository, useValue: createMock(), }, + { + provide: SequelizePreCreatedUsersRepository, + useValue: createMock(), + }, + { + provide: SequelizeSharingRepository, + useValue: createMock(), + }, { provide: SequelizeSharedWorkspaceRepository, useValue: createMock(), diff --git a/src/modules/user/user.usecase.ts b/src/modules/user/user.usecase.ts index 2179719cf..f7fcabcd3 100644 --- a/src/modules/user/user.usecase.ts +++ b/src/modules/user/user.usecase.ts @@ -1,9 +1,4 @@ -import { - Injectable, - Logger, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Environment } from '@internxt/inxt-js'; import { v4 } from 'uuid'; @@ -410,9 +405,8 @@ export class UserUseCases { newUserUuid: string, newPublicKey: string, ) { - const preCreatedUser = await this.preCreatedUserRepository.findByUsername( - email, - ); + const preCreatedUser = + await this.preCreatedUserRepository.findByUsername(email); if (!preCreatedUser) { return; diff --git a/test/__e2e__/repositories/folders-test.repository.ts b/test/__e2e__/repositories/folders-test.repository.ts new file mode 100644 index 000000000..2a3d299a3 --- /dev/null +++ b/test/__e2e__/repositories/folders-test.repository.ts @@ -0,0 +1,52 @@ +import { Sequelize, QueryTypes } from 'sequelize'; + +export class FolderTestRepository { + constructor(private readonly sequelize: Sequelize) {} + + public async getFoldersByUserId(userId: number): Promise { + const folders = await this.sequelize.query( + `SELECT * FROM folders WHERE user_id = :userId`, + { + replacements: { userId }, + type: QueryTypes.SELECT, + }, + ); + + return folders; + } + + public async createOrphan(uuid: string, userId: number): Promise { + await this.sequelize.query( + `INSERT INTO folders (parent_id, parent_uuid, name, bucket, user_id, uuid, plain_name, encrypt_version, deleted, removed, created_at, updated_at) VALUES (:parentId, :parentUuid, :name, :bucket, :userId, :uuid, :plainName, :encryptVersion, :deleted, :removed, :createdAt, :updatedAt)`, + { + replacements: { + parentId: 0, + parentUuid: null, + name: 'orphan folder', + bucket: 'bucket', + userId, + uuid, + plainName: 'orphan folder', + encryptVersion: '1.0', + deleted: false, + removed: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + type: QueryTypes.INSERT, + }, + ); + } + + public async getBy(key: string, value: T): Promise { + const folder = await this.sequelize.query( + `SELECT * FROM folders WHERE ${key} = :value`, + { + replacements: { value }, + type: QueryTypes.SELECT, + }, + ); + + return folder[0]; + } +} diff --git a/test/__e2e__/repositories/users-test.repository.ts b/test/__e2e__/repositories/users-test.repository.ts new file mode 100644 index 000000000..0cf6e966b --- /dev/null +++ b/test/__e2e__/repositories/users-test.repository.ts @@ -0,0 +1,43 @@ +import { QueryTypes, Sequelize } from 'sequelize'; +import { users as testUsers } from '../../../seeders/20230308180046-test-users'; +import { Sign } from '../../../src/middlewares/passport'; + +export class UserTestRepository { + constructor(private readonly sequelize: Sequelize) {} + + public async getPrincipalUser(): Promise { + const user = testUsers.testUser; + + const users = await this.sequelize.query( + `SELECT * FROM users WHERE email = :email`, + { + replacements: { email: user.email }, + type: QueryTypes.SELECT, + }, + ); + + return users[0]; + } + + public generateToken(user: any, jwtSecret: string): string { + return Sign( + { + payload: { + id: user.id, + uuid: user.uuid, + email: user.email, + name: user.name, + lastname: user.lastname, + username: user.username, + sharedWorkspace: true, + networkCredentials: { + user: user.bridge_user, + pass: user.user_id, + }, + }, + }, + jwtSecret, + true, + ); + } +} diff --git a/test/__e2e__/sequelize.ts b/test/__e2e__/sequelize.ts new file mode 100644 index 000000000..d4fc9db2f --- /dev/null +++ b/test/__e2e__/sequelize.ts @@ -0,0 +1,22 @@ +import { Sequelize } from 'sequelize'; +import getEnv from '../../src/config/configuration'; + +const { database } = getEnv(); + +const sequelizeTest = new Sequelize( + database.database, + database.username, + database.password, + { + dialect: 'postgres', + host: database.host, + pool: { + max: 20, + min: 0, + idle: 20000, + acquire: 20000, + }, + }, +); + +export { sequelizeTest }; diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index ef9b6b47e..56098a6b8 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -114,64 +114,4 @@ describe('Testing fixtures tests', () => { expect(folder.removedAt).toBe(settableAttributes.removedAt); }); }); - - describe("PrivateSharingFolder's fixture", () => { - it('When it generates a private sharing folder and no params are provided, then the attributes should be random', () => { - const privateSharingFolder = fixtures.newPrivateSharingFolder(); - const otherPrivateSharingFolder = fixtures.newPrivateSharingFolder(); - - expect(privateSharingFolder.id).not.toBe(otherPrivateSharingFolder.id); - expect(privateSharingFolder.folderId).not.toBe( - otherPrivateSharingFolder.folderId, - ); - expect(privateSharingFolder.ownerId).not.toBe( - otherPrivateSharingFolder.ownerId, - ); - expect(privateSharingFolder.sharedWith).not.toBe( - otherPrivateSharingFolder.sharedWith, - ); - expect(privateSharingFolder.createdAt.getTime()).not.toBe( - otherPrivateSharingFolder.createdAt.getTime(), - ); - expect(privateSharingFolder.encryptionKey).not.toBe( - otherPrivateSharingFolder.encryptionKey, - ); - }); - - it('When it generates a private sharing folder if the folder is provided, then the folderId should not be random', () => { - const folder = fixtures.newFolder(); - const privateSharingFolder = fixtures.newPrivateSharingFolder({ - folder, - }); - - expect(privateSharingFolder.folderId).toBe(folder.uuid); - }); - - it('When it generates a private sharing folder if the owner is provided, then the ownerId should be set', () => { - const owner = fixtures.newUser(); - const privateSharingFolder = fixtures.newPrivateSharingFolder({ - owner, - }); - - expect(privateSharingFolder.ownerId).toBe(owner.uuid); - }); - - it('When it generates a private sharing folder if the sharedWith is provided, then the sharedWith should be set', () => { - const sharedWith = fixtures.newUser(); - const privateSharingFolder = fixtures.newPrivateSharingFolder({ - sharedWith, - }); - - expect(privateSharingFolder.sharedWith).toBe(sharedWith.uuid); - }); - - it('When it generates a private sharing folder, then the encrytionKey should be random', () => { - const privateSharingFolder = fixtures.newPrivateSharingFolder(); - const otherPrivateSharingFolder = fixtures.newPrivateSharingFolder(); - - expect(privateSharingFolder.encryptionKey).not.toBe( - otherPrivateSharingFolder.encryptionKey, - ); - }); - }); }); diff --git a/test/fixtures.ts b/test/fixtures.ts index 16f077f6f..01246c86f 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -3,7 +3,6 @@ import { Chance } from 'chance'; import { Folder } from '../src/modules/folder/folder.domain'; import { User } from '../src/modules/user/user.domain'; -import { PrivateSharingFolder } from '../src/modules/private-share-folder/private-sharing-folder.domain'; import { Sharing, SharingRole } from '../src/modules/sharing/sharing.domain'; import { File } from '../src/modules/file/file.domain'; @@ -96,29 +95,13 @@ export const newUser = (): User => { }); }; -export const newPrivateSharingFolder = (bindTo?: { - owner?: User; - sharedWith?: User; - folder?: Folder; -}): PrivateSharingFolder => { - return PrivateSharingFolder.build({ - id: v4(), - folderId: bindTo?.folder?.uuid || v4(), - ownerId: bindTo?.owner?.uuid || v4(), - sharedWith: bindTo?.sharedWith?.uuid || v4(), - createdAt: randomDataGenerator.date(), - encryptionKey: randomDataGenerator.string({ - length: 32, - }), - }); -}; - export const newSharing = (bindTo?: { owner?: User; sharedWith?: User; item?: File | Folder; }): Sharing => { return Sharing.build({ + type: undefined, id: v4(), itemId: bindTo?.item?.uuid || v4(), itemType: (bindTo?.item instanceof File ? 'file' : 'folder') || 'folder',