diff --git a/.github/workflows/build-and-push-docker-image.yml b/.github/workflows/deploy-API.yml similarity index 82% rename from .github/workflows/build-and-push-docker-image.yml rename to .github/workflows/deploy-API.yml index 60042394..038f99b9 100644 --- a/.github/workflows/build-and-push-docker-image.yml +++ b/.github/workflows/deploy-API.yml @@ -1,9 +1,9 @@ -name: Build and push docker image +name: Deploy API on: push: branches: - - '*' + - 'main' tags: - 'v*' @@ -11,25 +11,11 @@ env: REGISTRY: ghcr.io jobs: - eslint: - name: Run eslint check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install dependencies - run: | - yarn install - - - name: Run ESLint - run: yarn lint - build: runs-on: ubuntu-22.04 steps: - - name: Checkout repository + - name: Checkout Repository uses: actions/checkout@v3 - name: Get full image name diff --git a/.github/workflows/run-build-check.yml b/.github/workflows/run-build-check.yml new file mode 100644 index 00000000..947a963b --- /dev/null +++ b/.github/workflows/run-build-check.yml @@ -0,0 +1,33 @@ +name: Run build check + +on: + push: + branches: + - 'main' + tags: + - 'v*' + + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Run yarn build + runs-on: ubuntu-22.04 + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install dependencies + run: yarn install + + - name: Build + run: yarn build + + diff --git a/.github/workflows/run-eslint-check.yml b/.github/workflows/run-eslint-check.yml new file mode 100644 index 00000000..2dcc99d0 --- /dev/null +++ b/.github/workflows/run-eslint-check.yml @@ -0,0 +1,26 @@ +name: Run ESlint check + +on: + push: + branches: + - 'main' + tags: + - 'v*' + + pull_request: + types: [opened, synchronize, reopened] + +jobs: + eslint: + name: Run eslint check + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + yarn install + + - name: Run ESLint + run: yarn lint \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8f72e1a0..9368da48 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,9 +1,14 @@ name: Run tests on: - pull_request: + push: branches: - - '*' + - 'main' + tags: + - 'v*' + + pull_request: + types: [opened, synchronize, reopened] jobs: tests: diff --git a/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql b/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql new file mode 100644 index 00000000..d7168f02 --- /dev/null +++ b/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql @@ -0,0 +1,22 @@ + +-- Adds "editor_tools" column at "users" if not exists +DO $$ +BEGIN + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='users' and column_name='editor_tools') + THEN + ALTER TABLE "public"."users" ADD COLUMN "editor_tools" jsonb; + END IF; +END $$; + +-- Removes "extensions" column at "users" if exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='users' and column_name='extensions') + THEN + ALTER TABLE "public"."users" DROP COLUMN "extensions"; + END IF; +END $$; diff --git a/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql new file mode 100644 index 00000000..46c2de25 --- /dev/null +++ b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql @@ -0,0 +1,33 @@ +-- "editor_tools" table: + +-- Rename column "isDefault" to "is_default" if "isDefault" exists and "is_default" not exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='isDefault') + THEN + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='is_default') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "isDefault" TO "is_default"; + END IF; + END IF; +END $$; + +-- Rename column "exportName" to "export_name" if "exportName" exists and "export_name" not exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='exportName') + THEN + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='export_name') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "exportName" TO "export_name"; + END IF; + END IF; +END $$; diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index 08a7dc0c..32957ede 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -1,4 +1,4 @@ -import type UserExtensions from '@domain/entities/userExtensions.js'; +import type EditorTool from './editorTools'; /** * User entity @@ -30,8 +30,7 @@ export default interface User { photo?: string; /** - * Custom plugins from the marketplace that improve - * editor or notes environment + * Custom plugins ids from the marketplace that improve editor or notes environment */ - extensions?: UserExtensions; + editorTools?: EditorTool['id'][]; } diff --git a/src/domain/entities/userExtensions.ts b/src/domain/entities/userExtensions.ts deleted file mode 100644 index 4ee4acc4..00000000 --- a/src/domain/entities/userExtensions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type EditorTool from '@domain/entities/editorTools.js'; - -/** - * Tools that user uses in the editor while changing notes - */ -export interface UserEditorTool { - /** - * Unique tool identifier - */ - id: EditorTool['id']; -} - -/** - * Custom user extensions and plugin that expand the capabilities - * of the editor - */ -export default interface UserExtensions { - editorTools?: UserEditorTool[]; -} diff --git a/src/domain/index.ts b/src/domain/index.ts index 8e88151c..c061e04e 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -63,10 +63,14 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe repositories.userSessionRepository ); - const userService = new UserService(repositories.userRepository); - - const aiService = new AIService(repositories.aiRepository); const editorToolsService = new EditorToolsService(repositories.editorToolsRepository); + const userService = new UserService(repositories.userRepository, { + editorTools: editorToolsService, + /** + * @todo find a way how to resolve circular dependency + */ + }); + const aiService = new AIService(repositories.aiRepository); return { noteService, diff --git a/src/domain/service/editorTools.ts b/src/domain/service/editorTools.ts index a25e51d2..c0a14282 100644 --- a/src/domain/service/editorTools.ts +++ b/src/domain/service/editorTools.ts @@ -1,11 +1,12 @@ import type EditorToolsRepository from '@repository/editorTools.repository.js'; import type EditorTool from '@domain/entities/editorTools.js'; import { createEditorToolId } from '@infrastructure/utils/id.js'; +import type EditorToolsServiceSharedMethods from './shared/editorTools.js'; /** * Editor tools service */ -export default class EditorToolsService { +export default class EditorToolsService implements EditorToolsServiceSharedMethods { /** * User repository instance */ @@ -32,10 +33,17 @@ export default class EditorToolsService { * * @param editorToolIds - tool ids */ - public async getToolsByIds(editorToolIds: EditorTool['id'][] ): Promise { + public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { return await this.repository.getToolsByIds(editorToolIds); } + /** + * Return tools that are available at Editor by default + */ + public async getDefaultTools(): Promise { + return await this.repository.getDefaultTools(); + } + /** * Adding custom editor tool * diff --git a/src/domain/service/shared/README.md b/src/domain/service/shared/README.md new file mode 100644 index 00000000..b7d2d0f0 --- /dev/null +++ b/src/domain/service/shared/README.md @@ -0,0 +1,30 @@ +# Shared Domain Services + +Sometimes you may want to call some domain method from other domain. We can inject them in constructor, but it creates a direct dependency. + +One way do decouple domains is to create a Shared Interfaces — domain will "expose" some public methods though it. You can call it "Contract". +So dependant domain will depend on it instead of direct dependency. + + +## Example + +```ts +interface DomainASharedMethods { + someMethodA: () => void; +} + +export type SharedDomainMethods = { + domainA: DomainASharedMethods; +}; + + +class DomainA implements DomainASharedMethods { + public someMethodA (){} +} + +class DomainB { + constructor(private readonly shared: SharedDomainMethods) { + this.shared.domainA.someMethodA(); // here we call method of Domain A, but without direct dependency + } +} +``` diff --git a/src/domain/service/shared/editorTools.ts b/src/domain/service/shared/editorTools.ts new file mode 100644 index 00000000..8f1d63a4 --- /dev/null +++ b/src/domain/service/shared/editorTools.ts @@ -0,0 +1,19 @@ +import type EditorTool from '@domain/entities/editorTools'; + +/** + * Which methods of Domain can be used by other domains + * Uses to decouple domains from each other + */ +export default interface EditorToolsServiceSharedMethods { + /** + * Return tools that are available at Editor by default + */ + getDefaultTools(): Promise; + + /** + * Get bunch of editor tools by their ids + * + * @param ids - tool ids to resolve + */ + getToolsByIds(ids: EditorTool['id'][]): Promise; +} diff --git a/src/domain/service/shared/index.ts b/src/domain/service/shared/index.ts new file mode 100644 index 00000000..11364d8f --- /dev/null +++ b/src/domain/service/shared/index.ts @@ -0,0 +1,5 @@ +import type EditorToolsServiceSharedMethods from './editorTools'; + +export type SharedDomainMethods = { + editorTools: EditorToolsServiceSharedMethods; +}; diff --git a/src/domain/service/user.ts b/src/domain/service/user.ts index f734bdb3..b54a0ea1 100644 --- a/src/domain/service/user.ts +++ b/src/domain/service/user.ts @@ -2,6 +2,7 @@ import type UserRepository from '@repository/user.repository.js'; import { Provider } from '@repository/user.repository.js'; import type User from '@domain/entities/user.js'; import type EditorTool from '@domain/entities/editorTools'; +import type { SharedDomainMethods } from './shared/index.js'; export { Provider @@ -20,8 +21,9 @@ export default class UserService { * User service constructor * * @param repository - user repository instance + * @param shared - shared domains */ - constructor(repository: UserRepository) { + constructor(repository: UserRepository, private readonly shared: SharedDomainMethods) { this.repository = repository; } @@ -47,15 +49,28 @@ export default class UserService { } /** - * Get user extensions that contains only editoTools for now - * TODO: Simplify extenisons + * Get user editor tools ids * * @param userId - user unique identifier */ - public async getUserExtensions(userId: User['id']): Promise { + public async getUserEditorTools(userId: User['id']): Promise { const user = await this.getUserById(userId); - return user?.extensions ?? {}; + if (user === null) { + throw new Error('User not found'); + } + + const userToolsIds = user.editorTools ?? []; + const defaultTools = await this.shared.editorTools.getDefaultTools(); + const uniqueDefaultEditorTools = defaultTools.filter(({ id }) => !userToolsIds.includes(id)); + const userTools = await this.shared.editorTools.getToolsByIds(userToolsIds) ?? []; + + /** + * Combine user tools and default tools + * + * @todo load tools in notes service + */ + return [...userTools, ...uniqueDefaultEditorTools]; } /** @@ -65,16 +80,14 @@ export default class UserService { */ public async addUserEditorTool({ userId, - editorToolId, + toolId, }: { userId: User['id'], - editorToolId: EditorTool['id'], + toolId: EditorTool['id'], }): Promise { return await this.repository.addUserEditorTool({ userId, - tool: { - id: editorToolId, - }, + toolId, }); } } diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index c7c1ab1c..2ee39395 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1,6 +1,9 @@ import { describe, test, expect } from 'vitest'; +import notes from '@tests/test-data/notes.json'; +import noteSettings from '@tests/test-data/notesSettings.json'; + describe('Note API', () => { describe('GET note/resolve-hostname/:hostname ', () => { test('Returns note with specified hostname', async () => { @@ -49,4 +52,89 @@ describe('Note API', () => { expect(body).toStrictEqual({ message: 'Note not found' }); }); }); -}); + + describe('GET note/:notePublicId ', () => { + test('Returns note by public id with 200 status when note is publicly available', async () => { + const expectedStatus = 200; + const correctID = 'Pq1T9vc23Q'; + + const expectedNote = { + 'id': 2, + 'publicId': 'Pq1T9vc23Q', + 'creatorId': 1, + 'content': null, + 'createdAt': '2023-10-16T13:49:19.000Z', + 'updatedAt': '2023-10-16T13:49:19.000Z', + }; + + const response = await global.api?.fakeRequest({ + method: 'GET', + url: `/note/${correctID}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + expect(response?.json()).toStrictEqual(expectedNote); + }); + + test('Returns 403 when public access is disabled, user is not creator of the note', async () => { + const expectedStatus = 403; + + const notPublicNote = notes.find(newNote => { + const settings = noteSettings.find(ns => ns.note_id === newNote.id); + + return settings!.is_public === false; + }); + + const response = await global.api?.fakeRequest({ + method: 'GET', + url: `/note/${notPublicNote!.public_id}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + expect(response?.json()).toStrictEqual({ message: 'Permission denied' }); + }); + + test('Returns 406 when the id does not exist', async () => { + const expectedStatus = 406; + const nonexistentId = 'ishvm5qH84'; + + const response = await global.api?.fakeRequest({ + method: 'GET', + url: `/note/${nonexistentId}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + expect(response?.json()).toStrictEqual({ message: 'Note not found' }); + }); + + test.each([ + { id: 'mVz3iHuez', + expectedMessage: 'params/notePublicId must NOT have fewer than 10 characters' }, + + { id: 'cR8eqF1mFf0', + expectedMessage: 'params/notePublicId must NOT have more than 10 characters' }, + + { id: '+=*&*5%&&^&-', + expectedMessage: '\'/note/+=*&*5%&&^&-\' is not a valid url component' }, + ]) + ('Returns 400 when id has incorrect characters and length', async ({ id, expectedMessage }) => { + const expectedStatus = 400; + + const response = await global.api?.fakeRequest({ + method: 'GET', + url: `/note/${id}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + expect(response?.json().message).toStrictEqual(expectedMessage); + }); + + test.todo('Returns note by public id with 200 status when access is disabled, but user is creator'); + + test.todo('API should not return internal id and "publicId". It should return only "id" which is public id.'); + }); +}); \ No newline at end of file diff --git a/src/presentation/http/router/noteList.test.ts b/src/presentation/http/router/noteList.test.ts new file mode 100644 index 00000000..27d4b0e3 --- /dev/null +++ b/src/presentation/http/router/noteList.test.ts @@ -0,0 +1,116 @@ +import userSessions from '@tests/test-data/userSessions.json'; +import { describe, test, expect, beforeAll } from 'vitest'; + + +/** + * Access token that will be used for Auhorization header + */ +let accessToken = ''; + + +describe('NoteList API', () => { + beforeAll(async () => { + /** + * userId for authorization + */ + const userId = userSessions[0]['user_id']; + + accessToken = global.auth(userId); + }); + + describe('GET /notes?page', () => { + test('Returns noteList with specified length (not for last page)', async () => { + const expectedStatus = 200; + const portionSize = 30; + const pageNumber = 1; + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes?page=${pageNumber}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + const body = response?.body !== undefined ? JSON.parse(response?.body) : {}; + + expect(body.items).toHaveLength(portionSize); + }); + + test('Returns noteList with specified lenght (for last page)', async () => { + const expectedStatus = 200; + const portionSize = 19; + const pageNumber = 2; + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes?page=${pageNumber}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + const body = response?.body !== undefined ? JSON.parse(response?.body) : {}; + + expect(body.items).toHaveLength(portionSize); + }); + + test('Returns noteList with no items if it has no notes', async () => { + const expectedStatus = 200; + const pageNumber = 3; + + console.log('accessToken', accessToken); + + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes?page=${pageNumber}`, + }); + + expect(response?.statusCode).toBe(expectedStatus); + + const body = response?.body !== undefined ? JSON.parse(response?.body) : {}; + + expect(body).toEqual( { items : [] } ); + expect(body.items).toHaveLength(0); + }); + + test('Returns 400 when page < 0', async () => { + const expextedStatus = 400; + const pageNumber = 0; + + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes?page=${pageNumber}`, + }); + + expect(response?.statusCode).toBe(expextedStatus); + }); + + test('Returns 400 when page is too large', async () => { + const expextedStatus = 400; + const pageNumber = 31; + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/notes?page=${pageNumber}`, + }); + + expect(response?.statusCode).toBe(expextedStatus); + }); + }); +}); diff --git a/src/presentation/http/router/user.ts b/src/presentation/http/router/user.ts index 2b1e120e..965f8076 100644 --- a/src/presentation/http/router/user.ts +++ b/src/presentation/http/router/user.ts @@ -30,7 +30,6 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don * Manage user data */ const userService = opts.userService; - const editorToolsService = opts.editorToolsService; /** * Get user by session @@ -63,7 +62,7 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }); /** - * Get user extensions + * Get user editor tools */ fastify.get('/editor-tools', { config: { @@ -93,12 +92,10 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const userId = request.userId as number; - const userExtensions = await userService.getUserExtensions(userId); - const userEditorToolIds = userExtensions?.editorTools?.map(tools => tools.id) ?? []; - const editorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; + const tools = await userService.getUserEditorTools(userId); return reply.send({ - data: editorTools, + data: tools, }); }); @@ -124,16 +121,16 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }, }, }, async (request, reply) => { - const editorToolId = request.body.toolId; + const toolId = request.body.toolId; const userId = request.userId as number; await userService.addUserEditorTool({ userId, - editorToolId, + toolId, }); return reply.send({ - data: editorToolId, + data: toolId, }); }); diff --git a/src/presentation/http/schema/User.ts b/src/presentation/http/schema/User.ts index 9061d2f3..4f03ad48 100644 --- a/src/presentation/http/schema/User.ts +++ b/src/presentation/http/schema/User.ts @@ -10,4 +10,11 @@ export const UserSchema = { name: { type: 'string' }, photo: { type: 'string' }, }, + editorTools: { + type: 'array', + description: 'List of editor tools ids installed by user from Marketplace', + items: { + type: 'string', + }, + }, }; diff --git a/src/repository/editorTools.repository.ts b/src/repository/editorTools.repository.ts index 9a20d359..c29dbe12 100644 --- a/src/repository/editorTools.repository.ts +++ b/src/repository/editorTools.repository.ts @@ -34,6 +34,13 @@ export default class EditorToolsRepository { return tools; } + /** + * Get all default tools + */ + public async getDefaultTools(): Promise { + return await this.storage.getDefaultTools(); + } + /** * Get all editor tools */ diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index e2baa164..73677924 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -1,6 +1,5 @@ import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; import type NoteStorage from '@repository/storage/note.storage.js'; -import type { NoteList } from '@domain/entities/noteList.js'; /** * Repository allows accessing data from business-logic (domain) level @@ -85,7 +84,6 @@ export default class NoteRepository { * @param id - note creator id * @param offset - number of skipped notes * @param limit - number of notes to get - * @returns { Promise } note */ public async getNoteListByCreatorId(id: number, offset: number, limit: number): Promise { return await this.storage.getNoteListByCreatorId(id, offset, limit); diff --git a/src/repository/storage/postgres/orm/sequelize/editorTools.ts b/src/repository/storage/postgres/orm/sequelize/editorTools.ts index f20d3c17..a431bb5d 100644 --- a/src/repository/storage/postgres/orm/sequelize/editorTools.ts +++ b/src/repository/storage/postgres/orm/sequelize/editorTools.ts @@ -13,7 +13,7 @@ export class EditorToolModel extends Model, Inf public declare id: EditorTool['id']; /** - * Custom name that uses in editor initiazliation. e.g. 'code' + * Custom name that uses in editor initialization. e.g. 'code' */ public declare name: EditorTool['name']; @@ -31,6 +31,11 @@ export class EditorToolModel extends Model, Inf * Editor tool sources */ public declare source: EditorTool['source']; + + /** + * Applies to user editor tools by default + */ + public declare isDefault: EditorTool['isDefault']; } /** @@ -85,6 +90,10 @@ export default class UserSequelizeStorage { type: DataTypes.JSON, allowNull: false, }, + isDefault: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, }, { tableName: this.tableName, sequelize: this.database, @@ -101,16 +110,16 @@ export default class UserSequelizeStorage { title, exportName, source, + isDefault, }: EditorTool): Promise { - const editorTool = await this.model.create({ + return await this.model.create({ id, name, title, exportName, source, + isDefault, }); - - return editorTool; } /** @@ -119,23 +128,30 @@ export default class UserSequelizeStorage { * @param editorToolIds - tool ids */ public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { - const editorTools = await this.model.findAll({ + return await this.model.findAll({ where: { id: { [Op.in]: editorToolIds, }, }, }); + } - return editorTools; + /** + * Get all default tools + */ + public async getDefaultTools(): Promise { + return await this.model.findAll({ + where: { + isDefault: true, + }, + }); } /** * Get all available editor tools */ public async getTools(): Promise { - const editorTools = await EditorToolModel.findAll(); - - return editorTools; + return await EditorToolModel.findAll(); } } diff --git a/src/repository/storage/postgres/orm/sequelize/index.ts b/src/repository/storage/postgres/orm/sequelize/index.ts index 93e3dc10..f8a2c9eb 100644 --- a/src/repository/storage/postgres/orm/sequelize/index.ts +++ b/src/repository/storage/postgres/orm/sequelize/index.ts @@ -28,6 +28,12 @@ export default class SequelizeOrm { this.conn = new Sequelize(this.config.dsn, { logging: databaseLogger.info.bind(databaseLogger), + define: { + /** + * Use snake_case for fields in db, but camelCase in code + */ + underscored: true, + }, }); } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 42e24449..03f40a3e 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -103,7 +103,6 @@ export default class NoteSequelizeStorage { }, { tableName: this.tableName, sequelize: this.database, - underscored: true, // use snake_case for fields in db }); } @@ -131,13 +130,11 @@ export default class NoteSequelizeStorage { * @returns { Note } - created note */ public async createNote(options: NoteCreationAttributes): Promise { - const createdNote = await this.model.create({ + return await this.model.create({ publicId: options.publicId, content: options.content, creatorId: options.creatorId, }); - - return createdNote; } /** @@ -170,20 +167,11 @@ export default class NoteSequelizeStorage { * @param id - internal id */ public async getNoteById(id: NoteInternalId): Promise { - const note = await this.model.findOne({ + return await this.model.findOne({ where: { id, }, }); - - /** - * If note not found, return null - */ - if (!note) { - return null; - } - - return note; } /** @@ -213,15 +201,13 @@ export default class NoteSequelizeStorage { * @returns { Promise } note */ public async getNoteListByCreatorId(creatorId: number, offset: number, limit: number): Promise { - const noteList = await this.model.findAll({ + return await this.model.findAll({ offset: offset, limit: limit, where: { creatorId, }, }); - - return noteList; } /** * Gets note by id @@ -256,12 +242,10 @@ export default class NoteSequelizeStorage { * @returns { Promise } found note */ public async getNoteByPublicId(publicId: NotePublicId): Promise { - const note = await this.model.findOne({ + return await this.model.findOne({ where: { publicId, }, }); - - return note; }; } diff --git a/src/repository/storage/postgres/orm/sequelize/noteSettings.ts b/src/repository/storage/postgres/orm/sequelize/noteSettings.ts index 04c8af68..2c36ba01 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteSettings.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteSettings.ts @@ -92,7 +92,6 @@ export default class NoteSettingsSequelizeStorage { tableName: this.tableName, sequelize: this.database, timestamps: false, - underscored: true, // use snake_case for fields in db }); } diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index ee241656..d6df080a 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -104,7 +104,6 @@ export default class TeamsSequelizeStorage { tableName: this.tableName, sequelize: this.database, timestamps: false, - underscored: true, // use snake_case for fields in db }); } diff --git a/src/repository/storage/postgres/orm/sequelize/user.ts b/src/repository/storage/postgres/orm/sequelize/user.ts index 242137c8..53585523 100644 --- a/src/repository/storage/postgres/orm/sequelize/user.ts +++ b/src/repository/storage/postgres/orm/sequelize/user.ts @@ -3,7 +3,7 @@ import { fn, col } from 'sequelize'; import { Model, DataTypes } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; import type User from '@domain/entities/user.js'; -import type { UserEditorTool } from '@domain/entities/userExtensions.js'; +import type EditorTool from '@domain/entities/editorTools'; /** * Query options for getting user @@ -47,9 +47,9 @@ export interface AddUserToolOptions { userId: User['id']; /** - * Editor tool data + * Editor tool identifier */ - tool: UserEditorTool; + toolId: EditorTool['id']; } /** @@ -62,9 +62,9 @@ interface RemoveUserEditorTool { userId: User['id']; /** - * Editor tool + * Editor tool identifier */ - editorTool: UserEditorTool; + toolId: EditorTool['id']; } /* eslint-disable @typescript-eslint/naming-convention */ @@ -91,7 +91,7 @@ export class UserModel extends Model, InferCreationAt /** * User created at */ - public declare created_at: Date; + public declare createdAt: Date; /** * User photo @@ -99,9 +99,9 @@ export class UserModel extends Model, InferCreationAt public declare photo: CreationOptional; /** - * + * List of tools ids installed by user from Marketplace */ - public declare extensions: CreationOptional; + public declare editorTools: CreationOptional; } /** @@ -149,15 +149,15 @@ export default class UserSequelizeStorage { type: DataTypes.STRING, allowNull: false, }, - created_at: { + createdAt: { type: DataTypes.DATE, allowNull: false, }, photo: { type: DataTypes.STRING, }, - extensions: { - type: DataTypes.JSON, + editorTools: { + type: DataTypes.ARRAY(DataTypes.STRING), }, }, { tableName: this.tableName, @@ -173,14 +173,14 @@ export default class UserSequelizeStorage { */ public async addUserEditorTool({ userId, - tool: editorTool, + toolId, }: AddUserToolOptions): Promise { await this.model.update({ - extensions: fn('array_append', col('editorTools'), editorTool), + editorTools: fn('array_append', col('editor_tools'), toolId), }, { where: { id: userId, - // TODO: Add check to unique editorTool id + // @todo Add check to unique editorTool id }, }); } @@ -192,10 +192,10 @@ export default class UserSequelizeStorage { */ public async removeUserEditorTool({ userId, - editorTool, + toolId, }: RemoveUserEditorTool): Promise { await this.model.update({ - extensions: fn('array_remove', col('editorTools'), editorTool), + editorTools: fn('array_remove', col('editor_tools'), toolId), }, { where: { id: userId, @@ -214,20 +214,12 @@ export default class UserSequelizeStorage { name, photo, }: InsertUserOptions): Promise { - const user = await this.model.create({ + return await this.model.create({ email, name, - created_at: new Date(), + createdAt: new Date(), photo, }); - - return { - id: user.id, - email: user.email, - name: user.name, - createdAt: user.created_at, - photo: user.photo, - }; } /** @@ -274,13 +266,6 @@ export default class UserSequelizeStorage { return null; } - return { - id: user.id, - email: user.email, - name: user.name, - createdAt: user.created_at, - photo: user.photo, - extensions: user.extensions, - }; + return user; } } diff --git a/src/repository/storage/postgres/orm/sequelize/userSession.ts b/src/repository/storage/postgres/orm/sequelize/userSession.ts index 317e1b07..e602b006 100644 --- a/src/repository/storage/postgres/orm/sequelize/userSession.ts +++ b/src/repository/storage/postgres/orm/sequelize/userSession.ts @@ -18,17 +18,17 @@ class UserSessionModel extends Model, InferCre /** * User id */ - public declare user_id: number; + public declare userId: number; /** * Refresh token */ - public declare refresh_token: string; + public declare refreshToken: string; /** * Refresh token expiration date */ - public declare refresh_token_expires_at: Date; + public declare refreshTokenExpiresAt: Date; } @@ -68,7 +68,7 @@ export default class UserSessionSequelizeStorage { autoIncrement: true, primaryKey: true, }, - user_id: { + userId: { type: DataTypes.INTEGER, allowNull: false, references: { @@ -76,12 +76,12 @@ export default class UserSessionSequelizeStorage { key: 'id', }, }, - refresh_token: { + refreshToken: { type: DataTypes.STRING, allowNull: false, unique: true, }, - refresh_token_expires_at: { + refreshTokenExpiresAt: { type: DataTypes.DATE, allowNull: false, }, @@ -101,18 +101,11 @@ export default class UserSessionSequelizeStorage { * @returns { UserSession } created user session */ public async create(userId: number, refreshToken: string, refreshTokenExpiresAt: Date): Promise { - const session = await this.model.create({ - user_id: userId, - refresh_token: refreshToken, - refresh_token_expires_at: refreshTokenExpiresAt, + return await this.model.create({ + userId: userId, + refreshToken: refreshToken, + refreshTokenExpiresAt: refreshTokenExpiresAt, }); - - return { - id: session.id, - userId: session.user_id, - refreshToken: session.refresh_token, - refreshTokenExpiresAt: session.refresh_token_expires_at, - }; } /** @@ -122,20 +115,9 @@ export default class UserSessionSequelizeStorage { * @returns { UserSession | null } found user session */ public async findByToken(token: string): Promise { - const session = await this.model.findOne({ - where: { refresh_token: token }, + return await this.model.findOne({ + where: { refreshToken: token }, }); - - if (!session) { - return null; - } - - return { - id: session.id, - userId: session.user_id, - refreshToken: session.refresh_token, - refreshTokenExpiresAt: session.refresh_token_expires_at, - }; } /** @@ -146,7 +128,9 @@ export default class UserSessionSequelizeStorage { */ public async removeByRefreshToken(refreshToken: string): Promise { await this.model.destroy({ - where: { refresh_token: refreshToken }, + where: { + refreshToken, + }, }); } } diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts index 0705c2ef..f3018149 100644 --- a/src/repository/user.repository.ts +++ b/src/repository/user.repository.ts @@ -1,9 +1,9 @@ import type User from '@domain/entities/user.js'; -import type { UserEditorTool } from '@domain/entities/userExtensions.js'; import type UserStorage from '@repository/storage/user.storage.js'; import type GoogleApiTransport from '@repository/transport/google-api/index.js'; import type GetUserInfoResponsePayload from '@repository/transport/google-api/types/GetUserInfoResponsePayload.js'; import type { AddUserToolOptions } from '@repository/storage/postgres/orm/sequelize/user'; +import type EditorTool from '@domain/entities/editorTools'; /** * OAuth provider @@ -27,7 +27,7 @@ interface RemoveUserEditorToolOptions { /** * Editor tool identifier */ - editorToolId: UserEditorTool['id']; + toolId: EditorTool['id']; } /** @@ -127,10 +127,10 @@ export default class UserRepository { * * @param options - identifiers of user and tool */ - public async addUserEditorTool({ userId, tool }: AddUserToolOptions): Promise { + public async addUserEditorTool({ userId, toolId }: AddUserToolOptions): Promise { await this.storage.addUserEditorTool({ userId, - tool, + toolId, }); } @@ -139,22 +139,22 @@ export default class UserRepository { * * @param options - identifiers of user and tool */ - public async removeUserEditorTool({ userId, editorToolId }: RemoveUserEditorToolOptions): Promise { + public async removeUserEditorTool({ userId, toolId }: RemoveUserEditorToolOptions): Promise { const user = await this.getUserById(userId); if (!user) { - throw new Error('There is no user with such userId'); + throw new Error('There is no user with such id'); } - const editorTool = user.extensions?.editorTools?.find(tool => tool.id === editorToolId); + const editorTool = user.editorTools?.find(id => id === toolId); - if (!editorTool) { - throw new Error('User has no tool with such editorToolId'); + if (editorTool === undefined) { + throw new Error('User has no tool with such id'); } await this.storage.removeUserEditorTool({ userId, - editorTool, + toolId: editorTool, }); } } diff --git a/src/tests/README.md b/src/tests/README.md index b0c5e08f..1c048b72 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -8,6 +8,15 @@ File `src/tests/utils/setup.ts` does necessary preparations to make in possible ## Test data Test data lies in `src/tests/test-data/` directory. If you are missing some data for your tests, fill free to modify existing files or add new ones. Just remember to support relations between data that is usually provided by foreign key mechanism. +## Authorization in test +Use `global.auth(userId)` for getting accessToken, which you can use in: +``` +headers: { + authorization: `Bearer ${accessToken}` +} +``` +in your fake requests + ## Writing tests Please, locate a test file near the file that it tests. Make sure test file name is the same as tested file name, but with `.test` suffix. API test file shoul follow such structure: diff --git a/src/tests/test-data/notes-settings.json b/src/tests/test-data/notes-settings.json deleted file mode 100644 index 822b37c9..00000000 --- a/src/tests/test-data/notes-settings.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "id": 1, - "note_id": 1, - "custom_hostname": "codex.so", - "is_public": true - } -] diff --git a/src/tests/test-data/notes.json b/src/tests/test-data/notes.json index 4fc09cbf..885ed5be 100644 --- a/src/tests/test-data/notes.json +++ b/src/tests/test-data/notes.json @@ -2,9 +2,418 @@ { "id": 1, "public_id": "note_1", - "creator_id": 1, + "creatorId": 1, "content": [], - "created_at": "2023-10-16T13:49:19.000Z", - "updated_at": "2023-10-16T13:49:19.000Z" + "createdAt": "2023-10-16T13:49:19.000Z", + "updatedAt": "2023-10-16T13:49:19.000Z" + }, + { + "id": 2, + "public_id": "Pq1T9vc23Q", + "creatorId": 1, + "content": [], + "createdAt": "2023-10-16T13:49:19.000Z", + "updatedAt": "2023-10-16T13:49:19.000Z" + }, + { + "id": 3, + "public_id": "73NdxFZ4k7", + "creatorId": 1, + "content": [], + "createdAt": "2023-10-16T13:49:19.000Z", + "updatedAt": "2023-10-16T13:49:19.000Z" + }, + { + "id": 4, + "public_id": "99999903", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:3.000Z", + "updatedAt": "2023-10-16T13:49:3.000Z" + }, + { + "id": 5, + "public_id": "99999904", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:4.000Z", + "updatedAt": "2023-10-16T13:49:4.000Z" + }, + { + "id": 6, + "public_id": "99999905", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:5.000Z", + "updatedAt": "2023-10-16T13:49:5.000Z" + }, + { + "id": 7, + "public_id": "99999906", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:6.000Z", + "updatedAt": "2023-10-16T13:49:6.000Z" + }, + { + "id": 8, + "public_id": "99999907", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:7.000Z", + "updatedAt": "2023-10-16T13:49:7.000Z" + }, + { + "id": 9, + "public_id": "99999908", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:8.000Z", + "updatedAt": "2023-10-16T13:49:8.000Z" + }, + { + "id": 10, + "public_id": "99999909", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:9.000Z", + "updatedAt": "2023-10-16T13:49:9.000Z" + }, + { + "id": 11, + "public_id": "99999910", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:10.000Z", + "updatedAt": "2023-10-16T13:49:10.000Z" + }, + { + "id": 12, + "public_id": "99999911", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:11.000Z", + "updatedAt": "2023-10-16T13:49:11.000Z" + }, + { + "id": 13, + "public_id": "99999912", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:12.000Z", + "updatedAt": "2023-10-16T13:49:12.000Z" + }, + { + "id": 14, + "public_id": "99999913", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:13.000Z", + "updatedAt": "2023-10-16T13:49:13.000Z" + }, + { + "id": 15, + "public_id": "99999914", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:14.000Z", + "updatedAt": "2023-10-16T13:49:14.000Z" + }, + { + "id": 16, + "public_id": "99999915", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:15.000Z", + "updatedAt": "2023-10-16T13:49:15.000Z" + }, + { + "id": 17, + "public_id": "99999916", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:16.000Z", + "updatedAt": "2023-10-16T13:49:16.000Z" + }, + { + "id": 18, + "public_id": "99999917", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:17.000Z", + "updatedAt": "2023-10-16T13:49:17.000Z" + }, + { + "id": 19, + "public_id": "99999918", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:18.000Z", + "updatedAt": "2023-10-16T13:49:18.000Z" + }, + { + "id": 20, + "public_id": "99999919", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:19.000Z", + "updatedAt": "2023-10-16T13:49:19.000Z" + }, + { + "id": 21, + "public_id": "99999920", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:20.000Z", + "updatedAt": "2023-10-16T13:49:20.000Z" + }, + { + "id": 22, + "public_id": "99999921", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:21.000Z", + "updatedAt": "2023-10-16T13:49:21.000Z" + }, + { + "id": 23, + "public_id": "99999922", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:22.000Z", + "updatedAt": "2023-10-16T13:49:22.000Z" + }, + { + "id": 24, + "public_id": "99999923", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:23.000Z", + "updatedAt": "2023-10-16T13:49:23.000Z" + }, + { + "id": 25, + "public_id": "99999924", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:24.000Z", + "updatedAt": "2023-10-16T13:49:24.000Z" + }, + { + "id": 26, + "public_id": "99999925", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:25.000Z", + "updatedAt": "2023-10-16T13:49:25.000Z" + }, + { + "id": 27, + "public_id": "99999926", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:26.000Z", + "updatedAt": "2023-10-16T13:49:26.000Z" + }, + { + "id": 28, + "public_id": "99999927", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:27.000Z", + "updatedAt": "2023-10-16T13:49:27.000Z" + }, + { + "id": 29, + "public_id": "99999928", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:28.000Z", + "updatedAt": "2023-10-16T13:49:28.000Z" + }, + { + "id": 30, + "public_id": "99999929", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:29.000Z", + "updatedAt": "2023-10-16T13:49:29.000Z" + }, + { + "id": 31, + "public_id": "99999930", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:30.000Z", + "updatedAt": "2023-10-16T13:49:30.000Z" + }, + { + "id": 32, + "public_id": "99999931", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:31.000Z", + "updatedAt": "2023-10-16T13:49:31.000Z" + }, + { + "id": 33, + "public_id": "99999932", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:32.000Z", + "updatedAt": "2023-10-16T13:49:32.000Z" + }, + { + "id": 34, + "public_id": "99999933", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:33.000Z", + "updatedAt": "2023-10-16T13:49:33.000Z" + }, + { + "id": 35, + "public_id": "99999934", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:34.000Z", + "updatedAt": "2023-10-16T13:49:34.000Z" + }, + { + "id": 36, + "public_id": "99999935", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:35.000Z", + "updatedAt": "2023-10-16T13:49:35.000Z" + }, + { + "id": 37, + "public_id": "99999936", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:36.000Z", + "updatedAt": "2023-10-16T13:49:36.000Z" + }, + { + "id": 38, + "public_id": "99999937", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:37.000Z", + "updatedAt": "2023-10-16T13:49:37.000Z" + }, + { + "id": 39, + "public_id": "99999938", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:38.000Z", + "updatedAt": "2023-10-16T13:49:38.000Z" + }, + { + "id": 40 + , + "public_id": "99999939", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:39.000Z", + "updatedAt": "2023-10-16T13:49:39.000Z" + }, + { + "id": 41, + "public_id": "99999940", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:40.000Z", + "updatedAt": "2023-10-16T13:49:40.000Z" + }, + { + "id": 42, + "public_id": "99999941", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:41.000Z", + "updatedAt": "2023-10-16T13:49:41.000Z" + }, + { + "id": 43, + "public_id": "99999942", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:42.000Z", + "updatedAt": "2023-10-16T13:49:42.000Z" + }, + { + "id": 44, + "public_id": "99999943", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:43.000Z", + "updatedAt": "2023-10-16T13:49:43.000Z" + }, + { + "id": 45, + "public_id": "99999944", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:44.000Z", + "updatedAt": "2023-10-16T13:49:44.000Z" + }, + { + "id": 46, + "public_id": "99999945", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:45.000Z", + "updatedAt": "2023-10-16T13:49:45.000Z" + }, + { + "id": 47, + "public_id": "99999946", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:46.000Z", + "updatedAt": "2023-10-16T13:49:46.000Z" + }, + { + "id": 48, + "public_id": "99999947", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:47.000Z", + "updatedAt": "2023-10-16T13:49:47.000Z" + }, + { + "id": 49, + "public_id": "99999948", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:48.000Z", + "updatedAt": "2023-10-16T13:49:48.000Z" + }, + { + "id": 50, + "public_id": "99999949", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:49.000Z", + "updatedAt": "2023-10-16T13:49:49.000Z" + }, + { + "id": 51, + "public_id": "99999950", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:50.000Z", + "updatedAt": "2023-10-16T13:49:50.000Z" + }, + { + "id": 52, + "public_id": "99999902", + "creatorId": 4, + "content": [], + "createdAt": "2023-10-16T13:49:2.000Z", + "updatedAt": "2023-10-16T13:49:2.000Z" } -] +] \ No newline at end of file diff --git a/src/tests/test-data/notesSettings.json b/src/tests/test-data/notesSettings.json new file mode 100644 index 00000000..299ce98e --- /dev/null +++ b/src/tests/test-data/notesSettings.json @@ -0,0 +1,22 @@ +[ + { + "id": 1, + "note_id": 1, + "custom_hostname": "codex.so", + "is_public": true + }, + + { + "id": 2, + "note_id": 2, + "custom_hostname": "codex.so", + "is_public": true + }, + + { + "id": 3, + "note_id": 3, + "custom_hostname": "codex.so", + "is_public": false + } +] diff --git a/src/tests/test-data/userSessions.json b/src/tests/test-data/userSessions.json new file mode 100644 index 00000000..a9d26410 --- /dev/null +++ b/src/tests/test-data/userSessions.json @@ -0,0 +1,8 @@ +[ + { + "id" : 1, + "user_id" : 4, + "refresh_token" : "IqrTkSKmel", + "refresh_token_expires_at" : "2025-11-21 19:19:40.911+03" + } +] diff --git a/src/tests/test-data/users.json b/src/tests/test-data/users.json index 2b993510..b92b4931 100644 --- a/src/tests/test-data/users.json +++ b/src/tests/test-data/users.json @@ -4,5 +4,12 @@ "email": "a@a.com", "name": "Test user 1", "created_at": "2023-10-16T13:49:19.000Z" + }, + { + "id": 4, + "email": "Egoramurin@gmail.com", + "name": "Егор Амурин", + "created_at": "2023-10-22 19:19:40.892+03", + "photo": "https://lh3.googleusercontent.com/a/ACg8ocL9_uBaC7XMFhosJZfIkDLC8tTm1GhtbvDfSbjf9eI2=s96-c" } ] diff --git a/src/tests/utils/insert-data.ts b/src/tests/utils/insert-data.ts index cbb3c590..fcb8aae2 100644 --- a/src/tests/utils/insert-data.ts +++ b/src/tests/utils/insert-data.ts @@ -1,7 +1,8 @@ import type SequelizeOrm from '@repository/storage/postgres/orm/index.js'; import users from '../test-data/users.json'; +import userSessions from '../test-data/userSessions.json'; import notes from '../test-data/notes.json'; -import noteSettings from '../test-data/notes-settings.json'; +import noteSettings from '../test-data/notesSettings.json'; /** * Fills in the database with users data @@ -14,6 +15,16 @@ async function insertUsers(db: SequelizeOrm): Promise { } } +/** + * Fills in the database with user sessions + * + * @param db - SequelizeOrm instance + */ +async function insertUserSessions(db: SequelizeOrm): Promise { + for (const userSession of userSessions) { + await db.connection.query(`INSERT INTO public.user_sessions (id, "user_id", "refresh_token", "refresh_token_expires_at") VALUES (${userSession.id}, ${userSession.user_id}, '${userSession.refresh_token}', '${userSession.refresh_token_expires_at}')`); + } +} /** * Fills in the database with notes data * @@ -21,7 +32,7 @@ async function insertUsers(db: SequelizeOrm): Promise { */ async function insertNotes(db: SequelizeOrm): Promise { for (const note of notes) { - await db.connection.query(`INSERT INTO public.notes (id, "public_id", "creator_id", "created_at", "updated_at") VALUES (${note.id}, '${note.public_id}', '${note.creator_id}', '${note.created_at}', '${note.updated_at}')`); + await db.connection.query(`INSERT INTO public.notes (id, "public_id", "creator_id", "created_at", "updated_at") VALUES (${note.id}, '${note.public_id}', '${note.creatorId}', '${note.createdAt}', '${note.updatedAt}')`); } } @@ -44,6 +55,7 @@ async function insertNoteSettings(db: SequelizeOrm): Promise { */ export async function insertData(db: SequelizeOrm): Promise { await insertUsers(db); + await insertUserSessions(db); await insertNotes(db); await insertNoteSettings(db); } diff --git a/src/tests/utils/setup.ts b/src/tests/utils/setup.ts index c445c631..c9be8c95 100644 --- a/src/tests/utils/setup.ts +++ b/src/tests/utils/setup.ts @@ -25,6 +25,15 @@ declare global { */ /* eslint-disable-next-line no-var */ var api: Api | undefined; + + /** + * Globally exposed method for creating accessToken using id + * Is accessed as 'global.server' in tests + * + * @param userId - id of the user that will be considered the author of the request + * @returns accessToken for authorization + */ + function auth(userId: number) : string; } /** @@ -50,6 +59,9 @@ beforeAll(async () => { await insertData(orm); global.api = api; + global.auth = (userId: number) => { + return domainServices.authService.signAccessToken({ id : userId }); + }; }, TIMEOUT); afterAll(async () => { diff --git a/tsconfig.json b/tsconfig.json index 93e90d57..407fef84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule":true, + "resolveJsonModule": true, "strict": true, "skipLibCheck": true, "paths": { @@ -15,7 +15,8 @@ "@presentation/*": ["presentation/*"], "@lib/*": ["lib/*"], "@domain/*": ["domain/*"], - "@repository/*": ["repository/*"] + "@repository/*": ["repository/*"], + "@tests/*": ["tests/*"] }, }, "include": [ diff --git a/vitest.config.js b/vitest.config.js index dcbc97f0..6539b273 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -14,6 +14,8 @@ export default defineConfig({ '@lib/': '/src/lib/', '@domain/': '/src/domain/', '@repository/': '/src/repository/', + '@tests/': '/src/tests/', }, }, + console: 'integratedTerminal', });