From 1a10862b68bf6de1facb35d5d6744d8ff2004b44 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Tue, 11 Nov 2025 14:53:57 +0000 Subject: [PATCH 01/24] fix: expose with the /api prefix --- .devcontainer/docker-compose.yml | 13 ++++++- .devcontainer/nginx/nginx.conf | 67 ++++++++++++++++++++++++++++++++ src/app.ts | 46 +++++++++++----------- src/langs/fr.json | 2 +- src/plugins/meta.ts | 4 +- src/plugins/swagger.ts | 2 +- 6 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 .devcontainer/nginx/nginx.conf diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 34cdd9e81c..97b1e4338b 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -45,7 +45,7 @@ services: # Overrides default command so things don't shut down after the process ends. command: sleep infinity ports: - - 3000:3000 + - 3001:3000 db: hostname: db @@ -185,5 +185,16 @@ services: ports: - "1080:1080" + nginx: + image: nginx:1.25-alpine + hostname: nginx + depends_on: + - core + ports: + - "3000:3000" # expose nginx on localhost:3000 + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + restart: unless-stopped + volumes: postgres-data: diff --git a/.devcontainer/nginx/nginx.conf b/.devcontainer/nginx/nginx.conf new file mode 100644 index 0000000000..a2d8f77ce2 --- /dev/null +++ b/.devcontainer/nginx/nginx.conf @@ -0,0 +1,67 @@ +# nginx.conf +# This Nginx config listens on port 3000 and appends /api to the incoming request path, +# then forwards it to the 'core' container on port 3000. +# It also preserves important headers and supports WebSocket upgrades. + +events { + worker_connections 1024; +} + +http { + # Basic proxy settings + proxy_redirect off; + proxy_buffering off; + client_max_body_size 50m; + + # Timeouts + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Forwarded headers + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream core_upstream { + server core:3000; # Docker DNS resolves the 'core' service name + keepalive 32; + } + + server { + listen 3000; + server_name _; + + # Health check endpoint + location = /health { + return 200 "ok (nginx)\n"; + add_header Content-Type text/plain; + } + + # Catch-all: append /api to request path and proxy to core + # Example: GET /users -> proxied as /api/users + location / { + # Rewrite the path by inserting /api right after the leading slash + # $request_uri includes path + args; we'll rewrite just the path and preserve query string + set $new_path /api$uri; + + # Proxy to core using the rewritten path; args preserved + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_http_version 1.1; + + # Use proxy_pass with a URI to keep our rewritten $new_path + # We construct the full URI using the variable and keep query string via $is_args$args + proxy_pass http://core_upstream$new_path$is_args$args; + } + } +} diff --git a/src/app.ts b/src/app.ts index cb8e3ae132..2c5782029f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -41,30 +41,30 @@ export default async function (instance: FastifyInstance): Promise { // register some dependencies manually registerDependencies(instance.log); - - await instance.register(fp(metaPlugin)); + await instance.register(metaPlugin); await instance.register(fp(passportPlugin)); - // need to be defined before member and item for auth check - - await instance.register(maintenancePlugin); - - await instance.register(fp(authPlugin)); - await instance.register(async (instance) => { - // core API modules - await instance - // the websockets plugin must be registered before but in the same scope as the apis - // otherwise tests somehow bypass mocking the authentication through jest.spyOn(app, 'verifyAuthentication') - .register(fp(websocketsPlugin), { - prefix: '/ws', - redis: { - channelName: 'graasp-realtime-updates', - connection: REDIS_CONNECTION, - }, - }) - .register(fp(MemberServiceApi)) - .register(fp(ItemServiceApi)) - .register(tagPlugin); - }); + await instance.register( + async (instance) => { + // need to be defined before member and item for auth check + await instance.register(fp(authPlugin)); + // core API modules + await instance + // the websockets plugin must be registered before but in the same scope as the apis + // otherwise tests somehow bypass mocking the authentication through jest.spyOn(app, 'verifyAuthentication') + .register(fp(websocketsPlugin), { + prefix: '/ws', + redis: { + channelName: 'graasp-realtime-updates', + connection: REDIS_CONNECTION, + }, + }) + .register(fp(MemberServiceApi)) + .register(fp(ItemServiceApi)) + .register(tagPlugin) + .register(maintenancePlugin); + }, + { prefix: '/api' }, + ); } diff --git a/src/langs/fr.json b/src/langs/fr.json index 4331777847..ac98ed95b4 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -2,7 +2,7 @@ "GREETINGS": "Merci de vous être inscrit-e sur notre plateforme.", "SIGN_UP_VERIFY_EMAIL": "Vérifiez votre e-mail en cliquant sur le bouton ci-dessous.", "SIGN_UP_TEXT": "Complétez l'inscription et connectez-vous.", - "IGNORE_EMAIL_IF_NOT_REQUESTED": "Si vous n'êtes pas l'auteur-e de cette action', vous pouvez ignorer cet e-mail.", + "IGNORE_EMAIL_IF_NOT_REQUESTED": "Si vous n'êtes pas l'auteur-e de cette action, vous pouvez ignorer cet e-mail.", "SIGN_IN_BUTTON_TEXT": "Se connecter", "SIGN_UP_BUTTON_TEXT": "S'inscrire", "SIGN_IN_TEXT": "Vous nous avez demandé un lien magique pour vous connecter rapidement. Le voici :", diff --git a/src/plugins/meta.ts b/src/plugins/meta.ts index 7cb35737dd..2b477605a6 100644 --- a/src/plugins/meta.ts +++ b/src/plugins/meta.ts @@ -90,7 +90,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { return 'OK' as const; }); - fastify.get('/status', async (_, reply) => { + fastify.get('/api/status', async (_, reply) => { const searchService = resolveDependency(SearchService); const api = new HealthyStatus().format(); const database = (await getDBStatusCheck(fastify.log)).format(); @@ -110,7 +110,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }; }); - fastify.get('/version', async (_, reply) => { + fastify.get('/api/version', async (_, reply) => { // allow request cross origin reply.header('Access-Control-Allow-Origin', '*'); return `${APP_VERSION} @ ${BUILD_TIMESTAMP}`; diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts index 3548556e52..2af683b267 100644 --- a/src/plugins/swagger.ts +++ b/src/plugins/swagger.ts @@ -85,7 +85,7 @@ export default async function (instance: FastifyInstance): Promise { }); await instance.register(swaggerUiPlugin, { - routePrefix: '/docs', + routePrefix: '/api/docs', transformSpecification: (swaggerObject, _request, _reply) => { return swaggerObject; }, From 599da4b54a23deba7f0292e0aa84df801336546e Mon Sep 17 00:00:00 2001 From: spaenleh Date: Tue, 11 Nov 2025 15:18:07 +0000 Subject: [PATCH 02/24] fix: upgrade sdk --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a9ac888df9..68e271c467 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@fastify/type-provider-typebox": "5.1.0", "@fastify/websocket": "11.0.2", "@graasp/etherpad-api": "2.1.1", - "@graasp/sdk": "5.16.4", + "@graasp/sdk": "5.16.5", "@rapideditor/country-coder": "5.4.0", "@sentry/node": "7.119.2", "@sentry/tracing": "7.120.3", diff --git a/yarn.lock b/yarn.lock index 831f59aed8..97a0c91954 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,9 +2585,9 @@ __metadata: languageName: node linkType: hard -"@graasp/sdk@npm:5.16.4": - version: 5.16.4 - resolution: "@graasp/sdk@npm:5.16.4" +"@graasp/sdk@npm:5.16.5": + version: 5.16.5 + resolution: "@graasp/sdk@npm:5.16.5" dependencies: "@faker-js/faker": "npm:9.9.0" filesize: "npm:10.1.6" @@ -2595,7 +2595,7 @@ __metadata: peerDependencies: date-fns: ^4.0.0 uuid: ^11.0.0 - checksum: 10/73e1fc0ba8b299b1f67bca02a14366e8088a681cd5dfc8dba6411aeca3e2daf00d9281248c2f6c2bf3d8f143bc6fd05403d90fabb5b03546746bc568b472aa92 + checksum: 10/be066539252aac129befa28fc95e23e2c0d10ab9fa25b2f079b124fccedcf14cb594a02fb2c05ccb537719e1e14a99809a50e04212661f1739bcd912f1d0b4d8 languageName: node linkType: hard @@ -8586,7 +8586,7 @@ __metadata: "@fastify/type-provider-typebox": "npm:5.1.0" "@fastify/websocket": "npm:11.0.2" "@graasp/etherpad-api": "npm:2.1.1" - "@graasp/sdk": "npm:5.16.4" + "@graasp/sdk": "npm:5.16.5" "@jest/globals": "npm:29.7.0" "@quobix/vacuum": "npm:0.18.5" "@rapideditor/country-coder": "npm:5.4.0" From eb88780ac9df37171d9aa024e631bcb3b32ba03e Mon Sep 17 00:00:00 2001 From: spaenleh Date: Tue, 11 Nov 2025 15:40:39 +0000 Subject: [PATCH 03/24] fix: add api prefix to routes --- src/services/auth/auth.service.ts | 4 +- .../magicLink.controller.register.test.ts | 12 ++-- .../magicLink/magicLink.controller.test.ts | 12 ++-- .../password/password.controller.test.ts | 66 ++++++++--------- .../item/item.controller.read.test.ts | 14 ++-- .../item.controller.s3.regression.test.ts | 2 +- .../item/item.controller.update.test.ts | 70 +++++++++---------- .../plugins/app/appItem.controller.test.ts | 8 +-- .../document/document.controller.test.ts | 14 ++-- .../plugins/document/document.legacy.test.ts | 10 +-- .../embeddedLink/link.controller.test.ts | 18 ++--- .../etherpad/etherpad.controller.test.ts | 16 ++--- .../plugins/file/itemFile.controller.test.ts | 4 +- .../plugins/folder/folder.controller.test.ts | 10 +-- .../itemGeolocation.controller.test.ts | 4 +- .../importExport.controller.test.ts | 16 ++--- .../invitation/invitation.controller.test.ts | 4 +- .../itemLike/itemLike.controller.test.ts | 12 ++-- .../item/plugins/page/page.controller.test.ts | 16 ++--- .../plugins/search/search.controller.test.ts | 8 +-- .../recycled/recycled.controller.test.ts | 18 ++--- .../shortcut/shortcut.controller.test.ts | 12 ++-- .../member.controller.email.storage.test.ts | 50 ++++++------- src/services/member/member.controller.test.ts | 12 ++-- .../action/memberAction.controller.test.ts | 2 +- src/services/member/test/register.test.ts | 12 ++-- 26 files changed, 213 insertions(+), 213 deletions(-) diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index a07b16ab03..e5c81a71f4 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -38,7 +38,7 @@ export class AuthService { const redirectionUrl = getRedirectionLink(this.log, url); const domain = PUBLIC_URL; - const destination = new URL('/auth', domain); + const destination = new URL('/api/auth', domain); destination.searchParams.set(SHORT_TOKEN_PARAM, token); destination.searchParams.set('url', redirectionUrl); const link = destination.toString(); @@ -73,7 +73,7 @@ export class AuthService { const redirectionUrl = getRedirectionLink(this.log, url); const domain = PUBLIC_URL; - const destination = new URL('/auth', domain); + const destination = new URL('/api/auth', domain); destination.searchParams.set(SHORT_TOKEN_PARAM, token); destination.searchParams.set('url', redirectionUrl); const link = destination.toString(); diff --git a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts index 3af03285a2..69a497d2a7 100644 --- a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts +++ b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts @@ -61,7 +61,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -182,7 +182,7 @@ describe('POST /register', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { ...member, captcha: MOCK_CAPTCHA }, }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); @@ -203,7 +203,7 @@ describe('POST /register', () => { const name = 'anna'; const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -219,7 +219,7 @@ describe('POST /register', () => { const name = '
%"^'; const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -238,7 +238,7 @@ describe('POST /register', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email: invitation.email, name: 'some-name', captcha: MOCK_CAPTCHA }, }); const member = await getMemberByEmail(invitation.email); @@ -268,7 +268,7 @@ describe('POST /register', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name: 'some-name', captcha: MOCK_CAPTCHA }, }); diff --git a/src/services/auth/plugins/magicLink/magicLink.controller.test.ts b/src/services/auth/plugins/magicLink/magicLink.controller.test.ts index 9b108b173c..dc715f2f63 100644 --- a/src/services/auth/plugins/magicLink/magicLink.controller.test.ts +++ b/src/services/auth/plugins/magicLink/magicLink.controller.test.ts @@ -69,7 +69,7 @@ describe('Auth routes tests', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: '/login', + url: '/api/login', payload: { email: member.email, captcha: MOCK_CAPTCHA }, }); @@ -102,7 +102,7 @@ describe('Auth routes tests', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: '/login', + url: '/api/login', payload: { email, captcha: MOCK_CAPTCHA }, }); @@ -114,7 +114,7 @@ describe('Auth routes tests', () => { const email = 'wrongemail'; const response = await app.inject({ method: HttpMethod.Post, - url: '/login', + url: '/api/login', payload: { email, captcha: MOCK_CAPTCHA }, }); @@ -127,7 +127,7 @@ describe('Auth routes tests', () => { const email = faker.internet.email(); const response = await app.inject({ method: HttpMethod.Post, - url: '/login', + url: '/api/login', payload: { email, captcha: MOCK_CAPTCHA }, }); @@ -224,7 +224,7 @@ describe('Auth routes tests', () => { it('Authenticate successfully', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/logout', + url: '/api/logout', }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); }); @@ -240,7 +240,7 @@ describe('Auth routes tests', () => { const responseRegister = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); expect(responseRegister.statusCode).toBe(StatusCodes.NO_CONTENT); diff --git a/src/services/auth/plugins/password/password.controller.test.ts b/src/services/auth/plugins/password/password.controller.test.ts index 99eb51bf64..cf70b26c62 100644 --- a/src/services/auth/plugins/password/password.controller.test.ts +++ b/src/services/auth/plugins/password/password.controller.test.ts @@ -38,7 +38,7 @@ async function login( mockCaptchaValidationOnce(RecaptchaAction.SignInWithPassword); return app.inject({ method: 'POST', - url: '/login-password', + url: '/api/login-password', payload: { email, password, @@ -87,7 +87,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: pwd, @@ -116,7 +116,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: pwd, @@ -140,7 +140,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: pwd, @@ -164,7 +164,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: pwd, @@ -182,7 +182,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: wrongPassword, captcha: MOCK_CAPTCHA }, }); expect(response.statusCode).toEqual(StatusCodes.UNAUTHORIZED); @@ -197,7 +197,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password, captcha: MOCK_CAPTCHA }, }); expect(response.statusCode).toEqual(StatusCodes.NOT_ACCEPTABLE); @@ -209,7 +209,7 @@ describe('Password', () => { const password = faker.internet.password({ prefix: '!1Aa' }); const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email, password, captcha: MOCK_CAPTCHA }, }); @@ -223,7 +223,7 @@ describe('Password', () => { const password = '1234'; const response = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email, password, captcha: MOCK_CAPTCHA }, }); @@ -247,7 +247,7 @@ describe('Password', () => { mockCaptchaValidationOnce(RecaptchaAction.ResetPassword); const response = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: member.email, captcha: MOCK_CAPTCHA, @@ -266,7 +266,7 @@ describe('Password', () => { mockCaptchaValidationOnce(RecaptchaAction.ResetPassword); const response = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: faker.internet.email().toLowerCase(), captcha: MOCK_CAPTCHA, @@ -285,7 +285,7 @@ describe('Password', () => { mockCaptchaValidationOnce(RecaptchaAction.ResetPassword); const response = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: actor.email, captcha: MOCK_CAPTCHA, @@ -304,7 +304,7 @@ describe('Password', () => { mockCaptchaValidationOnce(RecaptchaAction.SignIn); const response = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: actor.email, captcha: 'bad captcha', @@ -339,7 +339,7 @@ describe('Password', () => { // insert request in caching for patch to work const response = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: actor.email, captcha: MOCK_CAPTCHA, @@ -359,7 +359,7 @@ describe('Password', () => { const newPassword = faker.internet.password({ prefix: '!1Aa' }); const responseReset = await app.inject({ method: 'PATCH', - url: '/password/reset', + url: '/api/password/reset', payload: { password: newPassword, }, @@ -400,7 +400,7 @@ describe('Password', () => { // token should be single use const responseSecondReset = await app.inject({ method: 'PATCH', - url: '/password/reset', + url: '/api/password/reset', payload: { password: `${newPassword}a`, }, @@ -415,7 +415,7 @@ describe('Password', () => { const newPassword = faker.internet.password({ prefix: '!1Aa' }); const response = await app.inject({ method: 'PATCH', - url: '/password/reset', + url: '/api/password/reset', payload: { password: newPassword, }, @@ -434,7 +434,7 @@ describe('Password', () => { const newPassword = faker.internet.password({ prefix: '!1Aa' }); const response = await app.inject({ method: 'PATCH', - url: '/password/reset', + url: '/api/password/reset', payload: { password: newPassword, }, @@ -466,7 +466,7 @@ describe('Password', () => { mockCaptchaValidationOnce(RecaptchaAction.ResetPassword); const responseCreateReset = await app.inject({ method: 'POST', - url: '/password/reset', + url: '/api/password/reset', payload: { email: expiredMember.email, captcha: MOCK_CAPTCHA, @@ -487,7 +487,7 @@ describe('Password', () => { const newPassword = faker.internet.password({ prefix: '!1Aa' }); const responseReset = await app.inject({ method: 'PATCH', - url: '/password/reset', + url: '/api/password/reset', payload: { password: newPassword, }, @@ -508,7 +508,7 @@ describe('Password', () => { it('Throws when signed out', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/password', + url: '/api/password', payload: { password: faker.internet.password({ prefix: '!1Aa' }), }, @@ -524,7 +524,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Post, - url: '/password', + url: '/api/password', payload: { password: newPassword, }, @@ -548,7 +548,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Post, - url: '/password', + url: '/api/password', payload: { password: newPassword, }, @@ -571,7 +571,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Post, - url: '/password', + url: '/api/password', payload: { password: newPassword, }, @@ -586,7 +586,7 @@ describe('Password', () => { const currentPassword = faker.internet.password({ prefix: '!1Aa' }); const response = await app.inject({ method: HttpMethod.Patch, - url: '/password', + url: '/api/password', payload: { currentPassword, password: newPassword, @@ -604,7 +604,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Patch, - url: '/password', + url: '/api/password', payload: { currentPassword, password: newPassword, @@ -630,7 +630,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Patch, - url: '/password', + url: '/api/password', payload: { currentPassword, password: newPassword, @@ -654,7 +654,7 @@ describe('Password', () => { const newPassword = faker.internet.password({ prefix: '!1Aa' }); const response = await app.inject({ method: HttpMethod.Patch, - url: '/password', + url: '/api/password', payload: { currentPassword: '', password: newPassword, @@ -671,7 +671,7 @@ describe('Password', () => { mockAuthenticate(currentMember); const response = await app.inject({ method: HttpMethod.Patch, - url: '/password', + url: '/api/password', payload: { currentPassword, password: 'weak', @@ -685,7 +685,7 @@ describe('Password', () => { it('Throws when signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/password/status', + url: '/api/members/current/password/status', }); expect(response.statusCode).toEqual(StatusCodes.UNAUTHORIZED); }); @@ -698,7 +698,7 @@ describe('Password', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/password/status', + url: '/api/members/current/password/status', }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.json()).toEqual({ hasPassword: false }); @@ -711,7 +711,7 @@ describe('Password', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/password/status', + url: '/api/members/current/password/status', }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.json()).toEqual({ hasPassword: true }); @@ -732,7 +732,7 @@ describe('Password', () => { // login const loginResponse = await app.inject({ method: HttpMethod.Post, - url: '/login-password', + url: '/api/login-password', payload: { email: actor.email, password: pwd, captcha: MOCK_CAPTCHA }, }); expect(loginResponse.statusCode).toEqual(StatusCodes.NO_CONTENT); diff --git a/src/services/item/item.controller.read.test.ts b/src/services/item/item.controller.read.test.ts index 71ff7afa4d..9a3ba18636 100644 --- a/src/services/item/item.controller.read.test.ts +++ b/src/services/item/item.controller.read.test.ts @@ -188,7 +188,7 @@ describe('Item routes tests', () => { it('Bad Request for invalid id', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/invalid-id', + url: '/api/items/invalid-id', }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -278,7 +278,7 @@ describe('Item routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/accessible', + url: '/api/items/accessible', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -342,7 +342,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/accessible', + url: '/api/items/accessible', }); expect(response.statusCode).toBe(StatusCodes.OK); const { data } = response.json<{ data: PackedItem[] }>(); @@ -405,7 +405,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/accessible', + url: '/api/items/accessible', }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -1087,7 +1087,7 @@ describe('Item routes tests', () => { it('Bad Request for invalid id', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/invalid-id/children', + url: '/api/items/invalid-id/children', }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -1292,7 +1292,7 @@ describe('Item routes tests', () => { it('Bad Request for invalid id', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/invalid-id/descendants', + url: '/api/items/invalid-id/descendants', }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -1393,7 +1393,7 @@ describe('Item routes tests', () => { it('Bad Request for invalid id', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/invalid-id/parents', + url: '/api/items/invalid-id/parents', }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); diff --git a/src/services/item/item.controller.s3.regression.test.ts b/src/services/item/item.controller.s3.regression.test.ts index 802bdb352b..9d78cfc7ed 100644 --- a/src/services/item/item.controller.s3.regression.test.ts +++ b/src/services/item/item.controller.s3.regression.test.ts @@ -94,7 +94,7 @@ describe('Item routes tests', () => { // server should still not be dead const otherCall = await app.inject({ method: HttpMethod.Get, - url: '/version', + url: '/api/version', }); expect(otherCall.statusCode).toBe(StatusCodes.OK); }); diff --git a/src/services/item/item.controller.update.test.ts b/src/services/item/item.controller.update.test.ts index ab326e2043..8feb59047a 100644 --- a/src/services/item/item.controller.update.test.ts +++ b/src/services/item/item.controller.update.test.ts @@ -110,7 +110,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -148,7 +148,7 @@ describe('Item routes tests', () => { const newItem = FolderItemFactory({ name: '' }); const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload: newItem, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -158,7 +158,7 @@ describe('Item routes tests', () => { const newItem1 = FolderItemFactory({ name: ' ' }); const response1 = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload: newItem1, }); expect(response1.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -170,7 +170,7 @@ describe('Item routes tests', () => { const newItem = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload: { ...newItem, type: 'invalid-type' }, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -223,7 +223,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -948,7 +948,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: '/items/invalid-id', + url: '/api/items/invalid-id', payload, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -1039,7 +1039,7 @@ describe('Item routes tests', () => { } = await seedFromJson({ actor: null, items: [{}] }); const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: [item.id] }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -1067,7 +1067,7 @@ describe('Item routes tests', () => { const items = [item1, item2]; const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: items.map(({ id }) => id) }, }); expect(response.json()).toEqual(items.map(({ id }) => id)); @@ -1101,7 +1101,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: [item1.id] }, }); expect(response.json()).toEqual([item1.id]); @@ -1141,7 +1141,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: [parent.id] }, }); expect(response.json()).toEqual([parent.id]); @@ -1192,7 +1192,7 @@ describe('Item routes tests', () => { const items = [item1, item2]; const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: items.map(({ id }) => id) }, }); expect(response.json()).toEqual(items.map(({ id }) => id)); @@ -1231,7 +1231,7 @@ describe('Item routes tests', () => { const items = [item1, item2]; const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: items.map(({ id }) => id) }, }); expect(response.json()).toEqual(items.map(({ id }) => id)); @@ -1264,7 +1264,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: [...items.map(({ id }) => id), 'invalid-id'] }, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -1287,7 +1287,7 @@ describe('Item routes tests', () => { const missingId = uuidv4(); const response = await app.inject({ method: HttpMethod.Delete, - url: '/items', + url: '/api/items', query: { id: [...itemIds, missingId] }, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -1479,7 +1479,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: [item.id] }, payload: { parentId: parent.id, @@ -1509,7 +1509,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/api/items/move', query: { id: items.map(({ id }) => id) }, payload: { parentId: parent.id, @@ -1556,7 +1556,7 @@ describe('Item routes tests', () => { const items = [item1, item2, item3]; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: items.map(({ id }) => id) }, payload: {}, }); @@ -1594,7 +1594,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: item.id }, payload: {}, }); @@ -1635,7 +1635,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: item.id }, payload: { parentId: parentItem.id }, }); @@ -1678,7 +1678,7 @@ describe('Item routes tests', () => { const items = [item1, item2, item3]; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: items.map(({ id }) => id) }, payload: { parentId: parentItem.id, @@ -1718,7 +1718,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: items.map(({ id }) => id) }, payload: { parentId: parentItem.id, @@ -1746,7 +1746,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: [uuidv4(), 'invalid-id'] }, payload: {}, }); @@ -1771,7 +1771,7 @@ describe('Item routes tests', () => { const websocketMock = jest.spyOn(WebsocketService.prototype, 'publish'); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: [uuidv4()] }, payload: { parentId: parentItem.id, @@ -1799,7 +1799,7 @@ describe('Item routes tests', () => { } = await seedFromJson({ items: [{}] }); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [item.id] }, payload: {}, }); @@ -1836,7 +1836,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: items.map(({ id }) => id) }, payload: {}, }); @@ -1905,7 +1905,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: items.map(({ id }) => id) }, payload: { parentId: targetItem.id, @@ -1967,7 +1967,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: items.map(({ id }) => id) }, payload: { parentId: targetItem.id, @@ -2026,7 +2026,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: item.id }, payload: {}, }); @@ -2075,7 +2075,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: items.map(({ id }) => id) }, payload: {}, }); @@ -2123,7 +2123,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: items.map(({ id }) => id) }, payload: { parentId: parentItem.id, @@ -2163,7 +2163,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [item.id] }, payload: { parentId: parentItem.id, @@ -2211,7 +2211,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [parentItem.id] }, payload: { parentId: targetItem.id, @@ -2252,7 +2252,7 @@ describe('Item routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [...items.map(({ id }) => id), 'invalid-id'] }, payload: {}, }); @@ -2280,7 +2280,7 @@ describe('Item routes tests', () => { const websocketMock = jest.spyOn(WebsocketService.prototype, 'publish'); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [item.id, missingId] }, payload: { parentId: target.id }, }); @@ -2323,7 +2323,7 @@ describe('Item routes tests', () => { const websocketMock = jest.spyOn(WebsocketService.prototype, 'publish'); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/copy', + url: '/api/items/copy', query: { id: [item.id] }, payload: { parentId: parentItem.id, diff --git a/src/services/item/plugins/app/appItem.controller.test.ts b/src/services/item/plugins/app/appItem.controller.test.ts index 46f4bcca6c..dd20516dfa 100644 --- a/src/services/item/plugins/app/appItem.controller.test.ts +++ b/src/services/item/plugins/app/appItem.controller.test.ts @@ -43,7 +43,7 @@ describe('App Item tests', () => { const payload = { name: 'name', url: MOCK_URL }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/apps', + url: '/api/items/apps', payload, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -59,7 +59,7 @@ describe('App Item tests', () => { const payload = { name: 'name', url: MOCK_URL, description: 'description' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/apps', + url: '/api/items/apps', payload, }); @@ -104,7 +104,7 @@ describe('App Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/apps', + url: '/api/items/apps', payload, }); @@ -124,7 +124,7 @@ describe('App Item tests', () => { const response1 = await app.inject({ method: HttpMethod.Post, - url: '/items/apps', + url: '/api/items/apps', payload: payload1, }); diff --git a/src/services/item/plugins/document/document.controller.test.ts b/src/services/item/plugins/document/document.controller.test.ts index 5bfd858163..003a4862ff 100644 --- a/src/services/item/plugins/document/document.controller.test.ts +++ b/src/services/item/plugins/document/document.controller.test.ts @@ -37,7 +37,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -52,7 +52,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -67,7 +67,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -82,7 +82,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: '' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -97,7 +97,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content', isRaw: 'value' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -112,7 +112,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content', flavor: 'value' }; const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload, }); @@ -128,7 +128,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/documents', + url: '/api/items/documents', payload: { name: 'name', content: 'content' }, }); diff --git a/src/services/item/plugins/document/document.legacy.test.ts b/src/services/item/plugins/document/document.legacy.test.ts index 48c011093f..de36fab2e4 100644 --- a/src/services/item/plugins/document/document.legacy.test.ts +++ b/src/services/item/plugins/document/document.legacy.test.ts @@ -50,7 +50,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -66,7 +66,7 @@ describe('Document Item tests', () => { const payload = DocumentItemFactory({ extra }); const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -97,7 +97,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -114,7 +114,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload, }); @@ -131,7 +131,7 @@ describe('Document Item tests', () => { const response1 = await app.inject({ method: HttpMethod.Post, - url: '/items', + url: '/api/items', payload: payload1, }); diff --git a/src/services/item/plugins/embeddedLink/link.controller.test.ts b/src/services/item/plugins/embeddedLink/link.controller.test.ts index 0b4fdf9bc7..f0ad645647 100644 --- a/src/services/item/plugins/embeddedLink/link.controller.test.ts +++ b/src/services/item/plugins/embeddedLink/link.controller.test.ts @@ -54,7 +54,7 @@ describe('Tests Embedded Link Controller', () => { }); describe('GET /items/embedded-links/metadata', () => { - const URL = '/items/embedded-links/metadata'; + const URL = '/api/items/embedded-links/metadata'; it('Throws if signed out', async () => { const response = await app.inject({ @@ -151,7 +151,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if name is undefined', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { url: MOCK_URL }, }); @@ -160,7 +160,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if name is invalid', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: '', url: MOCK_URL }, }); @@ -169,7 +169,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if url is undefined', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: 'n' }, }); @@ -178,7 +178,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if url is invalid', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: 'name', url: 'url' }, }); @@ -224,7 +224,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: 'name', url: MOCK_URL }, }); @@ -237,7 +237,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: 'name', url: MOCK_URL }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -253,7 +253,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', payload: { name: 'name', url: MOCK_URL }, }); @@ -276,7 +276,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/embedded-links', + url: '/api/items/embedded-links', query: { parentId: parentItem.id, previousItemId: previousItem.id, diff --git a/src/services/item/plugins/etherpad/etherpad.controller.test.ts b/src/services/item/plugins/etherpad/etherpad.controller.test.ts index 0d183646a8..f9dc3d0f85 100644 --- a/src/services/item/plugins/etherpad/etherpad.controller.test.ts +++ b/src/services/item/plugins/etherpad/etherpad.controller.test.ts @@ -54,7 +54,7 @@ const createEtherpad = async (app, parentId?: string) => { }); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload: { name: faker.word.sample(), }, @@ -103,7 +103,7 @@ describe('Etherpad service API', () => { }; const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload, }); @@ -139,7 +139,7 @@ describe('Etherpad service API', () => { const name = faker.word.sample(); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload: { name, readerPermission: EtherpadPermission.Write, @@ -178,7 +178,7 @@ describe('Etherpad service API', () => { }); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload: { name: faker.word.sample(), }, @@ -209,7 +209,7 @@ describe('Etherpad service API', () => { }); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload: { name: faker.word.sample(), }, @@ -244,7 +244,7 @@ describe('Etherpad service API', () => { }); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/etherpad/create', + url: '/api/items/etherpad/create', payload: { name: faker.word.sample(), }, @@ -965,7 +965,7 @@ describe('Etherpad service API', () => { }); await app.inject({ method: 'POST', - url: '/items/copy', + url: '/api/items/copy', query: { id: [item.id], }, @@ -1011,7 +1011,7 @@ describe('Etherpad service API', () => { }); const res = await app.inject({ method: 'POST', - url: '/items/copy', + url: '/api/items/copy', query: { id: [bogusItem.id], }, diff --git a/src/services/item/plugins/file/itemFile.controller.test.ts b/src/services/item/plugins/file/itemFile.controller.test.ts index 0c6b8d7a67..5d66345d1f 100644 --- a/src/services/item/plugins/file/itemFile.controller.test.ts +++ b/src/services/item/plugins/file/itemFile.controller.test.ts @@ -126,7 +126,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/upload', + url: '/api/items/upload', payload: form, headers: form.getHeaders(), }); @@ -1044,7 +1044,7 @@ describe('File Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: '/items/files/invalid-id', + url: '/api/items/files/invalid-id', payload, }); diff --git a/src/services/item/plugins/folder/folder.controller.test.ts b/src/services/item/plugins/folder/folder.controller.test.ts index c11fbaa433..02e8a24244 100644 --- a/src/services/item/plugins/folder/folder.controller.test.ts +++ b/src/services/item/plugins/folder/folder.controller.test.ts @@ -99,7 +99,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/folders', + url: '/api/items/folders', payload, }); @@ -135,7 +135,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/folders', + url: '/api/items/folders', payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -496,7 +496,7 @@ describe('Folder routes tests', () => { const newItem = FolderItemFactory({ name: '' }); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/folders', + url: '/api/items/folders', payload: newItem, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -506,7 +506,7 @@ describe('Folder routes tests', () => { const newItem1 = FolderItemFactory({ name: ' ' }); const response1 = await app.inject({ method: HttpMethod.Post, - url: '/items/folders', + url: '/api/items/folders', payload: newItem1, }); expect(response1.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -865,7 +865,7 @@ describe('Folder routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: '/items/folders/invalid-id', + url: '/api/items/folders/invalid-id', payload, }); diff --git a/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts b/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts index 34d658574f..daa160a595 100644 --- a/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts +++ b/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts @@ -695,7 +695,7 @@ describe('Item Geolocation', () => { if (GEOLOCATION_API_HOST) { nock(GEOLOCATION_API_HOST) - .get('/reverse') + .get('/api/reverse') .query(true) .reply(200, { results: [{ formatted: 'address', country: 'country' }], @@ -762,7 +762,7 @@ describe('Item Geolocation', () => { const id2 = uuid(); if (GEOLOCATION_API_HOST) { nock(GEOLOCATION_API_HOST) - .get('/search') + .get('/api/search') .query(true) .reply(200, { results: [ diff --git a/src/services/item/plugins/importExport/importExport.controller.test.ts b/src/services/item/plugins/importExport/importExport.controller.test.ts index 3069b1c4aa..ce4e526d1d 100644 --- a/src/services/item/plugins/importExport/importExport.controller.test.ts +++ b/src/services/item/plugins/importExport/importExport.controller.test.ts @@ -218,7 +218,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, headers: form.getHeaders(), }); @@ -229,7 +229,7 @@ describe('ZIP routes tests', () => { // get actor's root items const rootItemResponse = await app.inject({ method: HttpMethod.Get, - url: '/items/accessible', + url: '/api/items/accessible', }); const rootItems = rootItemResponse.json().data; expect(rootItems).toHaveLength(7); @@ -291,7 +291,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, query: { parentId: parentItem.id }, headers: form.getHeaders(), @@ -354,7 +354,7 @@ describe('ZIP routes tests', () => { const form = createFormData('empty.zip'); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, headers: form.getHeaders(), query: { parentId: parentItem.id }, @@ -383,7 +383,7 @@ describe('ZIP routes tests', () => { const form = createFormData('htmlAndText.zip'); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, headers: form.getHeaders(), query: { parentId: parentItem.id }, @@ -437,7 +437,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, headers: form.getHeaders(), }); @@ -466,7 +466,7 @@ describe('ZIP routes tests', () => { const form = createFormData('graasp-archive.zip'); const importResponse = await app.inject({ method: HttpMethod.Post, - url: '/items/zip-import', + url: '/api/items/zip-import', payload: form, headers: form.getHeaders(), query: { parentId: parentItem.id }, @@ -633,7 +633,7 @@ describe('ZIP routes tests', () => { const h5pUploadResponse = await app.inject({ method: HttpMethod.Post, - url: '/items/h5p-import', + url: '/api/items/h5p-import', payload: form, headers: form.getHeaders(), }); diff --git a/src/services/item/plugins/invitation/invitation.controller.test.ts b/src/services/item/plugins/invitation/invitation.controller.test.ts index 4d46f24216..329e822dc5 100644 --- a/src/services/item/plugins/invitation/invitation.controller.test.ts +++ b/src/services/item/plugins/invitation/invitation.controller.test.ts @@ -938,7 +938,7 @@ describe('Invitation Plugin', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email: invitation.email, name: 'some-name', captcha: MOCK_CAPTCHA }, }); const member = await db.query.accountsTable.findFirst({ @@ -987,7 +987,7 @@ describe('Invitation Plugin', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email: faker.internet.email(), name: 'some-name', captcha: MOCK_CAPTCHA }, }); await new Promise((done) => { diff --git a/src/services/item/plugins/itemLike/itemLike.controller.test.ts b/src/services/item/plugins/itemLike/itemLike.controller.test.ts index 0213968d44..838130c80f 100644 --- a/src/services/item/plugins/itemLike/itemLike.controller.test.ts +++ b/src/services/item/plugins/itemLike/itemLike.controller.test.ts @@ -79,7 +79,7 @@ describe('Item Like', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/liked', + url: '/api/items/liked', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -96,7 +96,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/liked', + url: '/api/items/liked', }); expect(res.statusCode).toBe(StatusCodes.OK); @@ -118,7 +118,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/liked', + url: '/api/items/liked', }); expect(res.statusCode).toBe(StatusCodes.OK); @@ -276,7 +276,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/invalid-id/likes', + url: '/api/items/invalid-id/likes', }); expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST); }); @@ -386,7 +386,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Post, - url: '/items/invalid-id/like', + url: '/api/items/invalid-id/like', }); expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST); }); @@ -492,7 +492,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Delete, - url: '/items/invalid-id/like', + url: '/api/items/invalid-id/like', }); expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST); }); diff --git a/src/services/item/plugins/page/page.controller.test.ts b/src/services/item/plugins/page/page.controller.test.ts index 90c05a7c9b..812f11a219 100644 --- a/src/services/item/plugins/page/page.controller.test.ts +++ b/src/services/item/plugins/page/page.controller.test.ts @@ -43,7 +43,7 @@ async function getAppPort(app: FastifyInstance) { async function expectServerToBeResponsive(app: FastifyInstance) { const result = await app.inject({ method: 'GET', - url: '/version', + url: '/api/version', }); expect(result.statusCode).toEqual(StatusCodes.OK); } @@ -60,7 +60,7 @@ async function connectToItemWs( const doc = new Doc(); const provider = new WebsocketProvider( `ws://localhost:${port}`, - `items/pages/${itemId}/ws${readOnly ? '/read' : ''}`, + `/api/items/pages/${itemId}/ws${readOnly ? '/read' : ''}`, doc, { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -94,7 +94,7 @@ describe('Page routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/pages', + url: '/api/items/pages', payload, }); @@ -110,7 +110,7 @@ describe('Page routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/pages', + url: '/api/items/pages', payload: { name: 'my page' }, }); expect(response.statusCode).toBe(StatusCodes.CREATED); @@ -140,7 +140,7 @@ describe('Page routes tests', () => { method: HttpMethod.Get, path: { protocol: 'ws', - pathname: '/items/pages/wrong-id/ws', + pathname: '/api/items/pages/wrong-id/ws', }, }); @@ -436,7 +436,7 @@ describe('Page routes tests', () => { method: HttpMethod.Get, path: { protocol: 'ws', - pathname: '/items/pages/wrong-id/ws/read', + pathname: '/api/items/pages/wrong-id/ws/read', }, }); @@ -832,7 +832,7 @@ describe('Page routes tests', () => { await app.inject({ method: HttpMethod.Post, path: { - pathname: '/items/copy', + pathname: '/api/items/copy', query: { id: item.id }, }, }); @@ -879,7 +879,7 @@ describe('Page routes tests', () => { const copyOp = await app.inject({ method: HttpMethod.Post, path: { - pathname: '/items/copy', + pathname: '/api/items/copy', query: { id: folder.id }, }, payload: {}, diff --git a/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts b/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts index bac109cac0..0a98343b4a 100644 --- a/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts +++ b/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts @@ -290,7 +290,7 @@ describe('Collection Search endpoints', () => { const move1 = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: item.id }, payload: { parentId: unpublishedFolder.id, @@ -332,7 +332,7 @@ describe('Collection Search endpoints', () => { const move2 = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: item.id }, payload: { parentId: publishedFolder.id, @@ -368,7 +368,7 @@ describe('Collection Search endpoints', () => { const move3 = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: unpublishedItem.id }, payload: { parentId: publishedFolder.id, @@ -404,7 +404,7 @@ describe('Collection Search endpoints', () => { const move4 = await app.inject({ method: HttpMethod.Post, - url: '/items/move', + url: '/api/items/move', query: { id: unpublishedItem.id }, payload: { parentId: unpublishedFolder.id, diff --git a/src/services/item/plugins/recycled/recycled.controller.test.ts b/src/services/item/plugins/recycled/recycled.controller.test.ts index 3ec1822b46..8987cc2db4 100644 --- a/src/services/item/plugins/recycled/recycled.controller.test.ts +++ b/src/services/item/plugins/recycled/recycled.controller.test.ts @@ -44,7 +44,7 @@ describe('Recycle Bin Tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/items/recycled', + url: '/api/items/recycled', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -80,7 +80,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/recycled', + url: '/api/items/recycled', }); const response = res.json(); @@ -133,7 +133,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/recycled', + url: '/api/items/recycled', query: { page: '2', pageSize: '5' }, }); @@ -177,7 +177,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/recycled', + url: '/api/items/recycled', }); const response = res.json(); @@ -210,7 +210,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Get, - url: '/items/recycled', + url: '/api/items/recycled', }); const response = res.json(); @@ -267,7 +267,7 @@ describe('Recycle Bin Tests', () => { const itemIds = items.map((i) => i.id); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/recycle', + url: '/api/items/recycle', query: { id: itemIds }, }); expect(res.statusCode).toBe(StatusCodes.ACCEPTED); @@ -312,7 +312,7 @@ describe('Recycle Bin Tests', () => { const itemIds = items.map((i) => i.id); const res = await app.inject({ method: HttpMethod.Post, - url: '/items/recycle', + url: '/api/items/recycle', query: { id: itemIds }, }); @@ -351,7 +351,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Post, - url: '/items/recycle', + url: '/api/items/recycle', query: { id: items.map(({ id }) => id) }, }); expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -364,7 +364,7 @@ describe('Recycle Bin Tests', () => { const res = await app.inject({ method: HttpMethod.Post, - url: '/items/recycle', + url: '/api/items/recycle', query: { id: ['invalid-id', v4()] }, }); diff --git a/src/services/item/plugins/shortcut/shortcut.controller.test.ts b/src/services/item/plugins/shortcut/shortcut.controller.test.ts index 58b6da6cd7..251955e1ff 100644 --- a/src/services/item/plugins/shortcut/shortcut.controller.test.ts +++ b/src/services/item/plugins/shortcut/shortcut.controller.test.ts @@ -43,7 +43,7 @@ describe('Shortcut routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/shortcuts', + url: '/api/items/shortcuts', payload: { target: v4() }, }); @@ -79,7 +79,7 @@ describe('Shortcut routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/shortcuts', + url: '/api/items/shortcuts', payload: { target: target.id, name: 'name' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -116,7 +116,7 @@ describe('Shortcut routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/items/shortcuts', + url: '/api/items/shortcuts', payload: { target: target.id }, }); expect(response.statusMessage).toEqual(ReasonPhrases.OK); @@ -132,7 +132,7 @@ describe('Shortcut routes tests', () => { const newItem = ShortcutItemFactory({ name: '' }); const response = await app.inject({ method: HttpMethod.Post, - url: '/items/shortcuts', + url: '/api/items/shortcuts', payload: newItem, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -142,7 +142,7 @@ describe('Shortcut routes tests', () => { const newItem1 = ShortcutItemFactory({ name: ' ' }); const response1 = await app.inject({ method: HttpMethod.Post, - url: '/items/shortcuts', + url: '/api/items/shortcuts', payload: newItem1, }); expect(response1.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -282,7 +282,7 @@ describe('Shortcut routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: '/items/shortcuts/invalid-id', + url: '/api/items/shortcuts/invalid-id', payload, }); diff --git a/src/services/member/member.controller.email.storage.test.ts b/src/services/member/member.controller.email.storage.test.ts index a663054dcb..35221c43ff 100644 --- a/src/services/member/member.controller.email.storage.test.ts +++ b/src/services/member/member.controller.email.storage.test.ts @@ -52,7 +52,7 @@ describe('Member Storage Controller', () => { assertIsMember(actor); const response = await app.inject({ method: 'POST', - url: '/members/current/email/change', + url: '/api/members/current/email/change', body: { email: faker.internet.email() }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -72,7 +72,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: 'POST', - url: '/members/current/email/change', + url: '/api/members/current/email/change', }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); expect(mockSendEmail).not.toHaveBeenCalled(); @@ -90,7 +90,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: 'POST', - url: '/members/current/email/change', + url: '/api/members/current/email/change', body: { email: 'abc' }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -114,7 +114,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: 'POST', - url: '/members/current/email/change', + url: '/api/members/current/email/change', body: { email: member.email }, }); @@ -136,7 +136,7 @@ describe('Member Storage Controller', () => { const email = faker.internet.email(); const response = await app.inject({ method: 'POST', - url: '/members/current/email/change', + url: '/api/members/current/email/change', body: { email }, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -163,7 +163,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); // Email didn't change @@ -185,7 +185,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', headers: { Authorization: `Bearer ${token}` }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -211,7 +211,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', headers: { Authorization: `Bearer ${token}` }, }); expect(response.statusCode).toBe(StatusCodes.CONFLICT); @@ -236,7 +236,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', headers: { Authorization: `Bearer ${token}` }, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -256,7 +256,7 @@ describe('Member Storage Controller', () => { // JWT is invalidated const response2 = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', headers: { Authorization: `Bearer ${token}` }, }); expect(response2.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -279,7 +279,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: '/members/current/email/change', + url: '/api/members/current/email/change', headers: { Authorization: `Bearer ${token}` }, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -299,7 +299,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', }); expect(response.statusCode).toBe(StatusCodes.OK); }); @@ -311,7 +311,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: FILE_METADATA_MIN_PAGE - 1 + '' }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -324,7 +324,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { pageSize: FILE_METADATA_MIN_PAGE_SIZE - 1 + '' }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -337,7 +337,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { pageSize: FILE_METADATA_MAX_PAGE_SIZE + 1 + '' }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -359,7 +359,7 @@ describe('Member Storage Controller', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', }); expect(response.statusCode).toBe(StatusCodes.OK); const resultDefault = await response.json(); @@ -404,7 +404,7 @@ describe('Member Storage Controller', () => { it('default parameters when not specified', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', }); expect(response.statusCode).toBe(StatusCodes.OK); const resultDefault = await response.json(); @@ -428,7 +428,7 @@ describe('Member Storage Controller', () => { let response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -440,7 +440,7 @@ describe('Member Storage Controller', () => { page = 2; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -455,7 +455,7 @@ describe('Member Storage Controller', () => { page = 3; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -471,7 +471,7 @@ describe('Member Storage Controller', () => { page = 4; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -501,7 +501,7 @@ describe('Member Storage Controller', () => { let response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -513,7 +513,7 @@ describe('Member Storage Controller', () => { page = 2; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -528,7 +528,7 @@ describe('Member Storage Controller', () => { page = 3; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -544,7 +544,7 @@ describe('Member Storage Controller', () => { page = 4; response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage/files', + url: '/api/members/current/storage/files', query: { page: page + '', pageSize: pageSize + '' }, }); expect(response.statusCode).toBe(StatusCodes.OK); diff --git a/src/services/member/member.controller.test.ts b/src/services/member/member.controller.test.ts index fbc0175ced..4eab650ca3 100644 --- a/src/services/member/member.controller.test.ts +++ b/src/services/member/member.controller.test.ts @@ -54,7 +54,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current', + url: '/api/members/current', }); const m = response.json(); @@ -81,7 +81,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current', + url: '/api/members/current', }); const m = await response.json(); expect(response.statusCode).toBe(StatusCodes.OK); @@ -100,7 +100,7 @@ describe('Member routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current', + url: '/api/members/current', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -173,7 +173,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage', + url: '/api/members/current/storage', }); const { current, maximum } = response.json(); expect(response.statusCode).toBe(StatusCodes.OK); @@ -204,7 +204,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage', + url: '/api/members/current/storage', }); const { current, maximum } = response.json(); expect(response.statusCode).toBe(StatusCodes.OK); @@ -214,7 +214,7 @@ describe('Member routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: '/members/current/storage', + url: '/api/members/current/storage', }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); diff --git a/src/services/member/plugins/action/memberAction.controller.test.ts b/src/services/member/plugins/action/memberAction.controller.test.ts index 6e7376413f..82a3e49849 100644 --- a/src/services/member/plugins/action/memberAction.controller.test.ts +++ b/src/services/member/plugins/action/memberAction.controller.test.ts @@ -14,7 +14,7 @@ export const getDateBeforeOrAfterNow = (dateDiff: number) => { return date.toISOString(); }; -const GET_URL = '/members/actions'; +const GET_URL = '/api/members/actions'; describe('Get member actions', () => { let app: FastifyInstance; diff --git a/src/services/member/test/register.test.ts b/src/services/member/test/register.test.ts index 7141824fee..da13cff01b 100644 --- a/src/services/member/test/register.test.ts +++ b/src/services/member/test/register.test.ts @@ -61,7 +61,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -182,7 +182,7 @@ describe('POST /register', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { ...member, captcha: MOCK_CAPTCHA }, }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); @@ -203,7 +203,7 @@ describe('POST /register', () => { const name = 'anna'; const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -219,7 +219,7 @@ describe('POST /register', () => { const name = '
%"^'; const response = await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -238,7 +238,7 @@ describe('POST /register', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email: invitation.email, name: 'some-name', captcha: MOCK_CAPTCHA }, }); const member = await getMemberByEmail(invitation.email); @@ -268,7 +268,7 @@ describe('POST /register', () => { // register await app.inject({ method: HttpMethod.Post, - url: '/register', + url: '/api/register', payload: { email, name: 'some-name', captcha: MOCK_CAPTCHA }, }); From 718ea9f30476cbc47bea08c8389e6409a36d997f Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 08:02:20 +0000 Subject: [PATCH 04/24] fix(test): first batch --- .../auth/plugins/passport/plugin.test.ts | 2 +- src/services/item/index.ts | 6 +- .../item/item.controller.read.test.ts | 86 +++++++++--------- .../item/item.controller.update.test.ts | 88 +++++++++---------- .../plugins/file/itemFile.controller.test.ts | 16 ++-- .../itemLogin/itemLogin.controller.test.ts | 2 +- src/utils/config.ts | 4 +- test/factories/member.factory.ts | 33 ++++++- 8 files changed, 131 insertions(+), 106 deletions(-) diff --git a/src/services/auth/plugins/passport/plugin.test.ts b/src/services/auth/plugins/passport/plugin.test.ts index 7096e10f50..93ff4e5b91 100644 --- a/src/services/auth/plugins/passport/plugin.test.ts +++ b/src/services/auth/plugins/passport/plugin.test.ts @@ -53,7 +53,7 @@ async function logIn(app: FastifyInstance, member: { id: string }) { const token = sign({ sub: member.id }, JWT_SECRET); const response = await app.inject({ method: HttpMethod.Get, - path: '/auth', + path: '/api/auth', query: { t: token }, }); return response.headers['set-cookie']; diff --git a/src/services/item/index.ts b/src/services/item/index.ts index 90460dec0c..b33d496738 100644 --- a/src/services/item/index.ts +++ b/src/services/item/index.ts @@ -3,7 +3,7 @@ import type { FastifyPluginAsync } from 'fastify'; import fp from 'fastify-plugin'; import { APPS_JWT_SECRET } from '../../config/secrets'; -import { APPS_PUBLISHER_ID, APP_ITEMS_PREFIX, ITEMS_ROUTE_PREFIX } from '../../utils/config'; +import { APPS_PUBLISHER_ID } from '../../utils/config'; import chatController from '../chat/chatMessage.controller'; import graaspItemLogin from '../itemLogin/itemLogin.controller'; import { itemMembershipsController } from '../itemMembership/membership.controller'; @@ -44,7 +44,7 @@ const plugin: FastifyPluginAsync = async (fastify) => { // because graaspApps extends the schemas await fastify.register(graaspApps, { jwtSecret: APPS_JWT_SECRET, - prefix: APP_ITEMS_PREFIX, + prefix: '/app-items', publisherId: APPS_PUBLISHER_ID, }); @@ -124,7 +124,7 @@ const plugin: FastifyPluginAsync = async (fastify) => { fastify.register(itemController); }); }, - { prefix: ITEMS_ROUTE_PREFIX }, + { prefix: '/items' }, ); }; diff --git a/src/services/item/item.controller.read.test.ts b/src/services/item/item.controller.read.test.ts index 9a3ba18636..89d0d6a0d7 100644 --- a/src/services/item/item.controller.read.test.ts +++ b/src/services/item/item.controller.read.test.ts @@ -79,7 +79,7 @@ describe('Item routes tests', () => { unmockAuthenticate(); }); - describe('GET /items/:id', () => { + describe('GET /api/items/:id', () => { it('Throws if signed out', async () => { const { items: [item], @@ -87,7 +87,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -108,7 +108,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); const returnedItem = response.json(); @@ -140,7 +140,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); const returnedItem = response.json(); @@ -173,7 +173,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); const returnedItem = response.json(); @@ -207,7 +207,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); expect(response.json()).toEqual(new MemberCannotAccess(item.id)); @@ -230,7 +230,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); const returnedItem = response.json(); @@ -258,7 +258,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, }); const returnedItem = response.json(); @@ -448,7 +448,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible?creatorId=${bob.id}`, + url: `/api/items/accessible?creatorId=${bob.id}`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -487,7 +487,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible?sortBy=${SortBy.ItemName}&ordering=asc`, + url: `/api/items/accessible?sortBy=${SortBy.ItemName}&ordering=asc`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -527,7 +527,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible?sortBy=${SortBy.ItemType}&ordering=desc`, + url: `/api/items/accessible?sortBy=${SortBy.ItemType}&ordering=desc`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -568,7 +568,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible?sortBy=${SortBy.ItemCreatorName}&ordering=asc`, + url: `/api/items/accessible?sortBy=${SortBy.ItemCreatorName}&ordering=asc`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -616,7 +616,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { keywords: ['dogs'] }, }); @@ -654,7 +654,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { sortBy: SortBy.ItemCreatorName, ordering: 'asc', @@ -693,7 +693,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { permissions: [PermissionLevel.Write, PermissionLevel.Admin], }, @@ -744,7 +744,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { types: [ItemType.FOLDER], }, @@ -777,7 +777,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { sortBy: 'nimp', ordering: Ordering.DESC, @@ -804,7 +804,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { sortBy: SortBy.ItemName, ordering: 'nimp', @@ -831,7 +831,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/accessible`, + url: '/api/items/accessible', query: { types: 'nimp', }, @@ -861,7 +861,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, // add sorting for result to be less flacky - url: `/items/accessible?ordering=asc&sortBy=${SortBy.ItemName}&pageSize=1&page=3`, + url: `/api/items/accessible?ordering=asc&sortBy=${SortBy.ItemName}&pageSize=1&page=3`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -881,7 +881,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/children`, + url: `/api/items/${item.id}/children`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -912,7 +912,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parentItem.id}/children`, + url: `/api/items/${parentItem.id}/children`, }); const data = response.json(); @@ -951,7 +951,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parentItem.id}/children`, + url: `/api/items/${parentItem.id}/children`, }); const data = response.json(); @@ -979,7 +979,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children`, + url: `/api/items/${parent.id}/children`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -1019,7 +1019,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children?types=folder`, + url: `/api/items/${parent.id}/children?types=folder`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -1070,7 +1070,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children`, + url: `/api/items/${parent.id}/children`, query: { keywords: ['dogs'] }, }); @@ -1097,7 +1097,7 @@ describe('Item routes tests', () => { const id = uuidv4(); const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${id}/children`, + url: `/api/items/${id}/children`, }); expect(response.json().message).toEqual(new ItemNotFound(id).message); @@ -1110,7 +1110,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children`, + url: `/api/items/${parent.id}/children`, }); expect(response.json()).toEqual(new MemberCannotAccess(parent.id)); @@ -1154,7 +1154,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children`, + url: `/api/items/${parent.id}/children`, }); const children = [child1, child2, child3].map((i) => @@ -1181,7 +1181,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/descendants`, + url: `/api/items/${item.id}/descendants`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -1210,7 +1210,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/descendants`, + url: `/api/items/${parent.id}/descendants`, }); const data = response.json(); @@ -1244,7 +1244,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/descendants`, + url: `/api/items/${parent.id}/descendants`, }); const data = response.json(); @@ -1274,7 +1274,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/descendants`, + url: `/api/items/${parent.id}/descendants`, }); const result = response.json(); @@ -1302,7 +1302,7 @@ describe('Item routes tests', () => { const id = uuidv4(); const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${id}/descendants`, + url: `/api/items/${id}/descendants`, }); expect(response.json().message).toEqual(new ItemNotFound(id).message); @@ -1315,7 +1315,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/descendants`, + url: `/api/items/${item.id}/descendants`, }); expect(response.json()).toEqual(new MemberCannotAccess(item.id)); @@ -1331,7 +1331,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/descendants`, + url: `/api/items/${parent.id}/descendants`, }); const data = response.json(); @@ -1353,7 +1353,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/parents`, + url: `/api/items/${item.id}/parents`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -1381,7 +1381,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${childOfChild.id}/parents`, + url: `/api/items/${childOfChild.id}/parents`, }); const data = response.json(); @@ -1403,7 +1403,7 @@ describe('Item routes tests', () => { const id = uuidv4(); const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${id}/parents`, + url: `/api/items/${id}/parents`, }); expect(response.json().message).toEqual(new ItemNotFound(id).message); @@ -1422,7 +1422,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/parents`, + url: `/api/items/${parent.id}/parents`, }); expect(response.json()).toEqual(new MemberCannotAccess(parent.id)); @@ -1450,7 +1450,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${child.id}/parents`, + url: `/api/items/${child.id}/parents`, }); expect(response.json()).toEqual(new MemberCannotAccess(child.id)); @@ -1478,7 +1478,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${childOfChild.id}/parents`, + url: `/api/items/${childOfChild.id}/parents`, }); const data = response.json(); @@ -1505,7 +1505,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${child.id}/parents`, + url: `/api/items/${child.id}/parents`, }); expect(response.json()).toEqual(new MemberCannotAccess(child.id)); diff --git a/src/services/item/item.controller.update.test.ts b/src/services/item/item.controller.update.test.ts index 8feb59047a..3066a47de6 100644 --- a/src/services/item/item.controller.update.test.ts +++ b/src/services/item/item.controller.update.test.ts @@ -105,7 +105,7 @@ describe('Item routes tests', () => { unmockAuthenticate(); }); - describe('POST /items', () => { + describe('POST /api/items', () => { it('Throws if signed out', async () => { const payload = FolderItemFactory(); const response = await app.inject({ @@ -122,7 +122,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: '/api/items', payload: { ...payload, geolocation: { lat: 1 } }, }); @@ -130,7 +130,7 @@ describe('Item routes tests', () => { const response1 = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: '/api/items', payload: { ...payload, geolocation: { lng: 1 } }, }); @@ -182,7 +182,7 @@ describe('Item routes tests', () => { const parentId = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parentId}`, + url: `/api/items?parentId=${parentId}`, payload, }); @@ -269,7 +269,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); const newItem = response.json(); @@ -309,7 +309,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); @@ -338,7 +338,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, payload: { ...payload, geolocation: { lat: 1, lng: 2 } }, }); @@ -363,7 +363,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory({ lang: 'fr' }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, payload, }); @@ -387,7 +387,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, payload: { name: faker.word.adverb(), type: ItemType.FOLDER }, query: { parentId: parentItem.id }, }); @@ -409,7 +409,7 @@ describe('Item routes tests', () => { }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, payload, }); @@ -438,7 +438,7 @@ describe('Item routes tests', () => { }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, payload, }); @@ -469,7 +469,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, query: { parentId: parentItem.id, previousItemId: previousItem.id }, payload, }); @@ -503,7 +503,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, query: { parentId: parentItem.id, previousItemId: lastItem.id }, payload, }); @@ -537,7 +537,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items`, + url: `/api/items`, query: { parentId: parentItem.id, previousItemId: anotherChild.id }, payload, }); @@ -563,7 +563,7 @@ describe('Item routes tests', () => { const parentId = uuidv4(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parentId}`, + url: `/api/items?parentId=${parentId}`, payload, }); @@ -583,7 +583,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); @@ -605,7 +605,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -631,7 +631,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); @@ -656,7 +656,7 @@ describe('Item routes tests', () => { const deepestItem = items.pop()!; const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${deepestItem.id}`, + url: `/api/items?parentId=${deepestItem.id}`, payload, }); @@ -683,7 +683,7 @@ describe('Item routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items?parentId=${parent.id}`, + url: `/api/items?parentId=${parent.id}`, payload, }); @@ -693,7 +693,7 @@ describe('Item routes tests', () => { }); }); - describe('POST /items/with-thumbnail', () => { + describe('POST /api/items/with-thumbnail', () => { it('Post item with thumbnail', async () => { const { actor } = await seedFromJson(); assertIsDefined(actor); @@ -708,7 +708,7 @@ describe('Item routes tests', () => { payload.append('file', imageStream); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/with-thumbnail`, + url: `/api/items/with-thumbnail`, payload, headers: payload.getHeaders(), }); @@ -730,14 +730,14 @@ describe('Item routes tests', () => { }); }); - describe('PATCH /items/:id', () => { + describe('PATCH /api/items/:id', () => { it('Throws if signed out', async () => { const { items: [item], } = await seedFromJson({ actor: null, items: [{}] }); const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload: { name: 'new name' }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -777,7 +777,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -815,7 +815,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -848,7 +848,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -884,7 +884,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -923,7 +923,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -961,7 +961,7 @@ describe('Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${uuidv4()}`, + url: `/api/items/${uuidv4()}`, payload, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -978,7 +978,7 @@ describe('Item routes tests', () => { const id = uuidv4(); const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${id}`, + url: `/api/items/${id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); @@ -1001,7 +1001,7 @@ describe('Item routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.json()).toEqual(new MemberCannotAccess(item.id)); @@ -1023,7 +1023,7 @@ describe('Item routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload: { name: 'newname' }, }); expect(response.json()).toEqual(new MemberCannotWriteItem(item.id)); @@ -1032,7 +1032,7 @@ describe('Item routes tests', () => { }); }); // delete many items - describe('DELETE /items', () => { + describe('DELETE /api/items', () => { it('Throws if signed out', async () => { const { items: [item], @@ -1302,14 +1302,14 @@ describe('Item routes tests', () => { }); }); - describe('PATCH /items/id/reorder', () => { + describe('PATCH /api/items/id/reorder', () => { it('Throws if signed out', async () => { const { items: [toReorder, previousItem], } = await seedFromJson({ actor: null, items: [{}, {}] }); const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: { previousItemId: previousItem.id, }, @@ -1336,7 +1336,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: {}, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -1365,7 +1365,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: { previousItemId: previousItem.id, }, @@ -1395,7 +1395,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: { previousItemId: previousItem.id, }, @@ -1425,7 +1425,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: { previousItemId: previousItem.id, }, @@ -1457,7 +1457,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${toReorder.id}/reorder`, + url: `/api/items/${toReorder.id}/reorder`, payload: { previousItemId: previousItem.id, }, @@ -1468,7 +1468,7 @@ describe('Item routes tests', () => { }); }); // move many items - describe('POST /items/move', () => { + describe('POST /api/items/move', () => { it('Throws if signed out', async () => { const { items: [parent, item], @@ -1509,7 +1509,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: '/api/api/items/move', + url: 'api/items/move', query: { id: items.map(({ id }) => id) }, payload: { parentId: parent.id, @@ -1792,7 +1792,7 @@ describe('Item routes tests', () => { }); }); // copy many items - describe('POST /items/copy', () => { + describe('POST /api/items/copy', () => { it('Throws if signed out', async () => { const { items: [item], diff --git a/src/services/item/plugins/file/itemFile.controller.test.ts b/src/services/item/plugins/file/itemFile.controller.test.ts index 5d66345d1f..1e6162a377 100644 --- a/src/services/item/plugins/file/itemFile.controller.test.ts +++ b/src/services/item/plugins/file/itemFile.controller.test.ts @@ -873,7 +873,7 @@ describe('File Item routes tests', () => { }); }); - describe('PATCH /items/files/:id', () => { + describe('PATCH /api/items/files/:id', () => { it('Throws if signed out', async () => { const { items: [item], @@ -884,7 +884,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload: { name: 'new name' }, }); @@ -914,7 +914,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); @@ -945,7 +945,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); @@ -979,7 +979,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); @@ -1019,7 +1019,7 @@ describe('File Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); @@ -1067,7 +1067,7 @@ describe('File Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); @@ -1094,7 +1094,7 @@ describe('File Item routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/files/${item.id}`, + url: `/api/items/files/${item.id}`, payload, }); diff --git a/src/services/itemLogin/itemLogin.controller.test.ts b/src/services/itemLogin/itemLogin.controller.test.ts index dd3392c7da..4de94992dd 100644 --- a/src/services/itemLogin/itemLogin.controller.test.ts +++ b/src/services/itemLogin/itemLogin.controller.test.ts @@ -64,7 +64,7 @@ describe('Item Login Tests', () => { unmockAuthenticate(); }); - describe('GET /:id/login-schema-type', () => { + describe('GET /api/items/:id/login-schema-type', () => { it('Get item login if signed out', async () => { const { items: [item], diff --git a/src/utils/config.ts b/src/utils/config.ts index 0099f6e9b4..8ff4080d86 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -196,8 +196,8 @@ export const EMBEDDED_LINK_ITEM_IFRAMELY_HREF_ORIGIN = requiredEnvVar( export const IMAGE_CLASSIFIER_API = process.env.IMAGE_CLASSIFIER_API ?? 'http://localhost:8080/infer'; -export const ITEMS_ROUTE_PREFIX = '/items'; -export const APP_ITEMS_PREFIX = '/app-items'; +export const ITEMS_ROUTE_PREFIX = '/api/items'; +export const APP_ITEMS_PREFIX = '/api/app-items'; export const THUMBNAILS_ROUTE_PREFIX = '/thumbnails'; export const MEMBER_PROFILE_ROUTE_PREFIX = '/profile'; diff --git a/test/factories/member.factory.ts b/test/factories/member.factory.ts index ac0fa317af..42c658e63e 100644 --- a/test/factories/member.factory.ts +++ b/test/factories/member.factory.ts @@ -35,15 +35,40 @@ export const MemberFactory = (m: Partial = {}): MemberRaw => { userAgreementsDate: m.userAgreementsDate ?? faker.date.past().toISOString(), ...baseAccount, ...m, - email: - m.email?.toLowerCase() ?? - baseAccount.email?.toLowerCase() ?? - faker.internet.email().toLowerCase(), + email: m.email?.toLowerCase() ?? baseAccount.email?.toLowerCase() ?? uniqueEmail(), type: AccountType.Individual, isValidated, }; }; +/** + * Creates a unique-ish email by appending timestamp + random bytes. + * Ensures RFC-safe local-part characters and keeps domain from faker. + * Example output: "john.doe+1731401674001-8f3a@example.com" + */ +function uniqueEmail() { + const base = faker.internet.email(); + const [local, domain] = base.split('@'); + + const ts = Date.now(); // ms since epoch + const rand = Math.random().toString(16).slice(2, 6); // short non-crypto random + const suffix = `${ts}-${rand}`; + + // Use plus addressing to keep domain valid and readable + const uniqueLocal = `${sanitizeLocal(local)}+${suffix}`; + + return `${uniqueLocal}@${domain}`; +} + +function sanitizeLocal(local) { + // Keep alphanumerics and . _ - only (safe common set) + return local + .toLowerCase() + .replace(/[^a-z0-9._-]/g, '.') + .replace(/\.+/g, '.') + .replace(/^\.+|\.+$/g, ''); +} + export const GuestFactory = (g: Partial & Pick) => ({ ...BaseAccountFactory({ type: AccountType.Guest }), ...g, From 5b837dee6503b6921e405457ae2bb7794db3a282 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 08:15:47 +0000 Subject: [PATCH 05/24] fix(test): move more to vitest --- setup.node.ts | 3 +++ src/plugins/mailer/{builder.test.ts => builder.spec.ts} | 1 + src/repositories/const.ts | 2 -- src/schemas/{regex.test.ts => regex.spec.ts} | 2 ++ .../{account.repository.test.ts => account.repository.spec.ts} | 1 + .../{action.repository.test.ts => action.repository.spec.ts} | 1 + src/services/action/test/fixtures/actions.ts | 1 + src/services/action/utils/{actions.test.ts => actions.spec.ts} | 2 ++ 8 files changed, 11 insertions(+), 2 deletions(-) rename src/plugins/mailer/{builder.test.ts => builder.spec.ts} (98%) delete mode 100644 src/repositories/const.ts rename src/schemas/{regex.test.ts => regex.spec.ts} (97%) rename src/services/account/{account.repository.test.ts => account.repository.spec.ts} (96%) rename src/services/action/{action.repository.test.ts => action.repository.spec.ts} (99%) rename src/services/action/utils/{actions.test.ts => actions.spec.ts} (87%) diff --git a/setup.node.ts b/setup.node.ts index d2c9bc6e64..3e197a0eb2 100644 --- a/setup.node.ts +++ b/setup.node.ts @@ -1 +1,4 @@ +import { config } from 'dotenv'; import 'reflect-metadata'; + +config({ path: '.env.test', override: true }); diff --git a/src/plugins/mailer/builder.test.ts b/src/plugins/mailer/builder.spec.ts similarity index 98% rename from src/plugins/mailer/builder.test.ts rename to src/plugins/mailer/builder.spec.ts index 697546eb6a..b4560e099b 100644 --- a/src/plugins/mailer/builder.test.ts +++ b/src/plugins/mailer/builder.spec.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import { describe, expect, it } from 'vitest'; import { TRANSLATIONS } from '../../langs/constants'; import enTranslations from '../../langs/en.json'; diff --git a/src/repositories/const.ts b/src/repositories/const.ts deleted file mode 100644 index a45356192e..0000000000 --- a/src/repositories/const.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** The default primary key used by the Graasp Entities */ -export const DEFAULT_PRIMARY_KEY = 'id'; diff --git a/src/schemas/regex.test.ts b/src/schemas/regex.spec.ts similarity index 97% rename from src/schemas/regex.test.ts rename to src/schemas/regex.spec.ts index cbf525e1cd..b0b3d0cd29 100644 --- a/src/schemas/regex.test.ts +++ b/src/schemas/regex.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { EMPTY_OR_SPACED_WORDS_REGEX, ITEM_NAME_REGEX } from './regex'; describe('Globals', () => { diff --git a/src/services/account/account.repository.test.ts b/src/services/account/account.repository.spec.ts similarity index 96% rename from src/services/account/account.repository.test.ts rename to src/services/account/account.repository.spec.ts index d400ae65f6..7e200b33a2 100644 --- a/src/services/account/account.repository.test.ts +++ b/src/services/account/account.repository.spec.ts @@ -1,4 +1,5 @@ import { v4 as uuidV4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import { seedFromJson } from '../../../test/mocks/seed'; import { db } from '../../drizzle/db'; diff --git a/src/services/action/action.repository.test.ts b/src/services/action/action.repository.spec.ts similarity index 99% rename from src/services/action/action.repository.test.ts rename to src/services/action/action.repository.spec.ts index bd9722bb5c..077bf64094 100644 --- a/src/services/action/action.repository.test.ts +++ b/src/services/action/action.repository.spec.ts @@ -1,4 +1,5 @@ import { and, eq } from 'drizzle-orm'; +import { describe, expect, it } from 'vitest'; import { Context } from '@graasp/sdk'; diff --git a/src/services/action/test/fixtures/actions.ts b/src/services/action/test/fixtures/actions.ts index e8d415fc10..a4ee72e0de 100644 --- a/src/services/action/test/fixtures/actions.ts +++ b/src/services/action/test/fixtures/actions.ts @@ -1,4 +1,5 @@ import { eq } from 'drizzle-orm'; +import { expect } from 'vitest'; import { type DBConnection } from '../../../../drizzle/db'; import { actionsTable } from '../../../../drizzle/schema'; diff --git a/src/services/action/utils/actions.test.ts b/src/services/action/utils/actions.spec.ts similarity index 87% rename from src/services/action/utils/actions.test.ts rename to src/services/action/utils/actions.spec.ts index 72040f60a3..7923a8d113 100644 --- a/src/services/action/utils/actions.test.ts +++ b/src/services/action/utils/actions.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { getGeolocationIp } from './actions'; describe('Action Utils', () => { From ab75cf3387f0e119cfabdd6dab2cf5e8282eec1f Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 09:00:48 +0000 Subject: [PATCH 06/24] fix(test): itemGeolocation --- src/services/item/errors.ts | 21 ++--- .../plugins/folder/folder.controller.test.ts | 58 ++++++------- ...service.test.ts => folder.service.spec.ts} | 77 ++++++++++------- .../itemGeolocation.controller.test.ts | 36 +++++++- ....ts => itemGeolocation.repository.spec.ts} | 6 +- ...est.ts => itemGeolocation.service.spec.ts} | 86 +++++++++---------- .../item/plugins/geolocation/test/utils.ts | 2 + ...l.service.test.ts => html.service.spec.ts} | 2 + test/app.vitest.ts | 9 ++ 9 files changed, 171 insertions(+), 126 deletions(-) rename src/services/item/plugins/folder/{folder.service.test.ts => folder.service.spec.ts} (70%) rename src/services/item/plugins/geolocation/{itemGeolocation.repository.test.ts => itemGeolocation.repository.spec.ts} (99%) rename src/services/item/plugins/geolocation/{itemGeolocation.service.test.ts => itemGeolocation.service.spec.ts} (82%) rename src/services/item/plugins/html/{html.service.test.ts => html.service.spec.ts} (96%) create mode 100644 test/app.vitest.ts diff --git a/src/services/item/errors.ts b/src/services/item/errors.ts index f5095c1eb7..9cc96bcef2 100644 --- a/src/services/item/errors.ts +++ b/src/services/item/errors.ts @@ -1,6 +1,8 @@ import { StatusCodes } from 'http-status-codes'; -import { ErrorFactory, type ItemTypeUnion } from '@graasp/sdk'; +import { createError } from '@fastify/error'; + +import { ErrorFactory } from '@graasp/sdk'; const PLUGIN_NAME = 'graasp-plugin-item'; @@ -10,18 +12,11 @@ const PLUGIN_NAME = 'graasp-plugin-item'; export const GraaspItemError = ErrorFactory(PLUGIN_NAME); -export class WrongItemTypeError extends GraaspItemError { - constructor(data?: ItemTypeUnion) { - super( - { - code: 'GIERR001', - statusCode: StatusCodes.BAD_REQUEST, - message: 'Item does not have the correct type', - }, - data, - ); - } -} +export const WrongItemTypeError = createError( + 'GIERR001', + 'Item does not have the correct type', + StatusCodes.BAD_REQUEST, +); export class ItemOrderingError extends GraaspItemError { constructor(reason?: string) { diff --git a/src/services/item/plugins/folder/folder.controller.test.ts b/src/services/item/plugins/folder/folder.controller.test.ts index 02e8a24244..afd46227c0 100644 --- a/src/services/item/plugins/folder/folder.controller.test.ts +++ b/src/services/item/plugins/folder/folder.controller.test.ts @@ -94,7 +94,7 @@ describe('Folder routes tests', () => { unmockAuthenticate(); }); - describe('POST /items/folders', () => { + describe('POST /api/items/folders', () => { it('Throws if signed out', async () => { const payload = FolderItemFactory(); const response = await app.inject({ @@ -177,7 +177,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); const newItem = response.json(); @@ -213,7 +213,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); @@ -246,7 +246,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload: { ...payload, geolocation: { lat: 1, lng: 2 } }, }); @@ -271,7 +271,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory({ lang: 'fr' }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload, }); @@ -295,7 +295,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload: { name: faker.word.adverb(), type: ItemType.FOLDER }, query: { parentId: parentItem.id }, }); @@ -317,7 +317,7 @@ describe('Folder routes tests', () => { }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload, }); @@ -346,7 +346,7 @@ describe('Folder routes tests', () => { }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload, }); @@ -372,7 +372,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', query: { parentId: parentItem.id, previousItemId: previousItem.id }, payload, }); @@ -404,7 +404,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', query: { parentId: parentItem.id, previousItemId: previousItem.id }, payload, }); @@ -439,7 +439,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', query: { parentId: parentItem.id, previousItemId: anotherChild.id }, payload, }); @@ -464,7 +464,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload: { ...payload, geolocation: { lat: 1 } }, }); @@ -478,7 +478,7 @@ describe('Folder routes tests', () => { const response1 = await app.inject({ method: HttpMethod.Post, - url: `/items/folders`, + url: '/api/items/folders', payload: { ...payload, geolocation: { lng: 1 } }, }); @@ -518,7 +518,7 @@ describe('Folder routes tests', () => { const parentId = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parentId}`, + url: `/api/items/folders?parentId=${parentId}`, payload, }); @@ -534,7 +534,7 @@ describe('Folder routes tests', () => { const parentId = uuidv4(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parentId}`, + url: `/api/items/folders?parentId=${parentId}`, payload, }); @@ -555,7 +555,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); @@ -576,7 +576,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -603,7 +603,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); @@ -629,7 +629,7 @@ describe('Folder routes tests', () => { const payload = FolderItemFactory(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders?parentId=${parent.id}`, + url: `/api/items/folders?parentId=${parent.id}`, payload, }); @@ -639,7 +639,7 @@ describe('Folder routes tests', () => { }); }); - describe('POST /items/folders-with-thumbnail', () => { + describe('POST /api/items/folders-with-thumbnail', () => { it('Post item with thumbnail', async () => { const { actor } = await seedFromJson({ actor: { extra: { lang: 'en' } } }); assertIsDefined(actor); @@ -656,7 +656,7 @@ describe('Folder routes tests', () => { payload.append('file', imageStream); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/folders-with-thumbnail`, + url: `/api/items/folders-with-thumbnail`, payload, headers: payload.getHeaders(), }); @@ -678,7 +678,7 @@ describe('Folder routes tests', () => { }); }); - describe('PATCH /items/folders/:id', () => { + describe('PATCH /api/items/folders/:id', () => { it('Throws if signed out', async () => { const { items: [item], @@ -689,7 +689,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload: { name: 'new name' }, }); @@ -726,7 +726,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); @@ -762,7 +762,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); @@ -798,7 +798,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); @@ -841,7 +841,7 @@ describe('Folder routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); @@ -888,7 +888,7 @@ describe('Folder routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); @@ -910,7 +910,7 @@ describe('Folder routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/folders/${item.id}`, + url: `/api/items/folders/${item.id}`, payload, }); diff --git a/src/services/item/plugins/folder/folder.service.test.ts b/src/services/item/plugins/folder/folder.service.spec.ts similarity index 70% rename from src/services/item/plugins/folder/folder.service.test.ts rename to src/services/item/plugins/folder/folder.service.spec.ts index eab0eb4898..69b06527d4 100644 --- a/src/services/item/plugins/folder/folder.service.test.ts +++ b/src/services/item/plugins/folder/folder.service.spec.ts @@ -1,16 +1,19 @@ import { v4 } from 'uuid'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppItemFactory } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { ItemFactory } from '../../../../../test/factories/item.factory'; import { db } from '../../../../drizzle/db'; import type { ItemWithCreator } from '../../../../drizzle/types'; import type { MinimalMember } from '../../../../types'; +import { ItemNotFound } from '../../../../utils/errors'; import { AuthorizedItemService } from '../../../authorizedItem.service'; import { ItemMembershipRepository } from '../../../itemMembership/membership.repository'; import { ThumbnailService } from '../../../thumbnail/thumbnail.service'; import { ItemWrapperService } from '../../ItemWrapper'; +import { WrongItemTypeError } from '../../errors'; import { ItemRepository } from '../../item.repository'; import { ItemGeolocationRepository } from '../geolocation/itemGeolocation.repository'; import { ItemVisibilityRepository } from '../itemVisibility/itemVisibility.repository'; @@ -20,15 +23,23 @@ import { RecycledBinService } from '../recycled/recycled.service'; import { ItemThumbnailService } from '../thumbnail/itemThumbnail.service'; import { FolderItemService } from './folder.service'; +const itemRepository = { + getOneOrThrow: vi.fn(async () => { + return MOCK_ITEM; + }), +}; +const authorizedItemService = { + getItemById: vi.fn(), +}; const folderService = new FolderItemService( {} as ThumbnailService, {} as ItemThumbnailService, {} as ItemMembershipRepository, {} as MeiliSearchWrapper, - {} as ItemRepository, + itemRepository as unknown as ItemRepository, {} as ItemPublishedRepository, {} as ItemGeolocationRepository, - {} as AuthorizedItemService, + authorizedItemService as unknown as AuthorizedItemService, {} as ItemWrapperService, {} as ItemVisibilityRepository, {} as RecycledBinService, @@ -37,16 +48,14 @@ const folderService = new FolderItemService( const MOCK_ITEM = ItemFactory(); const MOCK_MEMBER = {} as MinimalMember; -const itemRepository = { - getOneOrThrow: async () => { - return MOCK_ITEM; - }, -} as unknown as ItemRepository; describe('Folder Service', () => { - afterEach(() => { - jest.clearAllMocks(); + beforeEach(() => { + vi.resetAllMocks(); }); + // afterEach(() => { + // // vi.clearAllMocks(); + // }); describe('get', () => { // it('return folder', async () => { @@ -92,32 +101,39 @@ describe('Folder Service', () => { it('throw if item is not a folder', async () => { const appItem = AppItemFactory() as ItemWithCreator; - jest.spyOn(AuthorizedItemService.prototype, 'getItemById').mockImplementation(async () => { + authorizedItemService.getItemById.mockImplementation(async () => { return appItem; }); - await expect(() => folderService.getFolder(db, MOCK_MEMBER, appItem.id)).rejects.toThrow(); + await expect(() => folderService.getFolder(db, MOCK_MEMBER, appItem.id)).rejects.toThrow( + new WrongItemTypeError('app'), + ); }); }); - describe('post', () => { - // it('set correct type and extra', async () => { - // const itemServicePostMock = jest - // .spyOn(ItemService.prototype, 'post') - // .mockImplementation(async () => { - // return {} as Item; - // }); - // await folderService.post(db, MOCK_MEMBER, { item: { name: 'name', type: 'folder' } }); - // expect(itemServicePostMock).toHaveBeenCalledWith(db, MOCK_MEMBER, { - // item: { name: 'name', extra: { [ItemType.FOLDER]: {} }, type: ItemType.FOLDER }, - // }); - // }); - }); + // describe('post', () => { + // it('set correct type and extra', async () => { + // const itemServicePostMock = jest + // .spyOn(ItemService.prototype, 'post') + // .mockImplementation(async () => { + // return {} as Item; + // }); + // await folderService.post(db, MOCK_MEMBER, { item: { name: 'name', type: 'folder' } }); + // expect(itemServicePostMock).toHaveBeenCalledWith(db, MOCK_MEMBER, { + // item: { name: 'name', extra: { [ItemType.FOLDER]: {} }, type: ItemType.FOLDER }, + // }); + // }); + // }); + describe('patch', () => { it('throw if item is not a folder', async () => { + const appItem = AppItemFactory() as ItemWithCreator; + itemRepository.getOneOrThrow.mockImplementation(async () => { + return appItem; + }); await expect(() => folderService.patch(db, MOCK_MEMBER, MOCK_ITEM.id, { name: 'name' }), - ).rejects.toThrow(); + ).rejects.toThrow(new WrongItemTypeError('app')); }); // it('use item service patch', async () => { // const itemServicePatchMock = jest @@ -134,13 +150,14 @@ describe('Folder Service', () => { // }); it('Cannot update not found item given id', async () => { - jest.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { - throw new Error(); + const id = v4(); + itemRepository.getOneOrThrow.mockImplementation(() => { + throw new ItemNotFound(id); }); await expect(() => - folderService.patch(db, MOCK_MEMBER, v4(), { name: 'name' }), - ).rejects.toThrow(); + folderService.patch(db, MOCK_MEMBER, id, { name: 'name' }), + ).rejects.toThrow(new ItemNotFound(id)); }); }); }); diff --git a/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts b/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts index daa160a595..deb00b2c93 100644 --- a/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts +++ b/src/services/item/plugins/geolocation/itemGeolocation.controller.test.ts @@ -19,7 +19,7 @@ import { MemberCannotAccess } from '../../../../utils/errors'; import { assertIsMemberForTest } from '../../../authentication'; import { ItemWrapper } from '../../ItemWrapper'; import { expectPackedItem, expectThumbnails } from '../../test/fixtures/items'; -import { expectPackedItemGeolocations } from './test/utils'; +import { PackedItemGeolocation } from './itemGeolocation.service'; // Mock S3 libraries const deleteObjectMock = jest.fn(async () => console.debug('deleteObjectMock')); @@ -56,6 +56,35 @@ jest.mock('@aws-sdk/lib-storage', () => { }; }); +const expectPackedItemGeolocations = ( + results: PackedItemGeolocation[] | null, + expected: PackedItemGeolocation[], +) => { + expect(results).toHaveLength(expected.length); + + for (const ig of expected) { + const publicTest = ig.item.public?.id + ? { public: expect.objectContaining({ id: ig.item.public.id }) } + : {}; + + expect(results).toContainEqual( + expect.objectContaining({ + lat: ig.lat, + lng: ig.lng, + addressLabel: ig.addressLabel, + helperLabel: ig.helperLabel, + country: ig.country, + item: expect.objectContaining({ + id: ig.item.id, + creator: ig.item.creator ? expect.objectContaining({ id: ig.item.creator.id }) : null, + permission: ig.item.permission, + ...publicTest, + }), + }), + ); + } +}; + describe('Item Geolocation', () => { let app: FastifyInstance; @@ -692,10 +721,9 @@ describe('Item Geolocation', () => { const { actor } = await seedFromJson(); assertIsDefined(actor); mockAuthenticate(actor); - if (GEOLOCATION_API_HOST) { nock(GEOLOCATION_API_HOST) - .get('/api/reverse') + .get('/reverse') .query(true) .reply(200, { results: [{ formatted: 'address', country: 'country' }], @@ -762,7 +790,7 @@ describe('Item Geolocation', () => { const id2 = uuid(); if (GEOLOCATION_API_HOST) { nock(GEOLOCATION_API_HOST) - .get('/api/search') + .get('/search') .query(true) .reply(200, { results: [ diff --git a/src/services/item/plugins/geolocation/itemGeolocation.repository.test.ts b/src/services/item/plugins/geolocation/itemGeolocation.repository.spec.ts similarity index 99% rename from src/services/item/plugins/geolocation/itemGeolocation.repository.test.ts rename to src/services/item/plugins/geolocation/itemGeolocation.repository.spec.ts index 7700a55963..8e31c26c76 100644 --- a/src/services/item/plugins/geolocation/itemGeolocation.repository.test.ts +++ b/src/services/item/plugins/geolocation/itemGeolocation.repository.spec.ts @@ -1,9 +1,9 @@ import { faker } from '@faker-js/faker'; import { eq } from 'drizzle-orm/sql'; +import { describe, expect, it } from 'vitest'; import { ItemType } from '@graasp/sdk'; -import { clearDatabase } from '../../../../../test/app'; import { buildFile, seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; import { itemGeolocationsTable } from '../../../../drizzle/schema'; @@ -25,10 +25,6 @@ const getGeolocationByItemPath = async (itemPath: ItemRaw['path']) => { }; describe('ItemGeolocationRepository', () => { - afterAll(async () => { - await clearDatabase(db); - }); - describe('copy', () => { it('copy geolocation on item copy', async () => { const { diff --git a/src/services/item/plugins/geolocation/itemGeolocation.service.test.ts b/src/services/item/plugins/geolocation/itemGeolocation.service.spec.ts similarity index 82% rename from src/services/item/plugins/geolocation/itemGeolocation.service.test.ts rename to src/services/item/plugins/geolocation/itemGeolocation.service.spec.ts index b879dc20d3..56af4d1c09 100644 --- a/src/services/item/plugins/geolocation/itemGeolocation.service.test.ts +++ b/src/services/item/plugins/geolocation/itemGeolocation.service.spec.ts @@ -1,8 +1,8 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { PermissionLevel } from '@graasp/sdk'; -import { clearDatabase } from '../../../../../test/app'; import { seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; import { assertIsDefined } from '../../../../utils/assertions'; @@ -15,18 +15,18 @@ import { ItemGeolocationService } from './itemGeolocation.service'; import { expectPackedItemGeolocations } from './test/utils'; const itemThumbnailService = { - getUrlsByItems: jest.fn(() => ({ small: 'url' })), + getUrlsByItems: vi.fn(() => ({ small: 'url' })), } as unknown as ItemThumbnailService; const authorizedItemService = { - getItemById: jest.fn(), - getPropertiesForItems: jest.fn(), + getItemById: vi.fn(), + getPropertiesForItems: vi.fn(), } as unknown as AuthorizedItemService; const itemGeolocationRepository = { - getUrlsByItems: jest.fn(() => ({ small: 'url' })), - delete: jest.fn(), - put: jest.fn(), - getByItem: jest.fn(), - getItemsIn: jest.fn(), + getUrlsByItems: vi.fn(() => ({ small: 'url' })), + delete: vi.fn(), + put: vi.fn(), + getByItem: vi.fn(), + getItemsIn: vi.fn(), } as unknown as ItemGeolocationRepository; const service = new ItemGeolocationService( @@ -37,12 +37,8 @@ const service = new ItemGeolocationService( ); describe('ItemGeolocationService', () => { - afterAll(async () => { - await clearDatabase(db); - }); - afterEach(async () => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('delete', () => { @@ -61,8 +57,8 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); - const deleteMock = jest.spyOn(itemGeolocationRepository, 'delete'); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); + const deleteMock = vi.spyOn(itemGeolocationRepository, 'delete'); await service.delete(db, actor, item.id); @@ -82,10 +78,10 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockImplementationOnce(() => { + vi.spyOn(authorizedItemService, 'getItemById').mockImplementationOnce(() => { throw new MemberCannotWriteItem(expect.anything()); }); - const deleteMock = jest.spyOn(itemGeolocationRepository, 'delete'); + const deleteMock = vi.spyOn(itemGeolocationRepository, 'delete'); await service .delete(db, actor, item.id) @@ -116,8 +112,8 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); - const getByItemMock = jest + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); + const getByItemMock = vi .spyOn(itemGeolocationRepository, 'getByItem') .mockResolvedValue({ ...geolocation, item }); @@ -137,8 +133,8 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); - jest.spyOn(itemGeolocationRepository, 'getByItem').mockResolvedValue(undefined); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); + vi.spyOn(itemGeolocationRepository, 'getByItem').mockResolvedValue(undefined); const res = await service.getByItem(db, actor, item.id); @@ -155,9 +151,9 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest - .spyOn(authorizedItemService, 'getItemById') - .mockRejectedValueOnce(new ItemNotFound(item.id)); + vi.spyOn(authorizedItemService, 'getItemById').mockRejectedValueOnce( + new ItemNotFound(item.id), + ); await service .getByItem(db, actor, v4()) @@ -195,11 +191,11 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ + vi.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ { ...geolocations[0], item: { ...items[0], creator: actor } }, { ...geolocations[1], item: { ...items[1], creator: actor } }, ]); - jest.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ + vi.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ itemMemberships: { data: { [items[0].id]: itemMemberships[0], @@ -256,11 +252,11 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ + vi.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ { ...geolocations[0], item: { ...items[0], creator: actor } }, { ...geolocations[1], item: { ...items[1], creator: actor } }, ]); - jest.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ + vi.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ itemMemberships: { data: { [items[1].id]: itemMemberships[0], @@ -319,10 +315,10 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest - .spyOn(itemGeolocationRepository, 'getItemsIn') - .mockResolvedValue([{ ...geolocations[0], item: { ...parent, creator: actor } }]); - jest.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ + vi.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ + { ...geolocations[0], item: { ...parent, creator: actor } }, + ]); + vi.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ itemMemberships: { data: { [parent.id]: null, @@ -378,10 +374,10 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest - .spyOn(itemGeolocationRepository, 'getItemsIn') - .mockResolvedValue([{ ...geolocations[0], item: { ...child, creator: actor } }]); - jest.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ + vi.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([ + { ...geolocations[0], item: { ...child, creator: actor } }, + ]); + vi.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ itemMemberships: { data: { [parent.id]: null, @@ -432,8 +428,8 @@ describe('ItemGeolocationService', () => { }); assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([]); - jest.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ + vi.spyOn(itemGeolocationRepository, 'getItemsIn').mockResolvedValue([]); + vi.spyOn(authorizedItemService, 'getPropertiesForItems').mockResolvedValue({ itemMemberships: { data: { [item.id]: itemMemberships[0], @@ -470,8 +466,8 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); - const putMock = jest.spyOn(itemGeolocationRepository, 'put'); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); + const putMock = vi.spyOn(itemGeolocationRepository, 'put'); await service.put(db, actor, item.id, { lat: 1, lng: 2 }); @@ -491,8 +487,8 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); - const putMock = jest.spyOn(itemGeolocationRepository, 'put'); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValueOnce(item); + const putMock = vi.spyOn(itemGeolocationRepository, 'put'); await service.put(db, actor, item.id, { lat: 1, lng: 2 }); @@ -512,10 +508,10 @@ describe('ItemGeolocationService', () => { assertIsDefined(actor); assertIsMemberForTest(actor); - jest.spyOn(authorizedItemService, 'getItemById').mockImplementationOnce(() => { + vi.spyOn(authorizedItemService, 'getItemById').mockImplementationOnce(() => { throw new MemberCannotWriteItem(expect.anything()); }); - const putMock = jest.spyOn(itemGeolocationRepository, 'put'); + const putMock = vi.spyOn(itemGeolocationRepository, 'put'); await service .put(db, actor, item.id, { lat: 1, lng: 2 }) diff --git a/src/services/item/plugins/geolocation/test/utils.ts b/src/services/item/plugins/geolocation/test/utils.ts index 01115592bb..91e549a20e 100644 --- a/src/services/item/plugins/geolocation/test/utils.ts +++ b/src/services/item/plugins/geolocation/test/utils.ts @@ -1,3 +1,5 @@ +import { expect } from 'vitest'; + import type { ItemGeolocationRaw, ItemGeolocationWithItemWithCreator, diff --git a/src/services/item/plugins/html/html.service.test.ts b/src/services/item/plugins/html/html.service.spec.ts similarity index 96% rename from src/services/item/plugins/html/html.service.test.ts rename to src/services/item/plugins/html/html.service.spec.ts index 32a9efee63..d2f62e59a0 100644 --- a/src/services/item/plugins/html/html.service.test.ts +++ b/src/services/item/plugins/html/html.service.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { BaseLogger } from '../../../../logger'; import { FileStorage } from '../../../file/types'; import { StorageService } from '../../../member/plugins/storage/memberStorage.service'; diff --git a/test/app.vitest.ts b/test/app.vitest.ts new file mode 100644 index 0000000000..4ba3205e67 --- /dev/null +++ b/test/app.vitest.ts @@ -0,0 +1,9 @@ +import { vi } from 'vitest'; + +import { BaseLogger } from '../src/logger'; + +export const MOCK_LOGGER = { + error: vi.fn(), + debug: vi.fn(), + info: vi.fn(), +} as unknown as BaseLogger; From 95822afaf990df3a8832f77b2008b2ae6d4d5d1f Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 09:22:59 +0000 Subject: [PATCH 07/24] fix: add more tests and convert to vitest --- .../importExport.controller.test.ts | 29 ++++----- .../{utils.test.ts => utils.spec.ts} | 2 + .../invitation/invitation.controller.test.ts | 3 +- ...est.ts => itemBookmark.repository.spec.ts} | 1 + .../item/plugins/itemFlag/itemFlag.errors.ts | 3 + .../item/plugins/itemFlag/utils/constants.ts | 1 - .../item/plugins/itemFlag/utils/errors.ts | 10 --- .../itemLike/itemLike.controller.test.ts | 44 +++++++------ .../item/plugins/itemLike/itemLike.errors.ts | 19 ++++++ ...ry.test.ts => itemLike.repository.spec.ts} | 1 + .../plugins/itemLike/itemLike.repository.ts | 2 +- ...rvice.test.ts => itemLike.service.spec.ts} | 61 ++++++++++--------- .../item/plugins/itemLike/utils/errors.ts | 31 ---------- src/workers/itemExportRequest.worker.spec.ts | 3 + src/workers/searchIndex.worker.spec.ts | 2 + test/factories/member.factory.ts | 2 +- 16 files changed, 105 insertions(+), 109 deletions(-) rename src/services/item/plugins/importExport/{utils.test.ts => utils.spec.ts} (99%) rename src/services/item/plugins/itemBookmark/{itemBookmark.repository.test.ts => itemBookmark.repository.spec.ts} (98%) create mode 100644 src/services/item/plugins/itemFlag/itemFlag.errors.ts delete mode 100644 src/services/item/plugins/itemFlag/utils/constants.ts delete mode 100644 src/services/item/plugins/itemFlag/utils/errors.ts create mode 100644 src/services/item/plugins/itemLike/itemLike.errors.ts rename src/services/item/plugins/itemLike/{itemLike.repository.test.ts => itemLike.repository.spec.ts} (98%) rename src/services/item/plugins/itemLike/{itemLike.service.test.ts => itemLike.service.spec.ts} (54%) delete mode 100644 src/services/item/plugins/itemLike/utils/errors.ts diff --git a/src/services/item/plugins/importExport/importExport.controller.test.ts b/src/services/item/plugins/importExport/importExport.controller.test.ts index ce4e526d1d..e557ef34a5 100644 --- a/src/services/item/plugins/importExport/importExport.controller.test.ts +++ b/src/services/item/plugins/importExport/importExport.controller.test.ts @@ -194,6 +194,7 @@ describe('ZIP routes tests', () => { afterAll(async () => { await clearDatabase(db); app.close(); + await importExportQueue.close(); }); beforeEach(() => { @@ -208,7 +209,7 @@ describe('ZIP routes tests', () => { jest.clearAllMocks(); }); - describe('POST /zip-import', () => { + describe('POST /api/zip-import', () => { it('Import successfully at root if signed in', async () => { const { actor } = await seedFromJson(); assertIsDefined(actor); @@ -238,7 +239,7 @@ describe('ZIP routes tests', () => { const parent = rootItems.find(({ name }) => name === ARCHIVE_CONTENT.folder.name); const itemsResponse = await app.inject({ method: HttpMethod.Get, - url: `/items/${parent.id}/children`, + url: `/api/items/${parent.id}/children`, }); const children = itemsResponse.json(); expect(children).toHaveLength(1); @@ -536,7 +537,7 @@ describe('ZIP routes tests', () => { }); }); - describe('POST /download-file', () => { + describe('POST /api/download-file', () => { it('Export successfully if has access', async () => { const { actor, @@ -556,7 +557,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/download-file`, + url: `/api/items/${item.id}/download-file`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -581,7 +582,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/download-file`, + url: `/api/items/${item.id}/download-file`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -604,7 +605,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/download-file`, + url: `/api/items/${item.id}/download-file`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -642,7 +643,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${h5pId}/download-file`, + url: `/api/items/${h5pId}/download-file`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -663,7 +664,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/download-file`, + url: `/api/items/${item.id}/download-file`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); }); @@ -682,7 +683,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/export`, + url: `/api/items/${item.id}/export`, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -712,7 +713,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/export`, + url: `/api/items/${item.id}/export`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); }); @@ -731,14 +732,14 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${documentId}/export`, + url: `/api/items/${documentId}/export`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); }); }); - describe('POST /graasp-export', () => { + describe('POST /api/graasp-export', () => { it('Exports successfully if signed in', async () => { const { actor, items } = await seedFromJson({ actor: {}, @@ -754,7 +755,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${items[0].id}/graasp-export`, + url: `/api/items/${items[0].id}/graasp-export`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.headers['content-disposition']).toContain(items[0].name); @@ -768,7 +769,7 @@ describe('ZIP routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${folderItem!.id}/graasp-export`, + url: `/api/items/${folderItem!.id}/graasp-export`, }); const { targetFolder, manifestItems } = await readResponseStream(response.stream()); diff --git a/src/services/item/plugins/importExport/utils.test.ts b/src/services/item/plugins/importExport/utils.spec.ts similarity index 99% rename from src/services/item/plugins/importExport/utils.test.ts rename to src/services/item/plugins/importExport/utils.spec.ts index 8a40410e3d..9eef89a222 100644 --- a/src/services/item/plugins/importExport/utils.test.ts +++ b/src/services/item/plugins/importExport/utils.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { AppItemFactory, DocumentItemFactory, diff --git a/src/services/item/plugins/invitation/invitation.controller.test.ts b/src/services/item/plugins/invitation/invitation.controller.test.ts index 329e822dc5..eed33b0559 100644 --- a/src/services/item/plugins/invitation/invitation.controller.test.ts +++ b/src/services/item/plugins/invitation/invitation.controller.test.ts @@ -17,6 +17,7 @@ import build, { mockAuthenticate, unmockAuthenticate, } from '../../../../../test/app'; +import { uniqueEmail } from '../../../../../test/factories/member.factory'; import { seedFromJson } from '../../../../../test/mocks/seed'; import { resolveDependency } from '../../../../di/utils'; import { db } from '../../../../drizzle/db'; @@ -233,7 +234,7 @@ describe('Invitation Plugin', () => { mockAuthenticate(actor); const invitation = { - email: faker.internet.email({ firstName: 'Alice', lastName: 'Bob' }), + email: uniqueEmail(), permission: PermissionLevel.Read, }; const response = await app.inject({ diff --git a/src/services/item/plugins/itemBookmark/itemBookmark.repository.test.ts b/src/services/item/plugins/itemBookmark/itemBookmark.repository.spec.ts similarity index 98% rename from src/services/item/plugins/itemBookmark/itemBookmark.repository.test.ts rename to src/services/item/plugins/itemBookmark/itemBookmark.repository.spec.ts index 24c8f3ec36..42d7dd2814 100644 --- a/src/services/item/plugins/itemBookmark/itemBookmark.repository.test.ts +++ b/src/services/item/plugins/itemBookmark/itemBookmark.repository.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import { seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; diff --git a/src/services/item/plugins/itemFlag/itemFlag.errors.ts b/src/services/item/plugins/itemFlag/itemFlag.errors.ts new file mode 100644 index 0000000000..5fadb91e1a --- /dev/null +++ b/src/services/item/plugins/itemFlag/itemFlag.errors.ts @@ -0,0 +1,3 @@ +import { createError } from '@fastify/error'; + +export const FlagNotFound = createError('GIFERR003', 'Flag not found', 404); diff --git a/src/services/item/plugins/itemFlag/utils/constants.ts b/src/services/item/plugins/itemFlag/utils/constants.ts deleted file mode 100644 index 5714d8a181..0000000000 --- a/src/services/item/plugins/itemFlag/utils/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PLUGIN_NAME = 'graasp-plugin-item-flagging'; diff --git a/src/services/item/plugins/itemFlag/utils/errors.ts b/src/services/item/plugins/itemFlag/utils/errors.ts deleted file mode 100644 index 6f41b8fedf..0000000000 --- a/src/services/item/plugins/itemFlag/utils/errors.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ErrorFactory } from '@graasp/sdk'; - -import { PLUGIN_NAME } from './constants'; - -export const GraaspItemFlagsError = ErrorFactory(PLUGIN_NAME); -export class FlagNotFound extends GraaspItemFlagsError { - constructor(data?: unknown) { - super({ code: 'GIFERR003', statusCode: 404, message: 'Flag not found' }, data); - } -} diff --git a/src/services/item/plugins/itemLike/itemLike.controller.test.ts b/src/services/item/plugins/itemLike/itemLike.controller.test.ts index 838130c80f..6f509c819d 100644 --- a/src/services/item/plugins/itemLike/itemLike.controller.test.ts +++ b/src/services/item/plugins/itemLike/itemLike.controller.test.ts @@ -21,7 +21,7 @@ import { MemberCannotAccess } from '../../../../utils/errors'; import { assertIsMember } from '../../../authentication'; import { ItemWrapper, type PackedItem } from '../../ItemWrapper'; import { expectManyPackedItems } from '../../test/fixtures/items'; -import { ItemLikeNotFound } from './utils/errors'; +import { ItemLikeNotFound } from './itemLike.errors'; export const expectItemLike = ( newLike: ItemLikeRaw | undefined, @@ -75,7 +75,7 @@ describe('Item Like', () => { unmockAuthenticate(); }); - describe('GET /liked', () => { + describe('GET /api/items/liked', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, @@ -133,7 +133,7 @@ describe('Item Like', () => { }); }); - describe('GET /:itemId/likes', () => { + describe('GET /api/items/:itemId/likes', () => { describe('Signed Out', () => { it('Throws if signed out', async () => { const { @@ -144,7 +144,7 @@ describe('Item Like', () => { }); const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/likes`, + url: `/api/items/${item.id}/likes`, }); expect(response.json()).toMatchObject(new MemberCannotAccess(expect.anything())); @@ -162,7 +162,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/likes`, + url: `/api/items/${item.id}/likes`, }); expect(res.statusCode).toBe(StatusCodes.OK); // get item like from repository with item (not returned in request) @@ -183,7 +183,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/likes`, + url: `/api/items/${item.id}/likes`, }); expect(res.statusCode).toBe(StatusCodes.NOT_FOUND); }); @@ -213,7 +213,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: `/items/${item1.id}/likes`, + url: `/api/items/${item1.id}/likes`, }); expect(res.statusCode).toBe(StatusCodes.OK); // get item like from repository with item (not returned in request) @@ -234,7 +234,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/likes`, + url: `/api/items/${item.id}/likes`, }); expect(res.json()).toEqual(new MemberCannotAccess(item.id)); }); @@ -258,7 +258,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/likes`, + url: `/api/items/${item.id}/likes`, }); expect(res.statusCode).toBe(StatusCodes.OK); @@ -282,7 +282,7 @@ describe('Item Like', () => { }); }); - describe('POST /:itemId/like', () => { + describe('POST /api/items/:itemId/like', () => { it('Throws if signed out', async () => { const { items: [item], @@ -290,7 +290,7 @@ describe('Item Like', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -315,7 +315,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(res.statusCode).toBe(StatusCodes.OK); @@ -345,7 +345,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(res.statusCode).toBe(StatusCodes.OK); // the creatorId should be the same @@ -373,7 +373,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(res.json()).toEqual(new MemberCannotAccess(item.id)); }); @@ -393,7 +393,7 @@ describe('Item Like', () => { }); }); - describe('DELETE :itemId/like', () => { + describe('DELETE /api/items/:itemId/like', () => { it('Throws if signed out', async () => { const { items: [item], @@ -401,7 +401,7 @@ describe('Item Like', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -427,7 +427,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(res.statusCode).toBe(StatusCodes.OK); expect(res.body).toEqual(itemLike.id); @@ -451,7 +451,7 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); expect(res.json()).toEqual(new MemberCannotAccess(item.id)); @@ -477,9 +477,13 @@ describe('Item Like', () => { const res = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/like`, + url: `/api/items/${item.id}/like`, }); - expect(res.json()).toEqual(new ItemLikeNotFound({ creatorId: actor.id, itemId: item.id })); + const response = await res.json(); + const expectedRes = new ItemLikeNotFound({ creatorId: actor.id, itemId: item.id }); + expect(response.message).toEqual(expectedRes.message); + expect(response.statusCode).toEqual(expectedRes.statusCode); + expect(response.code).toEqual(expectedRes.code); }); it('Bad request if item id is invalid', async () => { diff --git a/src/services/item/plugins/itemLike/itemLike.errors.ts b/src/services/item/plugins/itemLike/itemLike.errors.ts new file mode 100644 index 0000000000..a6c0c8974b --- /dev/null +++ b/src/services/item/plugins/itemLike/itemLike.errors.ts @@ -0,0 +1,19 @@ +import { StatusCodes } from 'http-status-codes'; + +import { createError } from '@fastify/error'; + +import { ErrorFactory } from '@graasp/sdk'; + +export const GraaspItemLikeError = ErrorFactory('graasp-plugin-item-like'); + +export const ItemLikeNotFound = createError( + 'GPILERR001', + 'Item Like not found', + StatusCodes.NOT_FOUND, +); + +export const CannotGetOthersLikes = createError( + 'GPILERR002', + "You cannot get other members' likes", + StatusCodes.NOT_FOUND, +); diff --git a/src/services/item/plugins/itemLike/itemLike.repository.test.ts b/src/services/item/plugins/itemLike/itemLike.repository.spec.ts similarity index 98% rename from src/services/item/plugins/itemLike/itemLike.repository.test.ts rename to src/services/item/plugins/itemLike/itemLike.repository.spec.ts index 81a12009e5..2c862b6c45 100644 --- a/src/services/item/plugins/itemLike/itemLike.repository.test.ts +++ b/src/services/item/plugins/itemLike/itemLike.repository.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import { FolderItemFactory } from '@graasp/sdk'; diff --git a/src/services/item/plugins/itemLike/itemLike.repository.ts b/src/services/item/plugins/itemLike/itemLike.repository.ts index de32fd8401..9fc2a69252 100644 --- a/src/services/item/plugins/itemLike/itemLike.repository.ts +++ b/src/services/item/plugins/itemLike/itemLike.repository.ts @@ -11,7 +11,7 @@ import type { ItemLikeWithItemWithCreator, } from '../../../../drizzle/types'; import { IllegalArgumentException } from '../../../../repositories/errors'; -import { ItemLikeNotFound } from './utils/errors'; +import { ItemLikeNotFound } from './itemLike.errors'; type CreatorId = ItemLikeRaw['creatorId']; type ItemId = ItemLikeRaw['itemId']; diff --git a/src/services/item/plugins/itemLike/itemLike.service.test.ts b/src/services/item/plugins/itemLike/itemLike.service.spec.ts similarity index 54% rename from src/services/item/plugins/itemLike/itemLike.service.test.ts rename to src/services/item/plugins/itemLike/itemLike.service.spec.ts index 07031baec9..b77ebd1d87 100644 --- a/src/services/item/plugins/itemLike/itemLike.service.test.ts +++ b/src/services/item/plugins/itemLike/itemLike.service.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { db } from '../../../../drizzle/db'; import type { @@ -16,25 +17,25 @@ import { ItemLikeRepository } from './itemLike.repository'; import { ItemLikeService } from './itemLike.service'; const authorizedItemService = { - getItemById: jest.fn() as AuthorizedItemService['getItemById'], + getItemById: vi.fn() as AuthorizedItemService['getItemById'], } as AuthorizedItemService; const meilisearchWrapper = { - updateItem: jest.fn() as MeiliSearchWrapper['updateItem'], + updateItem: vi.fn() as MeiliSearchWrapper['updateItem'], } as MeiliSearchWrapper; const itemLikeRepository = { - addOne: jest.fn() as ItemLikeRepository['addOne'], - deleteOneByCreatorAndItem: jest.fn() as ItemLikeRepository['deleteOneByCreatorAndItem'], - getCountByItemId: jest.fn() as ItemLikeRepository['getCountByItemId'], + addOne: vi.fn() as ItemLikeRepository['addOne'], + deleteOneByCreatorAndItem: vi.fn() as ItemLikeRepository['deleteOneByCreatorAndItem'], + getCountByItemId: vi.fn() as ItemLikeRepository['getCountByItemId'], } as ItemLikeRepository; const itemMembershipRepository = { - getForManyItems: jest.fn(async () => {}), + getForManyItems: vi.fn(async () => {}), } as unknown as ItemMembershipRepository; const itemVisibilityRepository = { - getManyForMany: jest.fn(async () => {}), + getManyForMany: vi.fn(async () => {}), } as unknown as ItemVisibilityRepository; const itemPublishedRepository = { - getForItem: jest.fn() as ItemPublishedRepository['getForItem'], + getForItem: vi.fn() as ItemPublishedRepository['getForItem'], } as ItemPublishedRepository; const itemLikeService = new ItemLikeService( @@ -50,13 +51,13 @@ const MOCK_LIKE = { creatorId: v4(), itemId: v4() } as ItemLikeRaw; describe('Item Like post', () => { afterEach(async () => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('does not update like count for indexed item if it is not published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); - jest.spyOn(itemLikeRepository, 'addOne').mockResolvedValue(MOCK_LIKE); - const updateItemMock = jest.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); + vi.spyOn(itemLikeRepository, 'addOne').mockResolvedValue(MOCK_LIKE); + const updateItemMock = vi.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); await itemLikeService.post(db, {} as MinimalMember, v4()); @@ -64,12 +65,12 @@ describe('Item Like post', () => { }); it('update like count for indexed item if it is published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); - jest.spyOn(itemLikeRepository, 'addOne').mockResolvedValue(MOCK_LIKE); - const updateItemMock = jest.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); - jest - .spyOn(itemPublishedRepository, 'getForItem') - .mockResolvedValue({} as ItemPublishedWithItemWithCreator); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); + vi.spyOn(itemLikeRepository, 'addOne').mockResolvedValue(MOCK_LIKE); + const updateItemMock = vi.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue( + {} as ItemPublishedWithItemWithCreator, + ); await itemLikeService.post(db, {} as MinimalMember, v4()); @@ -79,13 +80,13 @@ describe('Item Like post', () => { describe('Item Like removeOne', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('do not update like count for indexed item if it is not published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); - jest.spyOn(itemLikeRepository, 'deleteOneByCreatorAndItem').mockResolvedValue(MOCK_LIKE); - const updateItemMock = jest.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); + vi.spyOn(itemLikeRepository, 'deleteOneByCreatorAndItem').mockResolvedValue(MOCK_LIKE); + const updateItemMock = vi.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); await itemLikeService.removeOne(db, {} as MinimalMember, v4()); @@ -93,12 +94,12 @@ describe('Item Like removeOne', () => { }); it('update like count for indexed item if it is published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); - jest.spyOn(itemLikeRepository, 'deleteOneByCreatorAndItem').mockResolvedValue(MOCK_LIKE); - const updateItemMock = jest.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); - jest - .spyOn(itemPublishedRepository, 'getForItem') - .mockResolvedValue({} as ItemPublishedWithItemWithCreator); + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemWithCreator); + vi.spyOn(itemLikeRepository, 'deleteOneByCreatorAndItem').mockResolvedValue(MOCK_LIKE); + const updateItemMock = vi.spyOn(meilisearchWrapper, 'updateItem').mockResolvedValue(); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue( + {} as ItemPublishedWithItemWithCreator, + ); await itemLikeService.removeOne(db, {} as MinimalMember, v4()); diff --git a/src/services/item/plugins/itemLike/utils/errors.ts b/src/services/item/plugins/itemLike/utils/errors.ts deleted file mode 100644 index f17146a37c..0000000000 --- a/src/services/item/plugins/itemLike/utils/errors.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; - -import { ErrorFactory } from '@graasp/sdk'; - -export const GraaspItemLikeError = ErrorFactory('graasp-plugin-item-like'); - -export class ItemLikeNotFound extends GraaspItemLikeError { - constructor(data?: unknown) { - super( - { - code: 'GPILERR001', - statusCode: StatusCodes.NOT_FOUND, - message: 'Item Like not found', - }, - data, - ); - } -} - -export class CannotGetOthersLikes extends GraaspItemLikeError { - constructor(data?: unknown) { - super( - { - code: 'GPILERR002', - statusCode: StatusCodes.NOT_FOUND, - message: "You cannot get other members' likes", - }, - data, - ); - } -} diff --git a/src/workers/itemExportRequest.worker.spec.ts b/src/workers/itemExportRequest.worker.spec.ts index aae72eabaa..f7fa8cc1c8 100644 --- a/src/workers/itemExportRequest.worker.spec.ts +++ b/src/workers/itemExportRequest.worker.spec.ts @@ -50,6 +50,7 @@ describe('ItemExportRequest worker', { sequential: true }, () => { // expect the exportFolder to have been called expect(itemExportRequestService.exportFolderZipAndSendByEmail).toHaveBeenCalled(); + await queue.close(); }); it('fail if processing throws', async () => { @@ -82,6 +83,7 @@ describe('ItemExportRequest worker', { sequential: true }, () => { .toBeTruthy(); // expect the exportFolder to have been called expect(itemExportRequestService.exportFolderZipAndSendByEmail).toHaveBeenCalled(); + await queue.close(); }); it('fail with wrong data', async () => { @@ -106,5 +108,6 @@ describe('ItemExportRequest worker', { sequential: true }, () => { .toBeTruthy(); // expect the exportFolder to not have been called expect(itemExportRequestService.exportFolderZipAndSendByEmail).not.toHaveBeenCalled(); + await queue.close(); }); }); diff --git a/src/workers/searchIndex.worker.spec.ts b/src/workers/searchIndex.worker.spec.ts index 5715912da4..d6ad025129 100644 --- a/src/workers/searchIndex.worker.spec.ts +++ b/src/workers/searchIndex.worker.spec.ts @@ -40,6 +40,7 @@ describe('SearchIndex worker', { sequential: true }, () => { // expect the exportFolder to have been called expect(searchIndexService.buildIndex).toHaveBeenCalled(); + await queue.close(); }); it('fail if processing throws', async () => { @@ -63,5 +64,6 @@ describe('SearchIndex worker', { sequential: true }, () => { .toBeTruthy(); // expect the exportFolder to have been called expect(searchIndexService.buildIndex).toHaveBeenCalled(); + await queue.close(); }); }); diff --git a/test/factories/member.factory.ts b/test/factories/member.factory.ts index 42c658e63e..f10695350d 100644 --- a/test/factories/member.factory.ts +++ b/test/factories/member.factory.ts @@ -46,7 +46,7 @@ export const MemberFactory = (m: Partial = {}): MemberRaw => { * Ensures RFC-safe local-part characters and keeps domain from faker. * Example output: "john.doe+1731401674001-8f3a@example.com" */ -function uniqueEmail() { +export function uniqueEmail() { const base = faker.internet.email(); const [local, domain] = base.split('@'); From 69a10390872cb922d9fe99af0f2e131cb72f0e4d Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 09:59:18 +0000 Subject: [PATCH 08/24] fix(test): page, published --- ...t.ts => itemVisibility.repository.spec.ts} | 4 +- .../page/{WSDoc.test.ts => WSDoc.spec.ts} | 11 +-- .../item/plugins/page/page.controller.test.ts | 30 ++++---- ...sitory.test.ts => page.repository.spec.ts} | 1 + ...e.service.test.ts => page.service.spec.ts} | 1 + ...State.test.ts => publicationState.spec.ts} | 2 + ...st.ts => itemPublished.repository.spec.ts} | 6 +- .../published/itemPublished.repository.ts | 3 + ....test.ts => itemPublished.service.spec.ts} | 9 ++- ...test.ts => meilisearch.repository.spec.ts} | 1 + ...eilisearch.test.ts => meilisearch.spec.ts} | 62 +++++++-------- .../plugins/search/search.controller.test.ts | 10 +-- ...service.test.ts => search.service.spec.ts} | 75 +++++++------------ .../item/test/fixtures/items.vitest.ts | 41 ++++++++++ 14 files changed, 142 insertions(+), 114 deletions(-) rename src/services/item/plugins/itemVisibility/{itemVisibility.repository.test.ts => itemVisibility.repository.spec.ts} (95%) rename src/services/item/plugins/page/{WSDoc.test.ts => WSDoc.spec.ts} (80%) rename src/services/item/plugins/page/{page.repository.test.ts => page.repository.spec.ts} (99%) rename src/services/item/plugins/page/{page.service.test.ts => page.service.spec.ts} (99%) rename src/services/item/plugins/publication/publicationState/{publicationState.test.ts => publicationState.spec.ts} (98%) rename src/services/item/plugins/publication/published/{itemPublished.repository.test.ts => itemPublished.repository.spec.ts} (88%) rename src/services/item/plugins/publication/published/{itemPublished.service.test.ts => itemPublished.service.spec.ts} (86%) rename src/services/item/plugins/publication/published/plugins/search/{meilisearch.repository.test.ts => meilisearch.repository.spec.ts} (99%) rename src/services/item/plugins/publication/published/plugins/search/{meilisearch.test.ts => meilisearch.spec.ts} (72%) rename src/services/item/plugins/publication/published/plugins/search/{search.service.test.ts => search.service.spec.ts} (78%) create mode 100644 src/services/item/test/fixtures/items.vitest.ts diff --git a/src/services/item/plugins/itemVisibility/itemVisibility.repository.test.ts b/src/services/item/plugins/itemVisibility/itemVisibility.repository.spec.ts similarity index 95% rename from src/services/item/plugins/itemVisibility/itemVisibility.repository.test.ts rename to src/services/item/plugins/itemVisibility/itemVisibility.repository.spec.ts index 101dd0cd35..d05ba99316 100644 --- a/src/services/item/plugins/itemVisibility/itemVisibility.repository.test.ts +++ b/src/services/item/plugins/itemVisibility/itemVisibility.repository.spec.ts @@ -1,9 +1,11 @@ +import { describe, expect, it } from 'vitest'; + import { type ItemVisibilityOptionsType, ItemVisibilityType } from '@graasp/sdk'; import { seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; import { itemVisibilitiesTable } from '../../../../drizzle/schema'; -import { expectItem } from '../../test/fixtures/items'; +import { expectItem } from '../../test/fixtures/items.vitest'; import { ItemVisibilityRepository } from './itemVisibility.repository'; const repository = new ItemVisibilityRepository(); diff --git a/src/services/item/plugins/page/WSDoc.test.ts b/src/services/item/plugins/page/WSDoc.spec.ts similarity index 80% rename from src/services/item/plugins/page/WSDoc.test.ts rename to src/services/item/plugins/page/WSDoc.spec.ts index 277cdeb3e5..9c5a718cad 100644 --- a/src/services/item/plugins/page/WSDoc.test.ts +++ b/src/services/item/plugins/page/WSDoc.spec.ts @@ -1,16 +1,17 @@ +import { describe, expect, it, vi } from 'vitest'; import { WebSocket } from 'ws'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { WSDoc } from './WSDoc'; const conn1 = { - on: jest.fn(), - close: jest.fn(), + on: vi.fn(), + close: vi.fn(), } as unknown as WebSocket; const conn2 = { - on: jest.fn(), - close: jest.fn(), + on: vi.fn(), + close: vi.fn(), } as unknown as WebSocket; describe('WSDoc', () => { diff --git a/src/services/item/plugins/page/page.controller.test.ts b/src/services/item/plugins/page/page.controller.test.ts index 812f11a219..a3f0c542b8 100644 --- a/src/services/item/plugins/page/page.controller.test.ts +++ b/src/services/item/plugins/page/page.controller.test.ts @@ -89,7 +89,7 @@ describe('Page routes tests', () => { unmockAuthenticate(); }); - describe('POST /items/pages', () => { + describe('POST /api/items/pages', () => { it('Throws if signed out', async () => { const payload = FolderItemFactory(); const response = await app.inject({ @@ -125,11 +125,11 @@ describe('Page routes tests', () => { }); }); - describe('GET /items/pages/ws', () => { + describe('GET /api/items/pages/ws', () => { it('Throw if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: { protocol: 'ws', pathname: `/items/pages/${v4()}/ws` }, + url: { protocol: 'ws', pathname: `/api/items/pages/${v4()}/ws` }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -161,7 +161,7 @@ describe('Page routes tests', () => { method: HttpMethod.Get, path: { protocol: 'ws', - pathname: `/items/pages/${item.id}/ws`, + pathname: `/api/items/pages/${item.id}/ws`, }, }); @@ -187,7 +187,7 @@ describe('Page routes tests', () => { method: HttpMethod.Get, path: { protocol: 'ws', - pathname: `/items/pages/${item.id}/ws`, + pathname: `/api/items/pages/${item.id}/ws`, }, }); @@ -211,7 +211,7 @@ describe('Page routes tests', () => { // start server to correctly listen to websockets const port = await getAppPort(app); - const ws = new WebSocket(`http://localhost:${port}/items/pages/${item.id}/ws`); + const ws = new WebSocket(`http://localhost:${port}/api/items/pages/${item.id}/ws`); await new Promise((done, reject) => { ws.on('error', (e) => { @@ -405,7 +405,7 @@ describe('Page routes tests', () => { assertIsDefined(actor); mockAuthenticate(actor); const port = await getAppPort(app); - const ws = new WebSocket(`ws://localhost:${port}/items/pages/${item.id}/ws`); + const ws = new WebSocket(`ws://localhost:${port}/api/items/pages/${item.id}/ws`); // connection should close let hasClosed = false; @@ -430,7 +430,7 @@ describe('Page routes tests', () => { }); }); - describe('GET /items/pages/ws/read', () => { + describe('GET /api/items/pages/ws/read', () => { it('Throws if id is incorrect', async () => { const response = await app.inject({ method: HttpMethod.Get, @@ -452,7 +452,7 @@ describe('Page routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: { protocol: 'ws', pathname: `/items/pages/${item.id}/ws/read` }, + url: { protocol: 'ws', pathname: `/api/items/pages/${item.id}/ws/read` }, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -469,7 +469,7 @@ describe('Page routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Get, - url: { protocol: 'ws', pathname: `/items/pages/${item.id}/ws/read` }, + url: { protocol: 'ws', pathname: `/api/items/pages/${item.id}/ws/read` }, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -489,7 +489,7 @@ describe('Page routes tests', () => { method: HttpMethod.Get, path: { protocol: 'ws', - pathname: `/items/pages/${item.id}/ws/read`, + pathname: `/api/items/pages/${item.id}/ws/read`, }, }); @@ -514,7 +514,7 @@ describe('Page routes tests', () => { // start server to correctly listen to websockets await app.ready(); const port = (app.server.address() as AddressInfo)!.port; - const ws = new WebSocket(`http://localhost:${port}/items/pages/${item.id}/ws/read`); + const ws = new WebSocket(`http://localhost:${port}/api/items/pages/${item.id}/ws/read`); await new Promise((done, reject) => { ws.on('error', (e) => { @@ -550,7 +550,7 @@ describe('Page routes tests', () => { // start server to correctly listen to websockets await app.ready(); const port = (app.server.address() as AddressInfo)!.port; - const ws = new WebSocket(`http://localhost:${port}/items/pages/${item.id}/ws/read`); + const ws = new WebSocket(`http://localhost:${port}/api/items/pages/${item.id}/ws/read`); await new Promise((done, reject) => { ws.on('error', (e) => { @@ -584,7 +584,7 @@ describe('Page routes tests', () => { // start server to correctly listen to websockets await app.ready(); const port = (app.server.address() as AddressInfo)!.port; - const ws = new WebSocket(`http://localhost:${port}/items/pages/${item.id}/ws/read`); + const ws = new WebSocket(`http://localhost:${port}/api/items/pages/${item.id}/ws/read`); await new Promise((done, reject) => { ws.on('error', (e) => { @@ -793,7 +793,7 @@ describe('Page routes tests', () => { assertIsDefined(actor); mockAuthenticate(actor); const port = await getAppPort(app); - const ws = new WebSocket(`ws://localhost:${port}/items/pages/${item.id}/ws/read`); + const ws = new WebSocket(`ws://localhost:${port}/api/items/pages/${item.id}/ws/read`); // connection should close let hasClosed = false; diff --git a/src/services/item/plugins/page/page.repository.test.ts b/src/services/item/plugins/page/page.repository.spec.ts similarity index 99% rename from src/services/item/plugins/page/page.repository.test.ts rename to src/services/item/plugins/page/page.repository.spec.ts index 831298690e..8a8a7a31a6 100644 --- a/src/services/item/plugins/page/page.repository.test.ts +++ b/src/services/item/plugins/page/page.repository.spec.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import { eq } from 'drizzle-orm/sql'; +import { describe, expect, it } from 'vitest'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error import { Doc, encodeStateAsUpdate } from 'yjs'; diff --git a/src/services/item/plugins/page/page.service.test.ts b/src/services/item/plugins/page/page.service.spec.ts similarity index 99% rename from src/services/item/plugins/page/page.service.test.ts rename to src/services/item/plugins/page/page.service.spec.ts index dbd48313de..131ba7a575 100644 --- a/src/services/item/plugins/page/page.service.test.ts +++ b/src/services/item/plugins/page/page.service.spec.ts @@ -1,4 +1,5 @@ import { eq } from 'drizzle-orm/sql'; +import { describe, expect, it } from 'vitest'; import waitForExpect from 'wait-for-expect'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error diff --git a/src/services/item/plugins/publication/publicationState/publicationState.test.ts b/src/services/item/plugins/publication/publicationState/publicationState.spec.ts similarity index 98% rename from src/services/item/plugins/publication/publicationState/publicationState.test.ts rename to src/services/item/plugins/publication/publicationState/publicationState.spec.ts index 45edeaa798..e97f866cf3 100644 --- a/src/services/item/plugins/publication/publicationState/publicationState.test.ts +++ b/src/services/item/plugins/publication/publicationState/publicationState.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { ItemType, ItemValidationStatus, PublicationStatus } from '@graasp/sdk'; import { PublicationState } from './publicationState'; diff --git a/src/services/item/plugins/publication/published/itemPublished.repository.test.ts b/src/services/item/plugins/publication/published/itemPublished.repository.spec.ts similarity index 88% rename from src/services/item/plugins/publication/published/itemPublished.repository.test.ts rename to src/services/item/plugins/publication/published/itemPublished.repository.spec.ts index 17e51dad57..80ef9b7baa 100644 --- a/src/services/item/plugins/publication/published/itemPublished.repository.test.ts +++ b/src/services/item/plugins/publication/published/itemPublished.repository.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { seedFromJson } from '../../../../../../test/mocks/seed'; import { db } from '../../../../../drizzle/db'; import { ItemPublishedRepository } from './itemPublished.repository'; @@ -10,8 +12,8 @@ describe('ItemPublishedRepository', () => { await expect(() => // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - repository.touchUpdatedAt(undefined), - ).rejects.toThrow(); + repository.touchUpdatedAt(db, undefined), + ).rejects.toThrowError(new Error('path is not defined')); }); it('update updatedAt on current time', async () => { diff --git a/src/services/item/plugins/publication/published/itemPublished.repository.ts b/src/services/item/plugins/publication/published/itemPublished.repository.ts index 8ecba26515..adceeac084 100644 --- a/src/services/item/plugins/publication/published/itemPublished.repository.ts +++ b/src/services/item/plugins/publication/published/itemPublished.repository.ts @@ -121,6 +121,9 @@ export class ItemPublishedRepository { } async touchUpdatedAt(dbConnection: DBConnection, path: ItemRaw['path']): Promise { + if (!path) { + throw new Error('path is not defined'); + } const updatedAt = new Date().toISOString(); const result = await dbConnection diff --git a/src/services/item/plugins/publication/published/itemPublished.service.test.ts b/src/services/item/plugins/publication/published/itemPublished.service.spec.ts similarity index 86% rename from src/services/item/plugins/publication/published/itemPublished.service.test.ts rename to src/services/item/plugins/publication/published/itemPublished.service.spec.ts index 948ed78b43..4c1c94dd89 100644 --- a/src/services/item/plugins/publication/published/itemPublished.service.test.ts +++ b/src/services/item/plugins/publication/published/itemPublished.service.spec.ts @@ -1,8 +1,9 @@ import { v4 } from 'uuid'; +import { describe, expect, it, vi } from 'vitest'; import { buildPathFromIds } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../../test/app.vitest'; import { db } from '../../../../../drizzle/db'; import { MailerService } from '../../../../../plugins/mailer/mailer.service'; import { AuthorizedItemService } from '../../../../authorizedItem.service'; @@ -17,7 +18,7 @@ import { ItemPublishedService } from './itemPublished.service'; import { MeiliSearchWrapper } from './plugins/search/meilisearch'; const meiliSearchWrapper = { - updateItem: jest.fn(), + updateItem: vi.fn(), } as unknown as MeiliSearchWrapper; const itemPublishedRepository = new ItemPublishedRepository(); @@ -44,8 +45,8 @@ describe('ItemPublishedService - touchUpdatedAt', () => { const updatedAt = new Date().toISOString(); // MOCK - const updateItemMock = jest.spyOn(meiliSearchWrapper, 'updateItem'); - jest.spyOn(itemPublishedRepository, 'touchUpdatedAt').mockResolvedValue(updatedAt); + const updateItemMock = vi.spyOn(meiliSearchWrapper, 'updateItem'); + vi.spyOn(itemPublishedRepository, 'touchUpdatedAt').mockResolvedValue(updatedAt); // WHEN await itemPublishedService.touchUpdatedAt(db, item); diff --git a/src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.test.ts b/src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.spec.ts similarity index 99% rename from src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.test.ts rename to src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.spec.ts index c7d324b0f4..9ccd404958 100644 --- a/src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.test.ts +++ b/src/services/item/plugins/publication/published/plugins/search/meilisearch.repository.spec.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import { describe, expect, it } from 'vitest'; import { seedFromJson } from '../../../../../../../../test/mocks/seed'; import { db } from '../../../../../../../drizzle/db'; diff --git a/src/services/item/plugins/publication/published/plugins/search/meilisearch.test.ts b/src/services/item/plugins/publication/published/plugins/search/meilisearch.spec.ts similarity index 72% rename from src/services/item/plugins/publication/published/plugins/search/meilisearch.test.ts rename to src/services/item/plugins/publication/published/plugins/search/meilisearch.spec.ts index b4c84b39ba..ce725c10b8 100644 --- a/src/services/item/plugins/publication/published/plugins/search/meilisearch.test.ts +++ b/src/services/item/plugins/publication/published/plugins/search/meilisearch.spec.ts @@ -1,4 +1,3 @@ -import { expect, jest } from '@jest/globals'; import { EnqueuedTask, Index, @@ -9,10 +8,11 @@ import { TaskStatus, } from 'meilisearch'; import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { type IndexItem } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../../../../test/app.vitest'; import { ItemFactory } from '../../../../../../../../test/factories/item.factory'; import { type DBConnection } from '../../../../../../../drizzle/db'; import type { ItemPublishedWithItemWithCreator } from '../../../../../../../drizzle/types'; @@ -21,8 +21,6 @@ import { ItemPublishedRepository } from '../../itemPublished.repository'; import { ACTIVE_INDEX, MeiliSearchWrapper } from './meilisearch'; import { MeilisearchRepository } from './meilisearch.repository'; -jest.unmock('./meilisearch'); - const mockItemPublished = ({ id, path, @@ -44,22 +42,22 @@ const MOCK_DB = {} as DBConnection; const mockIndex = { uid: ACTIVE_INDEX, - addDocuments: jest.fn(() => { + addDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - waitForTask: jest.fn(() => { + waitForTask: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), - waitForTasks: jest.fn(() => { + waitForTasks: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), - deleteDocuments: jest.fn(() => { + deleteDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - deleteAllDocuments: jest.fn(() => { + deleteAllDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), -} as unknown as jest.Mocked>; +} as unknown as Index; const fakeClient = new MeiliSearch({ host: 'fake', @@ -67,25 +65,23 @@ const fakeClient = new MeiliSearch({ httpClient: () => Promise.resolve(), }); -jest.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [mockIndex], total: 1 }); -jest.spyOn(fakeClient, 'getIndex').mockResolvedValue(mockIndex); -jest.spyOn(fakeClient, 'index').mockReturnValue({ - updateFaceting: jest.fn(async () => { +vi.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [mockIndex], total: 1 }); +vi.spyOn(fakeClient, 'getIndex').mockResolvedValue(mockIndex); +vi.spyOn(fakeClient, 'index').mockReturnValue({ + updateFaceting: vi.fn(async () => { return { taskUid: '1' } as unknown as EnqueuedTask; }), - updateSettings: jest.fn(() => { + updateSettings: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - waitForTask: jest.fn(() => { + waitForTask: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), } as never); -jest - .spyOn(fakeClient, 'swapIndexes') - .mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); -jest - .spyOn(fakeClient, 'waitForTask') - .mockResolvedValue({ status: TaskStatus.TASK_SUCCEEDED } as Task); +vi.spyOn(fakeClient, 'swapIndexes').mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); +vi.spyOn(fakeClient, 'waitForTask').mockResolvedValue({ + status: TaskStatus.TASK_SUCCEEDED, +} as Task); const itemPublishedRepository = new ItemPublishedRepository(); const itemRepository = new ItemRepository(); @@ -100,11 +96,11 @@ const meilisearch = new MeiliSearchWrapper( describe('MeilisearchWrapper', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('search', () => { - const searchSpy = jest + const searchSpy = vi .spyOn(MeiliSearch.prototype, 'multiSearch') .mockResolvedValue({} as MultiSearchResponse); @@ -126,11 +122,11 @@ describe('MeilisearchWrapper', () => { const item = ItemFactory(); // Given - jest.spyOn(itemRepository, 'getDescendants').mockResolvedValue([]); + vi.spyOn(itemRepository, 'getDescendants').mockResolvedValue([]); const itemPublished = mockItemPublished(item); - jest.spyOn(meilisearchRepository, 'getIndexedTree').mockResolvedValue([{} as never]); + vi.spyOn(meilisearchRepository, 'getIndexedTree').mockResolvedValue([{} as never]); - const addDocumentSpy = jest.spyOn(mockIndex, 'addDocuments'); + const addDocumentSpy = vi.spyOn(mockIndex, 'addDocuments'); // When await meilisearch.indexOne(MOCK_DB, itemPublished); @@ -142,13 +138,13 @@ describe('MeilisearchWrapper', () => { it('can index multiple items', async () => { // Given const itemPublished1 = mockItemPublished(ItemFactory()); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(itemPublished1); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(itemPublished1); const itemPublished2 = mockItemPublished(ItemFactory()); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(itemPublished2); - const getIndexTreeMock = jest + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(itemPublished2); + const getIndexTreeMock = vi .spyOn(meilisearchRepository, 'getIndexedTree') .mockResolvedValue([{} as never]); - const addDocumentSpy = jest.spyOn(mockIndex, 'addDocuments'); + const addDocumentSpy = vi.spyOn(mockIndex, 'addDocuments'); // When await meilisearch.index(MOCK_DB, [itemPublished1, itemPublished2]); @@ -165,9 +161,9 @@ describe('MeilisearchWrapper', () => { const descendant = ItemFactory(); const descendant2 = ItemFactory(); - jest.spyOn(itemRepository, 'getDescendants').mockResolvedValue([descendant, descendant2]); + vi.spyOn(itemRepository, 'getDescendants').mockResolvedValue([descendant, descendant2]); - const deleteSpy = jest.spyOn(mockIndex, 'deleteDocuments'); + const deleteSpy = vi.spyOn(mockIndex, 'deleteDocuments'); await meilisearch.deleteOne(MOCK_DB, item); diff --git a/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts b/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts index 0a98343b4a..d5eb57a232 100644 --- a/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts +++ b/src/services/item/plugins/publication/published/plugins/search/search.controller.test.ts @@ -259,7 +259,7 @@ describe('Collection Search endpoints', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -421,7 +421,7 @@ describe('Collection Search endpoints', () => { }); }); - describe('GET /collections/facets', () => { + describe('GET /api/collections/facets', () => { it('throw if facet name is not provided', async () => { const res = await app.inject({ method: HttpMethod.Post, @@ -508,7 +508,7 @@ describe('Collection Search endpoints', () => { }); }); - describe('GET /collections/featured', () => { + describe('GET /api/collections/featured', () => { it('get featured collections', async () => { // Meilisearch is mocked so format of API doesn't matter, we just want it to proxy MultiSearchParams; const fakeResponse = { @@ -619,7 +619,7 @@ describe('Collection Search endpoints', () => { }); }); - describe('GET /collections/liked', () => { + describe('GET /api/collections/liked', () => { it('get most liked items', async () => { // Meilisearch is mocked so format of API doesn't matter, we just want it to proxy MultiSearchParams; const fakeResponse = { @@ -729,7 +729,7 @@ describe('Collection Search endpoints', () => { }); }); }); - describe('GET /collections/recent', () => { + describe('GET /api/collections/recent', () => { describe('Signed Out', () => { it('Get 2 most recent collections', async () => { // Meilisearch is mocked so format of API doesn't matter, we just want it to proxy MultiSearchParams; diff --git a/src/services/item/plugins/publication/published/plugins/search/search.service.test.ts b/src/services/item/plugins/publication/published/plugins/search/search.service.spec.ts similarity index 78% rename from src/services/item/plugins/publication/published/plugins/search/search.service.test.ts rename to src/services/item/plugins/publication/published/plugins/search/search.service.spec.ts index 0edbabb08f..eb25b5ac37 100644 --- a/src/services/item/plugins/publication/published/plugins/search/search.service.test.ts +++ b/src/services/item/plugins/publication/published/plugins/search/search.service.spec.ts @@ -1,6 +1,7 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; -import { MOCK_LOGGER } from '../../../../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../../../../test/app.vitest'; import { GRAASPER_CREATOR_ID } from '../../../../../../../utils/config'; import HookManager from '../../../../../../../utils/hook'; import { ItemService } from '../../../../../item.service'; @@ -10,35 +11,33 @@ import { MeiliSearchWrapper } from './meilisearch'; import { SearchService } from './search.service'; const meilisearchClient = { - search: jest.fn(async () => { + search: vi.fn(async () => { return {} as never; }), - getActiveIndexName: jest.fn(() => 'indexname'), + getActiveIndexName: vi.fn(() => 'indexname'), } as unknown as MeiliSearchWrapper; const searchService = new SearchService( { // eslint-disable-next-line @typescript-eslint/no-explicit-any - hooks: { setPostHook: jest.fn() } as unknown as HookManager, + hooks: { setPostHook: vi.fn() } as unknown as HookManager, } as ItemService, { // eslint-disable-next-line @typescript-eslint/no-explicit-any - hooks: { setPostHook: jest.fn() } as unknown as HookManager, + hooks: { setPostHook: vi.fn() } as unknown as HookManager, } as ItemPublishedService, meilisearchClient, - { getUrlsByItems: jest.fn() } as unknown as ItemThumbnailService, + { getUrlsByItems: vi.fn() } as unknown as ItemThumbnailService, MOCK_LOGGER, ); describe('search', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('filter by tags', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.search({ tags: { @@ -57,9 +56,7 @@ describe('search', () => { }); it('filter by langs', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.search({ langs: ['fr', 'en'], @@ -72,9 +69,7 @@ describe('search', () => { }); it('filter by published root', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.search({ isPublishedRoot: true, @@ -87,9 +82,7 @@ describe('search', () => { }); it('filter by query', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.search({ query: 'hello', @@ -101,9 +94,7 @@ describe('search', () => { }); it('filter by creator id', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const creatorId = v4(); const results = await searchService.search({ @@ -116,9 +107,7 @@ describe('search', () => { }); it('apply sort', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.search({ sort: ['likes:desc'], @@ -132,13 +121,11 @@ describe('search', () => { describe('getMostLiked', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('apply sort by likes', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.getMostLiked(4); expect(results).toEqual(MOCK_RESULT); @@ -151,13 +138,11 @@ describe('getMostLiked', () => { describe('getMostRecent', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('apply sort by publication updatedAt', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.getMostRecent(4); expect(results).toEqual(MOCK_RESULT); @@ -170,14 +155,12 @@ describe('getMostRecent', () => { describe('getFeatured', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('apply creator and sort by publication updatedAt', async () => { const MOCK_RESULT = { hits: [] } as never; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); - const serviceSpy = jest.spyOn(searchService, 'search'); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); + const serviceSpy = vi.spyOn(searchService, 'search'); const results = await searchService.getFeatured(GRAASPER_CREATOR_ID, 4); expect(results).toEqual(MOCK_RESULT); @@ -194,7 +177,7 @@ describe('getFeatured', () => { describe('getFacets', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('filter by tags', async () => { const MOCK_RESULT = { @@ -204,7 +187,7 @@ describe('getFacets', () => { processingTimeMs: 1, query: '', }; - const spy = jest + const spy = vi .spyOn(meilisearchClient, 'search') .mockResolvedValue({ results: [MOCK_RESULT as never] }); @@ -230,9 +213,7 @@ describe('getFacets', () => { processingTimeMs: 1, query: '', }; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.getFacets('lang', { langs: ['fr', 'en'], @@ -250,9 +231,7 @@ describe('getFacets', () => { processingTimeMs: 1, query: '', }; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.getFacets('lang', { isPublishedRoot: true, @@ -270,9 +249,7 @@ describe('getFacets', () => { processingTimeMs: 1, query: '', }; - const spy = jest - .spyOn(meilisearchClient, 'search') - .mockResolvedValue({ results: [MOCK_RESULT] }); + const spy = vi.spyOn(meilisearchClient, 'search').mockResolvedValue({ results: [MOCK_RESULT] }); const results = await searchService.getFacets('lang', { query: 'hello', diff --git a/src/services/item/test/fixtures/items.vitest.ts b/src/services/item/test/fixtures/items.vitest.ts new file mode 100644 index 0000000000..83b64f079a --- /dev/null +++ b/src/services/item/test/fixtures/items.vitest.ts @@ -0,0 +1,41 @@ +import { expect } from 'vitest'; + +import { buildPathFromIds } from '@graasp/sdk'; + +import { ItemRaw } from '../../../../drizzle/types'; +import { MinimalMember } from '../../../../types'; + +export const expectItem = ( + newItem: Partial | undefined | null, + correctItem: Partial> | undefined | null, + creator?: MinimalMember, + parent?: ItemRaw, +) => { + if (!newItem || !newItem.id) { + throw new Error('expectItem.newItem is not defined ' + JSON.stringify(newItem)); + } + if (!correctItem) { + throw new Error('expectItem.correctItem is not defined ' + JSON.stringify(correctItem)); + } + expect(newItem.name).toEqual(correctItem.name); + expect(newItem.description).toEqual(correctItem.description ?? null); + expect(newItem.extra).toEqual(correctItem.extra); + expect(newItem.type).toEqual(correctItem.type); + if (correctItem.lang) { + expect(newItem.lang).toEqual(correctItem.lang); + } + expect(newItem.path).toContain(buildPathFromIds(newItem.id)); + if (parent) { + expect(newItem.path).toContain(buildPathFromIds(parent.path)); + } + + if (newItem.creatorId && creator) { + expect(newItem.creatorId).toEqual(creator.id); + } + + if (correctItem.settings) { + for (const [k, s] of Object.entries(correctItem.settings)) { + expect(newItem.settings![k]).toEqual(s); + } + } +}; From a4736fa0cc0cd1be2f930d4101f4fb8ee3e25318 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 10:14:12 +0000 Subject: [PATCH 09/24] fix: ws tests --- .../item/plugins/action/test/ws.test.ts | 4 ++-- ...tion.test.ts => badWordsDetection.spec.ts} | 2 ++ ...nQueue.test.ts => validationQueue.spec.ts} | 20 +++++++++---------- .../item/plugins/recycled/recycled.ws.test.ts | 6 +++--- src/services/item/ws/ws.test.ts | 12 +++++------ src/services/itemMembership/ws/ws.test.ts | 6 +++--- .../websockets/test/test-websocket-client.ts | 2 +- 7 files changed, 26 insertions(+), 26 deletions(-) rename src/services/item/plugins/publication/validation/processes/{badWordsDetection.test.ts => badWordsDetection.spec.ts} (93%) rename src/services/item/plugins/publication/validation/test/{validationQueue.test.ts => validationQueue.spec.ts} (85%) diff --git a/src/services/item/plugins/action/test/ws.test.ts b/src/services/item/plugins/action/test/ws.test.ts index 5be75803bf..fd930430cd 100644 --- a/src/services/item/plugins/action/test/ws.test.ts +++ b/src/services/item/plugins/action/test/ws.test.ts @@ -85,7 +85,7 @@ describe('asynchronous feedback', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/actions/export`, + url: `/api/items/${item.id}/actions/export`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -113,7 +113,7 @@ describe('asynchronous feedback', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/actions/export`, + url: `/api/items/${item.id}/actions/export`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); diff --git a/src/services/item/plugins/publication/validation/processes/badWordsDetection.test.ts b/src/services/item/plugins/publication/validation/processes/badWordsDetection.spec.ts similarity index 93% rename from src/services/item/plugins/publication/validation/processes/badWordsDetection.test.ts rename to src/services/item/plugins/publication/validation/processes/badWordsDetection.spec.ts index 0ccf308a61..10e0207691 100644 --- a/src/services/item/plugins/publication/validation/processes/badWordsDetection.test.ts +++ b/src/services/item/plugins/publication/validation/processes/badWordsDetection.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { detectFieldNameWithBadWords } from './badWordsDetection'; describe('detectFieldNameWithBadWords', () => { diff --git a/src/services/item/plugins/publication/validation/test/validationQueue.test.ts b/src/services/item/plugins/publication/validation/test/validationQueue.spec.ts similarity index 85% rename from src/services/item/plugins/publication/validation/test/validationQueue.test.ts rename to src/services/item/plugins/publication/validation/test/validationQueue.spec.ts index e8d88e8272..b509f030c0 100644 --- a/src/services/item/plugins/publication/validation/test/validationQueue.test.ts +++ b/src/services/item/plugins/publication/validation/test/validationQueue.spec.ts @@ -1,12 +1,13 @@ -import type { FastifyInstance } from 'fastify'; +import Redis from 'ioredis'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; -import build from '../../../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../../../test/app.vitest'; +import { registerDependencies } from '../../../../../../di/container'; import { resolveDependency } from '../../../../../../di/utils'; import { ItemValidationAlreadyExist } from '../errors'; import { ValidationQueue } from '../validationQueue'; describe('Validation Queue Tests', () => { - let app: FastifyInstance; let validationQueue: ValidationQueue; const itemPath = '1b304da5_b342_46e8_a484_1712d5209e43'; const childPath = `${itemPath}.cc24344a_9745_42d6_ae03_17ef686ee824`; @@ -17,17 +18,14 @@ describe('Validation Queue Tests', () => { }; beforeAll(async () => { - ({ app } = await build()); - validationQueue = resolveDependency(ValidationQueue); - }); - - afterAll(() => { - app.close(); + registerDependencies(MOCK_LOGGER); + const redisInstance = resolveDependency(Redis); + validationQueue = new ValidationQueue(redisInstance); }); afterEach(async () => { await resetValidations(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('Adds new validation', async () => { @@ -43,7 +41,7 @@ describe('Validation Queue Tests', () => { await validationQueue.addInProgress(itemPath); }; - expect(addTwice()).rejects.toBeInstanceOf(ItemValidationAlreadyExist); + await expect(addTwice()).rejects.toBeInstanceOf(ItemValidationAlreadyExist); }); it('Adds validation of parent and child should throw', async () => { diff --git a/src/services/item/plugins/recycled/recycled.ws.test.ts b/src/services/item/plugins/recycled/recycled.ws.test.ts index b70ad3a410..ad466158ab 100644 --- a/src/services/item/plugins/recycled/recycled.ws.test.ts +++ b/src/services/item/plugins/recycled/recycled.ws.test.ts @@ -60,7 +60,7 @@ describe('Recycle websocket hooks', () => { const res = await app.inject({ method: HttpMethod.Post, - url: `/items/recycle?id=${item.id}`, + url: `/api/items/recycle?id=${item.id}`, }); expect(res.statusCode).toBe(StatusCodes.ACCEPTED); @@ -104,7 +104,7 @@ describe('Recycle websocket hooks', () => { const res = await app.inject({ method: HttpMethod.Post, - url: `/items/recycle?id=${item.id}`, + url: `/api/items/recycle?id=${item.id}`, }); expect(res.statusCode).toBe(StatusCodes.ACCEPTED); @@ -139,7 +139,7 @@ describe('Recycle websocket hooks', () => { const restore = await app.inject({ method: HttpMethod.Post, - url: `/items/restore?id=${item.id}`, + url: `/api/items/restore?id=${item.id}`, }); expect(restore.statusCode).toBe(StatusCodes.ACCEPTED); diff --git a/src/services/item/ws/ws.test.ts b/src/services/item/ws/ws.test.ts index 55e203887c..a0fb5d3c17 100644 --- a/src/services/item/ws/ws.test.ts +++ b/src/services/item/ws/ws.test.ts @@ -65,7 +65,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/?id=${item.id}`, + url: `/api/items/?id=${item.id}`, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -106,7 +106,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/?id=${item.id}`, + url: `/api/items/?id=${item.id}`, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -140,7 +140,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/move?id=${item.id}`, + url: `/api/items/move?id=${item.id}`, payload: { parentId: newParent.id }, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -185,7 +185,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/move?id=${item.id}`, + url: `/api/items/move?id=${item.id}`, payload: { parentId: newParent.id }, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -220,7 +220,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/copy?id=${item.id}`, + url: `/api/items/copy?id=${item.id}`, payload: { parentId: newParent.id }, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); @@ -264,7 +264,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/copy?id=${item.id}`, + url: `/api/items/copy?id=${item.id}`, payload: { parentId: newParent.id }, }); expect(response.statusCode).toBe(StatusCodes.ACCEPTED); diff --git a/src/services/itemMembership/ws/ws.test.ts b/src/services/itemMembership/ws/ws.test.ts index 6ad6c0e717..63d1af3e79 100644 --- a/src/services/itemMembership/ws/ws.test.ts +++ b/src/services/itemMembership/ws/ws.test.ts @@ -110,7 +110,7 @@ describe('Item websocket hooks', () => { const payload = { accountId: bob.id, permission: PermissionLevel.Read }; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -151,7 +151,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, payload: { permission: PermissionLevel.Admin }, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -196,7 +196,7 @@ describe('Item websocket hooks', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); diff --git a/src/services/websockets/test/test-websocket-client.ts b/src/services/websockets/test/test-websocket-client.ts index 83e5b2c348..d470cb106c 100644 --- a/src/services/websockets/test/test-websocket-client.ts +++ b/src/services/websockets/test/test-websocket-client.ts @@ -10,7 +10,7 @@ export class TestWsClient { private readonly ready: Promise; constructor(address: string) { - this.ws = new WebSocket(address.replace('http', 'ws') + '/ws'); + this.ws = new WebSocket(address.replace('http', 'ws') + '/api/ws'); this.ready = new Promise((resolve) => { this.ws.on('open', () => { resolve(); From 16fa4dadb3c9da24a7c77e1ab5dc5b194b110463 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 11:03:52 +0000 Subject: [PATCH 10/24] fix: make more tests pass --- .../magicLink.controller.register.test.ts | 23 +- ...temWrapper.test.ts => ItemWrapper.spec.ts} | 17 +- .../item.controller.s3.regression.test.ts | 2 +- src/services/item/item.controller.test.ts | 8 +- ...sitory.test.ts => item.repository.spec.ts} | 7 +- ...m.service.test.ts => item.service.spec.ts} | 1 + .../plugins/app/appItem.controller.test.ts | 4 +- .../recycled/recycled.controller.test.ts | 6 +- ...ry.test.ts => recycled.repository.spec.ts} | 2 + .../item/plugins/recycled/recycled.ws.test.ts | 2 +- .../plugins/shortLink/dto/ShortLinkDTO.ts | 20 -- .../plugins/shortLink/shortlink.service.ts | 19 +- .../shortcut/shortcut.controller.test.ts | 14 +- ...rvice.test.ts => shortcut.service.spec.ts} | 9 +- .../plugins/tag/ItemTag.repository.test.ts | 245 ----------------- .../plugins/tag/itemTag.controller.test.ts | 32 +-- .../plugins/tag/itemTag.repository.spec.ts | 248 ++++++++++++++++++ ...ag.repository.ts => itemTag.repository.ts} | 0 ...ervice.test.ts => itemTag.service.spec.ts} | 57 ++-- .../item/plugins/tag/itemTag.service.ts | 2 +- src/services/item/plugins/thumbnail/errors.ts | 11 + .../itemThumbnail.controller.test.ts | 7 +- .../thumbnail/itemThumbnail.controller.ts | 2 +- ....test.ts => itemThumbnail.service.spec.ts} | 19 +- .../plugins/thumbnail/test/fixtures/utils.ts | 2 + .../item/plugins/thumbnail/utils/errors.ts | 20 -- .../item/test/fixtures/items.vitest.ts | 17 ++ .../item/{utils.test.ts => utils.spec.ts} | 2 + src/services/itemLogin/errors.ts | 38 +-- ...st.ts => guestPassword.repository.spec.ts} | 1 + .../itemLogin/interfaces/item-login.ts | 5 - .../itemLogin/itemLogin.controller.test.ts | 11 +- src/services/itemLogin/itemLogin.service.ts | 6 +- ...tRole.test.ts => guestAccountRole.spec.ts} | 1 + .../membership.controller.test.ts | 58 ++-- ....test.ts => membership.repository.spec.ts} | 8 +- src/services/member/test/fixtures/members.ts | 2 + src/services/member/test/register.test.ts | 31 ++- 38 files changed, 505 insertions(+), 454 deletions(-) rename src/services/item/{ItemWrapper.test.ts => ItemWrapper.spec.ts} (86%) rename src/services/item/{item.repository.test.ts => item.repository.spec.ts} (99%) rename src/services/item/{item.service.test.ts => item.service.spec.ts} (99%) rename src/services/item/plugins/recycled/{recycled.repository.test.ts => recycled.repository.spec.ts} (99%) delete mode 100644 src/services/item/plugins/shortLink/dto/ShortLinkDTO.ts rename src/services/item/plugins/shortcut/{shortcut.service.test.ts => shortcut.service.spec.ts} (94%) delete mode 100644 src/services/item/plugins/tag/ItemTag.repository.test.ts create mode 100644 src/services/item/plugins/tag/itemTag.repository.spec.ts rename src/services/item/plugins/tag/{ItemTag.repository.ts => itemTag.repository.ts} (100%) rename src/services/item/plugins/tag/{itemTag.service.test.ts => itemTag.service.spec.ts} (63%) create mode 100644 src/services/item/plugins/thumbnail/errors.ts rename src/services/item/plugins/thumbnail/{itemThumbnail.service.test.ts => itemThumbnail.service.spec.ts} (87%) delete mode 100644 src/services/item/plugins/thumbnail/utils/errors.ts rename src/services/item/{utils.test.ts => utils.spec.ts} (98%) rename src/services/itemLogin/{guestPassword.repository.test.ts => guestPassword.repository.spec.ts} (97%) delete mode 100644 src/services/itemLogin/interfaces/item-login.ts rename src/services/itemLogin/strategies/{guestAccountRole.test.ts => guestAccountRole.spec.ts} (95%) rename src/services/itemMembership/{membership.repository.test.ts => membership.repository.spec.ts} (98%) diff --git a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts index 69a497d2a7..9f9b735a10 100644 --- a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts +++ b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts @@ -6,7 +6,14 @@ import waitForExpect from 'wait-for-expect'; import type { FastifyInstance } from 'fastify'; -import { HttpMethod, MAX_USERNAME_LENGTH, MemberFactory, RecaptchaAction } from '@graasp/sdk'; +import { + AccountType, + DEFAULT_LANG, + HttpMethod, + MAX_USERNAME_LENGTH, + MemberFactory, + RecaptchaAction, +} from '@graasp/sdk'; import build, { MOCK_CAPTCHA, clearDatabase, unmockAuthenticate } from '../../../../../test/app'; import { seedFromJson } from '../../../../../test/mocks/seed'; @@ -17,7 +24,6 @@ import { accountsTable, invitationsTable, itemMembershipsTable } from '../../../ import type { MemberRaw } from '../../../../drizzle/types'; import { MailerService } from '../../../../plugins/mailer/mailer.service'; import { assertIsDefined } from '../../../../utils/assertions'; -import { expectMember } from '../../../member/test/fixtures/members'; jest.mock('node-fetch'); (fetch as jest.MockedFunction).mockImplementation(async () => { @@ -31,6 +37,19 @@ const getMemberByEmail = async (lowercaseEmail: string) => { })) as MemberRaw | undefined; }; +const expectMember = ( + m: MemberRaw | undefined | null, + expectation: Partial> & Pick, +) => { + if (!m) { + throw 'member does not exist'; + } + expect(m.name).toEqual(expectation.name); + expect(m.email).toEqual(expectation.email); + expect(m.type).toEqual(expectation.type ?? AccountType.Individual); + expect(m.extra).toEqual(expectation.extra ?? { lang: DEFAULT_LANG }); +}; + describe('POST /register', () => { let app: FastifyInstance; let mailerService: MailerService; diff --git a/src/services/item/ItemWrapper.test.ts b/src/services/item/ItemWrapper.spec.ts similarity index 86% rename from src/services/item/ItemWrapper.test.ts rename to src/services/item/ItemWrapper.spec.ts index 18dbe8d580..0b4ab2e44b 100644 --- a/src/services/item/ItemWrapper.test.ts +++ b/src/services/item/ItemWrapper.spec.ts @@ -1,4 +1,5 @@ import { describe } from 'node:test'; +import { expect, it, vi } from 'vitest'; import { ItemVisibilityType } from '@graasp/sdk'; @@ -63,9 +64,9 @@ describe('ItemWrapperService', () => { assertIsMemberForTest(actor); const itemVisibilityRepository = { - getForManyItems: jest.fn(), + getForManyItems: vi.fn(), } as unknown as ItemVisibilityRepository; - jest.spyOn(itemVisibilityRepository, 'getForManyItems').mockImplementation(async () => ({ + vi.spyOn(itemVisibilityRepository, 'getForManyItems').mockImplementation(async () => ({ data: { [items[0].id]: [itemVisibilities[0]], [items[1].id]: [itemVisibilities[1]], @@ -78,14 +79,14 @@ describe('ItemWrapperService', () => { errors: [], } as any; const itemMembershipRepository = { - getForManyItems: jest.fn(), + getForManyItems: vi.fn(), } as unknown as ItemMembershipRepository; - jest - .spyOn(itemMembershipRepository, 'getForManyItems') - .mockImplementation(async () => resultOfMemberships); + vi.spyOn(itemMembershipRepository, 'getForManyItems').mockImplementation( + async () => resultOfMemberships, + ); - const itemThumbnailService = { getUrlsByItems: jest.fn() } as unknown as ItemThumbnailService; - jest.spyOn(itemThumbnailService, 'getUrlsByItems').mockImplementation(async () => ({})); + const itemThumbnailService = { getUrlsByItems: vi.fn() } as unknown as ItemThumbnailService; + vi.spyOn(itemThumbnailService, 'getUrlsByItems').mockImplementation(async () => ({})); const packedItems = await new ItemWrapperService( itemVisibilityRepository, diff --git a/src/services/item/item.controller.s3.regression.test.ts b/src/services/item/item.controller.s3.regression.test.ts index 9d78cfc7ed..64061124e7 100644 --- a/src/services/item/item.controller.s3.regression.test.ts +++ b/src/services/item/item.controller.s3.regression.test.ts @@ -80,7 +80,7 @@ describe('Item routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${childOfChild.id}/parents`, + url: `/api/items/${childOfChild.id}/parents`, }); expect(response.json()[1]).not.toHaveProperty('thumbnails'); diff --git a/src/services/item/item.controller.test.ts b/src/services/item/item.controller.test.ts index 74754282a6..5a9d7e6a5e 100644 --- a/src/services/item/item.controller.test.ts +++ b/src/services/item/item.controller.test.ts @@ -40,7 +40,7 @@ describe('Item controller', () => { mockAuthenticate(actor); const response = await app.inject({ method: 'GET', - url: `/items/${rootUUID}/descendants`, + url: `/api/items/${rootUUID}/descendants`, }); expect(response.statusCode).toBe(StatusCodes.OK); const json = response.json(); @@ -71,7 +71,7 @@ describe('Item controller', () => { mockAuthenticate(actor); const response = await app.inject({ method: 'GET', - url: `/items/${rootUUID}/descendants?types=${ItemType.FOLDER}`, + url: `/api/items/${rootUUID}/descendants?types=${ItemType.FOLDER}`, }); expect(response.statusCode).toBe(StatusCodes.OK); const json = response.json(); @@ -100,7 +100,7 @@ describe('Item controller', () => { mockAuthenticate(actor); const response = await app.inject({ method: 'GET', - url: `/items/${rootUUID}/descendants?types=${ItemType.APP}`, + url: `/api/items/${rootUUID}/descendants?types=${ItemType.APP}`, }); expect(response.statusCode).toBe(StatusCodes.OK); const json = response.json(); @@ -126,7 +126,7 @@ describe('Item controller', () => { mockAuthenticate(actor); const response = await app.inject({ method: 'GET', - url: `/items/${rootUUID}/descendants?showHidden=false`, + url: `/api/items/${rootUUID}/descendants?showHidden=false`, }); expect(response.statusCode).toBe(StatusCodes.OK); const json = response.json(); diff --git a/src/services/item/item.repository.test.ts b/src/services/item/item.repository.spec.ts similarity index 99% rename from src/services/item/item.repository.test.ts rename to src/services/item/item.repository.spec.ts index 6687532c08..1eca5f5923 100644 --- a/src/services/item/item.repository.test.ts +++ b/src/services/item/item.repository.spec.ts @@ -3,6 +3,7 @@ import { faker } from '@faker-js/faker/locale/en'; import { eq, inArray } from 'drizzle-orm/sql'; import { v4 } from 'uuid'; +import { describe, expect, it, vi } from 'vitest'; import { ItemType, @@ -31,7 +32,7 @@ import { MemberDTO } from '../member/types'; import { DEFAULT_ORDER } from './constants'; import type { FolderItem } from './discrimination'; import { ItemRepository } from './item.repository'; -import { expectItem, expectManyItems } from './test/fixtures/items'; +import { expectItem, expectManyItems } from './test/fixtures/items.vitest'; const alphabeticalOrder = (a: string, b: string) => a.localeCompare(b); @@ -1303,7 +1304,7 @@ describe('Item Repository', () => { // } = await seedFromJson({ // items: [{}], // }); - // // const updateFn = jest.spyOn(itemRawRepository, 'update'); + // // const updateFn = vi.spyOn(itemRawRepository, 'update'); // await itemRepository.rescaleOrder(db, actor, parentItem); // // expect(updateFn).not.toHaveBeenCalled(); // }); @@ -1398,7 +1399,7 @@ describe('Item Repository', () => { let updateMock; try { await db.transaction(async (tx) => { - updateMock = jest.spyOn(tx, 'update').mockImplementationOnce(() => { + updateMock = vi.spyOn(tx, 'update').mockImplementationOnce(() => { throw new Error('mock update error'); }); diff --git a/src/services/item/item.service.test.ts b/src/services/item/item.service.spec.ts similarity index 99% rename from src/services/item/item.service.test.ts rename to src/services/item/item.service.spec.ts index 52e80e615d..861aec090c 100644 --- a/src/services/item/item.service.test.ts +++ b/src/services/item/item.service.spec.ts @@ -1,5 +1,6 @@ // TODO // import { Readable } from 'node:stream'; +import { expect, it } from 'vitest'; // import { ItemGeolocation, ItemType, ItemVisibilityType } from '@graasp/sdk'; diff --git a/src/services/item/plugins/app/appItem.controller.test.ts b/src/services/item/plugins/app/appItem.controller.test.ts index dd20516dfa..f27d822098 100644 --- a/src/services/item/plugins/app/appItem.controller.test.ts +++ b/src/services/item/plugins/app/appItem.controller.test.ts @@ -141,7 +141,7 @@ describe('App Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/apps/${item.id}`, + url: `/api/items/apps/${item.id}`, payload: { name: 'new name' }, }); @@ -174,7 +174,7 @@ describe('App Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/apps/${item.id}`, + url: `/api/items/apps/${item.id}`, payload, }); diff --git a/src/services/item/plugins/recycled/recycled.controller.test.ts b/src/services/item/plugins/recycled/recycled.controller.test.ts index 8987cc2db4..93d492df4c 100644 --- a/src/services/item/plugins/recycled/recycled.controller.test.ts +++ b/src/services/item/plugins/recycled/recycled.controller.test.ts @@ -240,7 +240,7 @@ describe('Recycle Bin Tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/recycle?id=${item.id}`, + url: `/api/items/recycle?id=${item.id}`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -551,7 +551,7 @@ describe('Recycle Bin Tests', () => { const recycle = await app.inject({ method: HttpMethod.Post, - url: `/items/recycle?id=${parentItem.id}`, + url: `/api/items/recycle?id=${parentItem.id}`, }); expect(recycle.statusCode).toBe(StatusCodes.ACCEPTED); @@ -570,7 +570,7 @@ describe('Recycle Bin Tests', () => { const restore = await app.inject({ method: HttpMethod.Post, - url: `/items/restore?id=${parentItem.id}`, + url: `/api/items/restore?id=${parentItem.id}`, }); expect(restore.statusCode).toBe(StatusCodes.ACCEPTED); diff --git a/src/services/item/plugins/recycled/recycled.repository.test.ts b/src/services/item/plugins/recycled/recycled.repository.spec.ts similarity index 99% rename from src/services/item/plugins/recycled/recycled.repository.test.ts rename to src/services/item/plugins/recycled/recycled.repository.spec.ts index 6aac60cb53..ca8c1dc6a7 100644 --- a/src/services/item/plugins/recycled/recycled.repository.test.ts +++ b/src/services/item/plugins/recycled/recycled.repository.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { PermissionLevel } from '@graasp/sdk'; import { seedFromJson } from '../../../../../test/mocks/seed'; diff --git a/src/services/item/plugins/recycled/recycled.ws.test.ts b/src/services/item/plugins/recycled/recycled.ws.test.ts index ad466158ab..3e805a7d0f 100644 --- a/src/services/item/plugins/recycled/recycled.ws.test.ts +++ b/src/services/item/plugins/recycled/recycled.ws.test.ts @@ -32,8 +32,8 @@ describe('Recycle websocket hooks', () => { afterAll(async () => { await clearDatabase(db); - app.close(); ws.close(); + await app.close(); }); afterEach(() => { diff --git a/src/services/item/plugins/shortLink/dto/ShortLinkDTO.ts b/src/services/item/plugins/shortLink/dto/ShortLinkDTO.ts deleted file mode 100644 index 7738618d37..0000000000 --- a/src/services/item/plugins/shortLink/dto/ShortLinkDTO.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ShortLink } from '@graasp/sdk'; - -import type { ShortLinkRaw } from '../../../../../drizzle/types'; - -export class ShortLinkDTO { - /** - * Strip sensible data - */ - public static from({ - alias, - platform, - itemId, - }: Pick): ShortLink { - return { - alias, - platform, - itemId, - }; - } -} diff --git a/src/services/item/plugins/shortLink/shortlink.service.ts b/src/services/item/plugins/shortLink/shortlink.service.ts index 8512fadd12..040d69ba7c 100644 --- a/src/services/item/plugins/shortLink/shortlink.service.ts +++ b/src/services/item/plugins/shortLink/shortlink.service.ts @@ -12,19 +12,36 @@ import { import { SHORT_LINK_BASE_URL } from '../../../../config/hosts'; import { type DBConnection } from '../../../../drizzle/db'; +import { ShortLinkRaw } from '../../../../drizzle/types'; import type { AuthenticatedUser, MinimalMember } from '../../../../types'; import { ITEMS_ROUTE_PREFIX } from '../../../../utils/config'; import { UnauthorizedMember } from '../../../../utils/errors'; import { AuthorizedItemService } from '../../../authorizedItem.service'; import { ItemPublishedNotFound } from '../publication/published/errors'; import { ItemPublishedService } from '../publication/published/itemPublished.service'; -import { ShortLinkDTO } from './dto/ShortLinkDTO'; import { ShortLinkRepository } from './shortlink.repository'; export const SHORT_LINKS_ROUTE_PREFIX = '/short-links'; export const SHORT_LINKS_LIST_ROUTE = '/list'; export const SHORT_LINKS_FULL_PREFIX = `${ITEMS_ROUTE_PREFIX}${SHORT_LINKS_ROUTE_PREFIX}`; +class ShortLinkDTO { + /** + * Strip sensible data + */ + public static from({ + alias, + platform, + itemId, + }: Pick): ShortLink { + return { + alias, + platform, + itemId, + }; + } +} + @singleton() export class ShortLinkService { private authorizedItemService: AuthorizedItemService; diff --git a/src/services/item/plugins/shortcut/shortcut.controller.test.ts b/src/services/item/plugins/shortcut/shortcut.controller.test.ts index 251955e1ff..2a4a1482a6 100644 --- a/src/services/item/plugins/shortcut/shortcut.controller.test.ts +++ b/src/services/item/plugins/shortcut/shortcut.controller.test.ts @@ -158,7 +158,7 @@ describe('Shortcut routes tests', () => { const parentId = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/shortcuts?parentId=${parentId}`, + url: `/api/items/shortcuts?parentId=${parentId}`, payload, }); @@ -173,7 +173,7 @@ describe('Shortcut routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/shortcuts`, + url: `/api/items/shortcuts`, payload: { target: 'target' }, }); @@ -191,7 +191,7 @@ describe('Shortcut routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/shortcuts/${item.id}`, + url: `/api/items/shortcuts/${item.id}`, payload: { name: 'new name' }, }); @@ -222,7 +222,7 @@ describe('Shortcut routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/shortcuts/${item.id}`, + url: `/api/items/shortcuts/${item.id}`, payload, }); @@ -259,7 +259,7 @@ describe('Shortcut routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/shortcuts/${item.id}`, + url: `/api/items/shortcuts/${item.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -312,7 +312,7 @@ describe('Shortcut routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/shortcuts/${shortcut.id}`, + url: `/api/items/shortcuts/${shortcut.id}`, payload, }); @@ -343,7 +343,7 @@ describe('Shortcut routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/shortcuts/${shortcut.id}`, + url: `/api/items/shortcuts/${shortcut.id}`, payload, }); diff --git a/src/services/item/plugins/shortcut/shortcut.service.test.ts b/src/services/item/plugins/shortcut/shortcut.service.spec.ts similarity index 94% rename from src/services/item/plugins/shortcut/shortcut.service.test.ts rename to src/services/item/plugins/shortcut/shortcut.service.spec.ts index d1d07ae86f..f87f404cf8 100644 --- a/src/services/item/plugins/shortcut/shortcut.service.test.ts +++ b/src/services/item/plugins/shortcut/shortcut.service.spec.ts @@ -1,8 +1,9 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ItemType } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { ItemFactory } from '../../../../../test/factories/item.factory'; import { MemberFactory } from '../../../../../test/factories/member.factory'; import { db } from '../../../../drizzle/db'; @@ -45,7 +46,7 @@ const shortcutService = new ShortcutItemService( describe('Shortcut Service', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('postWithOptions', () => { @@ -107,7 +108,7 @@ describe('Shortcut Service', () => { // }); // }); it('throw if target does not exist', async () => { - jest.spyOn(AuthorizedItemService.prototype, 'getItemById').mockRejectedValue(new Error()); + vi.spyOn(AuthorizedItemService.prototype, 'getItemById').mockRejectedValue(new Error()); await expect(() => shortcutService.postWithOptions(db, MOCK_MEMBER, { @@ -138,7 +139,7 @@ describe('Shortcut Service', () => { // }); it('Cannot update not found item given id', async () => { - jest.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { + vi.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { throw new Error(); }); diff --git a/src/services/item/plugins/tag/ItemTag.repository.test.ts b/src/services/item/plugins/tag/ItemTag.repository.test.ts deleted file mode 100644 index b1f14fb183..0000000000 --- a/src/services/item/plugins/tag/ItemTag.repository.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { and, eq } from 'drizzle-orm/sql'; -import { v4 } from 'uuid'; - -import { FolderItemFactory, TagCategory, type TagCategoryType } from '@graasp/sdk'; - -import { seedFromJson } from '../../../../../test/mocks/seed'; -import { db } from '../../../../drizzle/db'; -import { itemTagsTable, itemsRawTable, tagsTable } from '../../../../drizzle/schema'; -import type { ItemInsertDTO } from '../../../../drizzle/types'; -import { IllegalArgumentException } from '../../../../repositories/errors'; -import { assertIsDefined } from '../../../../utils/assertions'; -import { ItemTagRepository } from './ItemTag.repository'; -import { TAG_COUNT_MAX_RESULTS } from './constants'; -import { ItemTagAlreadyExists } from './errors'; - -// tag are global in whole database and can collide -jest.retryTimes(3, { logErrorsBeforeRetry: true }); - -const repository = new ItemTagRepository(); - -async function saveTag(t: { name?: string; category: TagCategoryType }) { - const { - tags: [tag], - } = await seedFromJson({ actor: null, tags: [t] }); - return tag; -} - -async function saveItem(item: ItemInsertDTO) { - const res = await db.insert(itemsRawTable).values(item).returning(); - const newItem = res[0]; - assertIsDefined(newItem); - return newItem; -} - -async function saveItemTag(args: { itemId: string; tagId: string }) { - await db.insert(itemTagsTable).values(args); -} - -describe('ItemTag Repository', () => { - afterAll(async () => { - // less chaos if tag is wiped out - await db.delete(tagsTable); - }); - - describe('getCountBy', () => { - it('throw for empty search', async () => { - await expect(() => - repository.getCountBy(db, { - search: '', - category: TagCategory.Discipline, - }), - ).rejects.toBeInstanceOf(IllegalArgumentException); - }); - it('return empty count for empty set', async () => { - expect( - await repository.getCountBy(db, { - search: 'search', - category: TagCategory.Discipline, - }), - ).toHaveLength(0); - }); - it('get count for tags given search and category', async () => { - const category = TagCategory.Discipline; - const tag = await saveTag({ category }); - const tag1 = await saveTag({ name: tag.name + ' second', category }); - - const item = await saveItem(FolderItemFactory({ creator: null })); - const item1 = await saveItem(FolderItemFactory({ creator: null })); - - // add tag once, tag1 twice - await saveItemTag({ itemId: item.id, tagId: tag.id }); - await saveItemTag({ itemId: item1.id, tagId: tag1.id }); - await saveItemTag({ itemId: item.id, tagId: tag1.id }); - - // noise not in same category, but can match search - const tag2 = await saveTag({ name: tag.name + ' third', category: TagCategory.Level }); - await saveItemTag({ itemId: item1.id, tagId: tag2.id }); - - const tags = await repository.getCountBy(db, { - search: tag.name, - category, - }); - expect(tags).toHaveLength(2); - expect(tags.find(({ name }) => tag.name === name)!.count).toEqual(1); - expect(tags.find(({ name }) => tag1.name === name)!.count).toEqual(2); - }); - - it(`get max ${TAG_COUNT_MAX_RESULTS} counts for tags`, async () => { - const category = TagCategory.Discipline; - // create more tags and association than limit - const promises = Array.from({ length: TAG_COUNT_MAX_RESULTS + 13 }, async (_, idx) => { - const tag = await saveTag({ name: 'tag' + idx, category }); - for (let i = 0; i < idx; i++) { - const item = await saveItem(FolderItemFactory({ creator: null })); - await saveItemTag({ itemId: item.id, tagId: tag.id }); - } - return tag; - }); - const tags = await Promise.all(promises); - - const result = await repository.getCountBy(db, { - search: 'tag', - category, - }); - // should have only max number of counts - expect(result).toHaveLength(TAG_COUNT_MAX_RESULTS); - - // last tags in the array are the most used - for (const t of tags.slice(-TAG_COUNT_MAX_RESULTS)) { - const res = result.find(({ name }) => t.name === name); - expect(res).toBeDefined(); - expect(res?.count).toBeGreaterThan(1); - } - }); - }); - - describe('getForItem', () => { - it('throw for invalid item id', async () => { - await expect(() => repository.getByItemId(db, undefined!)).rejects.toBeInstanceOf( - IllegalArgumentException, - ); - }); - it('return empty tags for non-existing item', async () => { - expect(await repository.getByItemId(db, v4())).toHaveLength(0); - }); - it('get empty tags for item', async () => { - const item = await saveItem(FolderItemFactory({ creator: null })); - - const tags = await repository.getByItemId(db, item.id); - expect(tags).toHaveLength(0); - }); - - it('get tags for item', async () => { - // save item with tags - const item = await saveItem(FolderItemFactory({ creator: null })); - const tag1 = await saveTag({ category: TagCategory.Discipline }); - const tag2 = await saveTag({ category: TagCategory.Discipline }); - await saveItemTag({ itemId: item.id, tagId: tag1.id }); - await saveItemTag({ itemId: item.id, tagId: tag2.id }); - - // noise - const anotherTag = await saveTag({ category: TagCategory.Discipline }); - const anotherItem = await saveItem(FolderItemFactory({ creator: null })); - await saveItemTag({ itemId: anotherItem.id, tagId: anotherTag.id }); - - const tags = await repository.getByItemId(db, item.id); - expect(tags).toEqual(expect.arrayContaining([tag1, tag2])); - }); - }); - - describe('createForItem', () => { - it('throw for invalid item id', async () => { - const tag = await saveTag({ category: TagCategory.Discipline }); - await expect(() => repository.create(db, undefined!, tag.id)).rejects.toThrow(); - }); - it('throw for invalid tag id', async () => { - const item = await saveItem(FolderItemFactory({ creator: null })); - - await expect(() => repository.create(db, item.id, undefined!)).rejects.toThrow(); - }); - it('throw for non-existing item', async () => { - const tag = await saveTag({ category: TagCategory.Discipline }); - await expect(() => repository.create(db, v4(), tag.id)).rejects.toThrow(); - }); - it('throw for non-existing tag', async () => { - const item = await saveItem(FolderItemFactory({ creator: null })); - - await expect(() => repository.create(db, item.id, v4())).rejects.toThrow(); - }); - it('create tag for item', async () => { - const tag = await saveTag({ category: TagCategory.Discipline }); - const item = await saveItem(FolderItemFactory({ creator: null })); - - await repository.create(db, item.id, tag.id); - - const result = await db.query.itemTagsTable.findFirst({ - where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), - }); - expect(result).toBeDefined(); - }); - it('throw if tag already exists for item', async () => { - const tag = await saveTag({ category: TagCategory.Discipline }); - const item = await saveItem(FolderItemFactory({ creator: null })); - await saveItemTag({ tagId: tag.id, itemId: item.id }); - - await expect(async () => await repository.create(db, item.id, tag.id)).rejects.toThrow( - new ItemTagAlreadyExists({ itemId: item.id, tagId: tag.id }), - ); - }); - }); - describe('delete', () => { - it('throw for invalid item id', async () => { - const tag = await saveTag({ category: TagCategory.Discipline }); - await expect(() => repository.delete(db, undefined!, tag.id)).rejects.toThrow(); - }); - it('throw for invalid tag id', async () => { - const item = await saveItem(FolderItemFactory({ creator: null })); - - await expect(() => repository.delete(db, item.id, undefined!)).rejects.toThrow(); - }); - it('does not throw for non-existing item', async () => { - const tag = await saveTag({ category: TagCategory.Level }); - expect(await repository.delete(db, v4(), tag.id)).toBeUndefined(); - }); - it('does not throw for non-existing tag', async () => { - const item = await saveItem(FolderItemFactory({ creator: null })); - - expect(await repository.delete(db, item.id, v4())).toBeUndefined(); - }); - it('does not throw if tag is not associated with item', async () => { - const tag = await saveTag({ category: TagCategory.Level }); - const item = await saveItem(FolderItemFactory({ creator: null })); - - expect(await repository.delete(db, item.id, tag.id)).toBeUndefined(); - }); - it('delete tag for item', async () => { - const tag = await saveTag({ category: TagCategory.ResourceType }); - const item = await saveItem(FolderItemFactory({ creator: null })); - await saveItemTag({ tagId: tag.id, itemId: item.id }); - - // noise - const tag1 = await saveTag({ category: TagCategory.Discipline }); - await saveItemTag({ tagId: tag1.id, itemId: item.id }); - const preResult = await db.query.itemTagsTable.findFirst({ - where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), - }); - expect(preResult).toBeDefined(); - - await repository.delete(db, item.id, tag.id); - - const result = await db.query.itemTagsTable.findFirst({ - where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), - }); - expect(result).toBeUndefined(); - - // noise still exists - const result1 = await db.query.itemTagsTable.findFirst({ - where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag1.id)), - with: { tag: true }, - }); - expect(result1).toBeDefined(); - expect(result1?.tag?.id).toEqual(tag1.id); - }); - }); -}); diff --git a/src/services/item/plugins/tag/itemTag.controller.test.ts b/src/services/item/plugins/tag/itemTag.controller.test.ts index 1881948125..b7824f749a 100644 --- a/src/services/item/plugins/tag/itemTag.controller.test.ts +++ b/src/services/item/plugins/tag/itemTag.controller.test.ts @@ -43,7 +43,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/invalid/tags`, + url: `/api/items/invalid/tags`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); }); @@ -64,7 +64,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.json()).toEqual(expect.arrayContaining(tags)); @@ -83,7 +83,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -109,7 +109,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -128,7 +128,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -147,7 +147,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -159,7 +159,7 @@ describe('Item Tag Endpoints', () => { it('Throw for invalid item id', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/invalid/tags`, + url: `/api/items/invalid/tags`, payload: { name: 'name', category: TagCategory.Discipline }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -173,7 +173,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, payload: { name: 'name', category: TagCategory.Discipline }, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -198,7 +198,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, payload: { name: 'name', category: TagCategory.Discipline }, }); @@ -222,7 +222,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, payload: { name: 'name', category: 'wrong' }, }); @@ -245,7 +245,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/tags`, + url: `/api/items/${item.id}/tags`, payload: { name: 'name', category: TagCategory.Discipline }, }); @@ -258,14 +258,14 @@ describe('Item Tag Endpoints', () => { it('Throw for invalid item id', async () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/invalid/tags/${v4()}`, + url: `/api/items/invalid/tags/${v4()}`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); }); it('Throw for invalid tag id', async () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${v4()}/tags/invalid}`, + url: `/api/items/${v4()}/tags/invalid}`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); }); @@ -275,7 +275,7 @@ describe('Item Tag Endpoints', () => { it('Throw if signed out', async () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${v4()}/tags/${v4()}`, + url: `/api/items/${v4()}/tags/${v4()}`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); @@ -300,7 +300,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/tags/${tag.id}`, + url: `/api/items/${item.id}/tags/${tag.id}`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -324,7 +324,7 @@ describe('Item Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/tags/${tag.id}`, + url: `/api/items/${item.id}/tags/${tag.id}`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); diff --git a/src/services/item/plugins/tag/itemTag.repository.spec.ts b/src/services/item/plugins/tag/itemTag.repository.spec.ts new file mode 100644 index 0000000000..eb0bc94499 --- /dev/null +++ b/src/services/item/plugins/tag/itemTag.repository.spec.ts @@ -0,0 +1,248 @@ +import { and, eq } from 'drizzle-orm/sql'; +import { v4 } from 'uuid'; +import { afterAll, describe, expect, it } from 'vitest'; + +import { FolderItemFactory, TagCategory, type TagCategoryType } from '@graasp/sdk'; + +import { seedFromJson } from '../../../../../test/mocks/seed'; +import { db } from '../../../../drizzle/db'; +import { itemTagsTable, itemsRawTable, tagsTable } from '../../../../drizzle/schema'; +import type { ItemInsertDTO } from '../../../../drizzle/types'; +import { IllegalArgumentException } from '../../../../repositories/errors'; +import { assertIsDefined } from '../../../../utils/assertions'; +import { TAG_COUNT_MAX_RESULTS } from './constants'; +import { ItemTagAlreadyExists } from './errors'; +import { ItemTagRepository } from './itemTag.repository'; + +const repository = new ItemTagRepository(); + +async function saveTag(t: { name?: string; category: TagCategoryType }) { + const { + tags: [tag], + } = await seedFromJson({ actor: null, tags: [t] }); + return tag; +} + +async function saveItem(item: ItemInsertDTO) { + const res = await db.insert(itemsRawTable).values(item).returning(); + const newItem = res[0]; + assertIsDefined(newItem); + return newItem; +} + +async function saveItemTag(args: { itemId: string; tagId: string }) { + await db.insert(itemTagsTable).values(args); +} + +describe( + 'ItemTag Repository', + () => { + afterAll(async () => { + // less chaos if tag is wiped out + await db.delete(tagsTable); + }); + + describe('getCountBy', () => { + it('throw for empty search', async () => { + await expect(() => + repository.getCountBy(db, { + search: '', + category: TagCategory.Discipline, + }), + ).rejects.toBeInstanceOf(IllegalArgumentException); + }); + it('return empty count for empty set', async () => { + expect( + await repository.getCountBy(db, { + search: 'search', + category: TagCategory.Discipline, + }), + ).toHaveLength(0); + }); + it('get count for tags given search and category', async () => { + const category = TagCategory.Discipline; + const tag = await saveTag({ category }); + const tag1 = await saveTag({ name: tag.name + ' second', category }); + + const item = await saveItem(FolderItemFactory({ creator: null })); + const item1 = await saveItem(FolderItemFactory({ creator: null })); + + // add tag once, tag1 twice + await saveItemTag({ itemId: item.id, tagId: tag.id }); + await saveItemTag({ itemId: item1.id, tagId: tag1.id }); + await saveItemTag({ itemId: item.id, tagId: tag1.id }); + + // noise not in same category, but can match search + const tag2 = await saveTag({ name: tag.name + ' third', category: TagCategory.Level }); + await saveItemTag({ itemId: item1.id, tagId: tag2.id }); + + const tags = await repository.getCountBy(db, { + search: tag.name, + category, + }); + expect(tags).toHaveLength(2); + expect(tags.find(({ name }) => tag.name === name)!.count).toEqual(1); + expect(tags.find(({ name }) => tag1.name === name)!.count).toEqual(2); + }); + + it(`get max ${TAG_COUNT_MAX_RESULTS} counts for tags`, async () => { + const category = TagCategory.Discipline; + // create more tags and association than limit + const promises = Array.from({ length: TAG_COUNT_MAX_RESULTS + 13 }, async (_, idx) => { + const tag = await saveTag({ name: 'tag' + idx, category }); + for (let i = 0; i < idx; i++) { + const item = await saveItem(FolderItemFactory({ creator: null })); + await saveItemTag({ itemId: item.id, tagId: tag.id }); + } + return tag; + }); + const tags = await Promise.all(promises); + + const result = await repository.getCountBy(db, { + search: 'tag', + category, + }); + // should have only max number of counts + expect(result).toHaveLength(TAG_COUNT_MAX_RESULTS); + + // last tags in the array are the most used + for (const t of tags.slice(-TAG_COUNT_MAX_RESULTS)) { + const res = result.find(({ name }) => t.name === name); + expect(res).toBeDefined(); + expect(res?.count).toBeGreaterThan(1); + } + }); + }); + + describe('getForItem', () => { + it('throw for invalid item id', async () => { + await expect(() => repository.getByItemId(db, undefined!)).rejects.toBeInstanceOf( + IllegalArgumentException, + ); + }); + it('return empty tags for non-existing item', async () => { + expect(await repository.getByItemId(db, v4())).toHaveLength(0); + }); + it('get empty tags for item', async () => { + const item = await saveItem(FolderItemFactory({ creator: null })); + + const tags = await repository.getByItemId(db, item.id); + expect(tags).toHaveLength(0); + }); + + it('get tags for item', async () => { + // save item with tags + const item = await saveItem(FolderItemFactory({ creator: null })); + const tag1 = await saveTag({ category: TagCategory.Discipline }); + const tag2 = await saveTag({ category: TagCategory.Discipline }); + await saveItemTag({ itemId: item.id, tagId: tag1.id }); + await saveItemTag({ itemId: item.id, tagId: tag2.id }); + + // noise + const anotherTag = await saveTag({ category: TagCategory.Discipline }); + const anotherItem = await saveItem(FolderItemFactory({ creator: null })); + await saveItemTag({ itemId: anotherItem.id, tagId: anotherTag.id }); + + const tags = await repository.getByItemId(db, item.id); + expect(tags).toEqual(expect.arrayContaining([tag1, tag2])); + }); + }); + + describe('createForItem', () => { + it('throw for invalid item id', async () => { + const tag = await saveTag({ category: TagCategory.Discipline }); + await expect(() => repository.create(db, undefined!, tag.id)).rejects.toThrow(); + }); + it('throw for invalid tag id', async () => { + const item = await saveItem(FolderItemFactory({ creator: null })); + + await expect(() => repository.create(db, item.id, undefined!)).rejects.toThrow(); + }); + it('throw for non-existing item', async () => { + const tag = await saveTag({ category: TagCategory.Discipline }); + await expect(() => repository.create(db, v4(), tag.id)).rejects.toThrow(); + }); + it('throw for non-existing tag', async () => { + const item = await saveItem(FolderItemFactory({ creator: null })); + + await expect(() => repository.create(db, item.id, v4())).rejects.toThrow(); + }); + it('create tag for item', async () => { + const tag = await saveTag({ category: TagCategory.Discipline }); + const item = await saveItem(FolderItemFactory({ creator: null })); + + await repository.create(db, item.id, tag.id); + + const result = await db.query.itemTagsTable.findFirst({ + where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), + }); + expect(result).toBeDefined(); + }); + it('throw if tag already exists for item', async () => { + const tag = await saveTag({ category: TagCategory.Discipline }); + const item = await saveItem(FolderItemFactory({ creator: null })); + await saveItemTag({ tagId: tag.id, itemId: item.id }); + + await expect(async () => await repository.create(db, item.id, tag.id)).rejects.toThrow( + new ItemTagAlreadyExists({ itemId: item.id, tagId: tag.id }), + ); + }); + }); + describe('delete', () => { + it('throw for invalid item id', async () => { + const tag = await saveTag({ category: TagCategory.Discipline }); + await expect(() => repository.delete(db, undefined!, tag.id)).rejects.toThrow(); + }); + it('throw for invalid tag id', async () => { + const item = await saveItem(FolderItemFactory({ creator: null })); + + await expect(() => repository.delete(db, item.id, undefined!)).rejects.toThrow(); + }); + it('does not throw for non-existing item', async () => { + const tag = await saveTag({ category: TagCategory.Level }); + expect(await repository.delete(db, v4(), tag.id)).toBeUndefined(); + }); + it('does not throw for non-existing tag', async () => { + const item = await saveItem(FolderItemFactory({ creator: null })); + + expect(await repository.delete(db, item.id, v4())).toBeUndefined(); + }); + it('does not throw if tag is not associated with item', async () => { + const tag = await saveTag({ category: TagCategory.Level }); + const item = await saveItem(FolderItemFactory({ creator: null })); + + expect(await repository.delete(db, item.id, tag.id)).toBeUndefined(); + }); + it('delete tag for item', async () => { + const tag = await saveTag({ category: TagCategory.ResourceType }); + const item = await saveItem(FolderItemFactory({ creator: null })); + await saveItemTag({ tagId: tag.id, itemId: item.id }); + + // noise + const tag1 = await saveTag({ category: TagCategory.Discipline }); + await saveItemTag({ tagId: tag1.id, itemId: item.id }); + const preResult = await db.query.itemTagsTable.findFirst({ + where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), + }); + expect(preResult).toBeDefined(); + + await repository.delete(db, item.id, tag.id); + + const result = await db.query.itemTagsTable.findFirst({ + where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag.id)), + }); + expect(result).toBeUndefined(); + + // noise still exists + const result1 = await db.query.itemTagsTable.findFirst({ + where: and(eq(itemTagsTable.itemId, item.id), eq(itemTagsTable.tagId, tag1.id)), + with: { tag: true }, + }); + expect(result1).toBeDefined(); + expect(result1?.tag?.id).toEqual(tag1.id); + }); + }); + }, + // tag are global in whole database and can collide + { retry: 3 }, +); diff --git a/src/services/item/plugins/tag/ItemTag.repository.ts b/src/services/item/plugins/tag/itemTag.repository.ts similarity index 100% rename from src/services/item/plugins/tag/ItemTag.repository.ts rename to src/services/item/plugins/tag/itemTag.repository.ts diff --git a/src/services/item/plugins/tag/itemTag.service.test.ts b/src/services/item/plugins/tag/itemTag.service.spec.ts similarity index 63% rename from src/services/item/plugins/tag/itemTag.service.test.ts rename to src/services/item/plugins/tag/itemTag.service.spec.ts index b1b14c36a5..5d89889c78 100644 --- a/src/services/item/plugins/tag/itemTag.service.test.ts +++ b/src/services/item/plugins/tag/itemTag.service.spec.ts @@ -1,5 +1,6 @@ import { EnqueuedTask } from 'meilisearch'; import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { TagFactory } from '@graasp/sdk'; @@ -13,22 +14,22 @@ import { ItemRepository } from '../../item.repository'; import { ItemVisibilityRepository } from '../itemVisibility/itemVisibility.repository'; import { ItemPublishedRepository } from '../publication/published/itemPublished.repository'; import { MeiliSearchWrapper } from '../publication/published/plugins/search/meilisearch'; -import { ItemTagRepository } from './ItemTag.repository'; +import { ItemTagRepository } from './itemTag.repository'; import { ItemTagService } from './itemTag.service'; const meilisearchWrapper = { - indexOne: jest.fn() as MeiliSearchWrapper['indexOne'], + indexOne: vi.fn() as MeiliSearchWrapper['indexOne'], } as MeiliSearchWrapper; const itemTagRepository = { - create: jest.fn() as ItemTagRepository['create'], - delete: jest.fn() as ItemTagRepository['delete'], + create: vi.fn() as ItemTagRepository['create'], + delete: vi.fn() as ItemTagRepository['delete'], } as ItemTagRepository; const tagRepository = { - addOneIfDoesNotExist: jest.fn(async () => TagFactory() as TagRaw), + addOneIfDoesNotExist: vi.fn(async () => TagFactory() as TagRaw), } as unknown as TagRepository; const itemPublishedRepository = { - getForItem: jest.fn() as ItemPublishedRepository['getForItem'], + getForItem: vi.fn() as ItemPublishedRepository['getForItem'], } as ItemPublishedRepository; const authorizedItemService = new AuthorizedItemService( new ItemMembershipRepository(), @@ -46,15 +47,15 @@ const itemTagService = new ItemTagService( describe('Item Tag create', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('does not index item if it is not published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); - jest.spyOn(itemTagRepository, 'create').mockResolvedValue(); - const indexOneMock = jest + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); + vi.spyOn(itemTagRepository, 'create').mockResolvedValue(); + const indexOneMock = vi .spyOn(meilisearchWrapper, 'indexOne') .mockResolvedValue({} as unknown as EnqueuedTask); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); await itemTagService.create(db, {} as MinimalMember, v4(), TagFactory()); @@ -62,14 +63,14 @@ describe('Item Tag create', () => { }); it('index item if it is published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); - jest.spyOn(itemTagRepository, 'create').mockResolvedValue(); - const indexOneMock = jest + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); + vi.spyOn(itemTagRepository, 'create').mockResolvedValue(); + const indexOneMock = vi .spyOn(meilisearchWrapper, 'indexOne') .mockResolvedValue({} as unknown as EnqueuedTask); - jest - .spyOn(itemPublishedRepository, 'getForItem') - .mockResolvedValue({} as ItemPublishedWithItemWithCreator); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue( + {} as ItemPublishedWithItemWithCreator, + ); await itemTagService.create(db, {} as MinimalMember, v4(), TagFactory()); @@ -79,15 +80,15 @@ describe('Item Tag create', () => { describe('Item Tag delete', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('does not index item if it is not published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); - jest.spyOn(itemTagRepository, 'delete').mockResolvedValue(); - const indexOneMock = jest + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); + vi.spyOn(itemTagRepository, 'delete').mockResolvedValue(); + const indexOneMock = vi .spyOn(meilisearchWrapper, 'indexOne') .mockResolvedValue({} as unknown as EnqueuedTask); - jest.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue(null); await itemTagService.delete(db, {} as MinimalMember, v4(), v4()); @@ -95,14 +96,14 @@ describe('Item Tag delete', () => { }); it('index item if it is published', async () => { - jest.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); - jest.spyOn(itemTagRepository, 'delete').mockResolvedValue(); - const indexOneMock = jest + vi.spyOn(authorizedItemService, 'getItemById').mockResolvedValue({} as ItemRaw); + vi.spyOn(itemTagRepository, 'delete').mockResolvedValue(); + const indexOneMock = vi .spyOn(meilisearchWrapper, 'indexOne') .mockResolvedValue({} as unknown as EnqueuedTask); - jest - .spyOn(itemPublishedRepository, 'getForItem') - .mockResolvedValue({} as ItemPublishedWithItemWithCreator); + vi.spyOn(itemPublishedRepository, 'getForItem').mockResolvedValue( + {} as ItemPublishedWithItemWithCreator, + ); await itemTagService.delete(db, {} as MinimalMember, v4(), v4()); diff --git a/src/services/item/plugins/tag/itemTag.service.ts b/src/services/item/plugins/tag/itemTag.service.ts index 602106a4af..e355363004 100644 --- a/src/services/item/plugins/tag/itemTag.service.ts +++ b/src/services/item/plugins/tag/itemTag.service.ts @@ -8,7 +8,7 @@ import { AuthorizedItemService } from '../../../authorizedItem.service'; import { TagRepository } from '../../../tag/tag.repository'; import { ItemPublishedRepository } from '../publication/published/itemPublished.repository'; import { MeiliSearchWrapper } from '../publication/published/plugins/search/meilisearch'; -import { ItemTagRepository } from './ItemTag.repository'; +import { ItemTagRepository } from './itemTag.repository'; @singleton() export class ItemTagService { diff --git a/src/services/item/plugins/thumbnail/errors.ts b/src/services/item/plugins/thumbnail/errors.ts new file mode 100644 index 0000000000..b75170e159 --- /dev/null +++ b/src/services/item/plugins/thumbnail/errors.ts @@ -0,0 +1,11 @@ +import { StatusCodes } from 'http-status-codes'; + +import { createError } from '@fastify/error'; + +import { FAILURE_MESSAGES } from '@graasp/sdk'; + +export const UploadFileNotImageError = createError( + 'GPTERR001', + FAILURE_MESSAGES.FILE_IS_NOT_IMAGE, + StatusCodes.BAD_REQUEST, +); diff --git a/src/services/item/plugins/thumbnail/itemThumbnail.controller.test.ts b/src/services/item/plugins/thumbnail/itemThumbnail.controller.test.ts index 2f958d7074..48e2988361 100644 --- a/src/services/item/plugins/thumbnail/itemThumbnail.controller.test.ts +++ b/src/services/item/plugins/thumbnail/itemThumbnail.controller.test.ts @@ -20,7 +20,7 @@ import { itemsRawTable } from '../../../../drizzle/schema'; import { assertIsDefined } from '../../../../utils/assertions'; import { ITEMS_ROUTE_PREFIX, THUMBNAILS_ROUTE_PREFIX } from '../../../../utils/config'; import { MemberCannotAccess } from '../../../../utils/errors'; -import { UploadFileNotImageError } from './utils/errors'; +import { UploadFileNotImageError } from './errors'; const filepath = path.resolve(__dirname, './test/fixtures/image.png'); const textPath = path.resolve(__dirname, './test/fixtures/emptyFile'); @@ -295,7 +295,10 @@ describe('Thumbnail Plugin Tests', () => { }); expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST); - expect(res.json()).toEqual(new UploadFileNotImageError()); + const response = await res.json(); + const expectedRes = new UploadFileNotImageError(); + expect(response.message).toEqual(expectedRes.message); + expect(response.code).toEqual(expectedRes.code); const savedItem = await db.query.itemsRawTable.findFirst({ where: eq(itemsRawTable.id, item.id), }); diff --git a/src/services/item/plugins/thumbnail/itemThumbnail.controller.ts b/src/services/item/plugins/thumbnail/itemThumbnail.controller.ts index 3b62fcd8c5..fdfaab4da2 100644 --- a/src/services/item/plugins/thumbnail/itemThumbnail.controller.ts +++ b/src/services/item/plugins/thumbnail/itemThumbnail.controller.ts @@ -12,9 +12,9 @@ import { assertIsMember } from '../../../authentication'; import { UploadFileUnexpectedError } from '../../../file/utils/errors'; import { validatedMemberAccountRole } from '../../../member/strategies/validatedMemberAccountRole'; import { DEFAULT_MAX_FILE_SIZE } from '../file/utils/constants'; +import { UploadFileNotImageError } from './errors'; import { deleteSchema, download, upload } from './itemThumbnail.schemas'; import { ItemThumbnailService } from './itemThumbnail.service'; -import { UploadFileNotImageError } from './utils/errors'; type GraaspThumbnailsOptions = { shouldRedirectOnDownload?: boolean; diff --git a/src/services/item/plugins/thumbnail/itemThumbnail.service.test.ts b/src/services/item/plugins/thumbnail/itemThumbnail.service.spec.ts similarity index 87% rename from src/services/item/plugins/thumbnail/itemThumbnail.service.test.ts rename to src/services/item/plugins/thumbnail/itemThumbnail.service.spec.ts index 33a91f6ec7..72a7c17487 100644 --- a/src/services/item/plugins/thumbnail/itemThumbnail.service.test.ts +++ b/src/services/item/plugins/thumbnail/itemThumbnail.service.spec.ts @@ -1,8 +1,9 @@ import { v4 } from 'uuid'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ThumbnailSize } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { ItemFactory } from '../../../../../test/factories/item.factory'; import { db } from '../../../../drizzle/db'; import { AuthorizedItemService } from '../../../authorizedItem.service'; @@ -18,13 +19,13 @@ const mockedItemsId = [ ]; const dummyItemService = { - get: jest.fn(), + get: vi.fn(), } as unknown as ItemService; const stubThumbnailService = { getUrl: constructMockedUrl, } as unknown as ThumbnailService; const stubAuthorizedItemService = { - getItemById: jest.fn(), + getItemById: vi.fn(), } as unknown as AuthorizedItemService; export const itemThumbnailService = new ItemThumbnailService( @@ -36,14 +37,14 @@ export const itemThumbnailService = new ItemThumbnailService( describe('ItemThumbnailService', () => { beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); + vi.clearAllMocks(); + vi.resetAllMocks(); }); describe('getUrl', () => { it('return url for item and size', async () => { const MOCK_ITEM = ItemFactory({ settings: { hasThumbnail: true } }); - jest.spyOn(stubAuthorizedItemService, 'getItemById').mockResolvedValue(MOCK_ITEM); + vi.spyOn(stubAuthorizedItemService, 'getItemById').mockResolvedValue(MOCK_ITEM); const size = ThumbnailSize.Large; const result = await itemThumbnailService.getUrl(db, undefined, { @@ -54,7 +55,7 @@ describe('ItemThumbnailService', () => { }); it('return null for item without thumbnail', async () => { const MOCK_ITEM = ItemFactory({ settings: { hasThumbnail: false } }); - jest.spyOn(stubAuthorizedItemService, 'getItemById').mockResolvedValue(MOCK_ITEM); + vi.spyOn(stubAuthorizedItemService, 'getItemById').mockResolvedValue(MOCK_ITEM); const result = await itemThumbnailService.getUrl(db, undefined, { size: ThumbnailSize.Large, @@ -63,7 +64,7 @@ describe('ItemThumbnailService', () => { expect(result).toBeNull(); }); it('throw if cannot get item', async () => { - jest.spyOn(stubAuthorizedItemService, 'getItemById').mockRejectedValue(new Error()); + vi.spyOn(stubAuthorizedItemService, 'getItemById').mockRejectedValue(new Error()); await expect(() => itemThumbnailService.getUrl(db, undefined, { size: ThumbnailSize.Large, @@ -94,7 +95,7 @@ describe('ItemThumbnailService', () => { it('Handles failures gracefully', async () => { // define a flacky thumbnail service that fails for the first itemId const stubThumbnailService = { - getUrl: jest.fn().mockImplementation(async ({ id }) => { + getUrl: vi.fn().mockImplementation(async ({ id }) => { // fail for the first one if (id === mockedItemsId[0].id) { throw new Error(); diff --git a/src/services/item/plugins/thumbnail/test/fixtures/utils.ts b/src/services/item/plugins/thumbnail/test/fixtures/utils.ts index f1c08d3395..098b167b10 100644 --- a/src/services/item/plugins/thumbnail/test/fixtures/utils.ts +++ b/src/services/item/plugins/thumbnail/test/fixtures/utils.ts @@ -1,3 +1,5 @@ +import { expect } from 'vitest'; + import type { ThumbnailSizeType } from '@graasp/sdk'; import type { ItemsThumbnails } from '../../types'; diff --git a/src/services/item/plugins/thumbnail/utils/errors.ts b/src/services/item/plugins/thumbnail/utils/errors.ts deleted file mode 100644 index dddd7ab0f0..0000000000 --- a/src/services/item/plugins/thumbnail/utils/errors.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; - -import { ErrorFactory, FAILURE_MESSAGES } from '@graasp/sdk'; - -import { PLUGIN_NAME } from '../../../../thumbnail/constants'; - -const ThumbnailError = ErrorFactory(PLUGIN_NAME); - -export class UploadFileNotImageError extends ThumbnailError { - constructor(data?: unknown) { - super( - { - code: 'GPTERR001', - statusCode: StatusCodes.BAD_REQUEST, - message: FAILURE_MESSAGES.FILE_IS_NOT_IMAGE, - }, - data, - ); - } -} diff --git a/src/services/item/test/fixtures/items.vitest.ts b/src/services/item/test/fixtures/items.vitest.ts index 83b64f079a..92286bbe38 100644 --- a/src/services/item/test/fixtures/items.vitest.ts +++ b/src/services/item/test/fixtures/items.vitest.ts @@ -39,3 +39,20 @@ export const expectItem = ( } } }; + +export const expectManyItems = ( + items: ItemRaw[], + correctItems: Partial< + Pick + >[], + creator?: MinimalMember, + parent?: ItemRaw, +) => { + expect(items).toHaveLength(correctItems.length); + + items.forEach(({ id }) => { + const item = items.find(({ id: thisId }) => thisId === id); + const correctItem = correctItems.find(({ id: thisId }) => thisId === id); + expectItem(item, correctItem, creator, parent); + }); +}; diff --git a/src/services/item/utils.test.ts b/src/services/item/utils.spec.ts similarity index 98% rename from src/services/item/utils.test.ts rename to src/services/item/utils.spec.ts index 2dcc678956..f3b5e1cd34 100644 --- a/src/services/item/utils.test.ts +++ b/src/services/item/utils.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { ItemFactory } from '../../../test/factories/item.factory'; import type { FolderItem } from './discrimination'; import { sortChildrenForTreeWith } from './utils'; diff --git a/src/services/itemLogin/errors.ts b/src/services/itemLogin/errors.ts index 6fc04081ed..753ee763df 100644 --- a/src/services/itemLogin/errors.ts +++ b/src/services/itemLogin/errors.ts @@ -1,36 +1,24 @@ import { StatusCodes } from 'http-status-codes'; +import { createError } from '@fastify/error'; + import { ErrorFactory, FAILURE_MESSAGES } from '@graasp/sdk'; const PLUGIN_NAME = 'graasp-plugin-item-login'; export const GraaspItemLoginError = ErrorFactory(PLUGIN_NAME); -export class MemberIdentifierNotFound extends GraaspItemLoginError { - constructor(data?: unknown) { - super( - { - code: 'GILERR002', - statusCode: StatusCodes.NOT_FOUND, - message: 'Member identifier not found', - }, - data, - ); - } -} - -export class InvalidMember extends GraaspItemLoginError { - constructor(data?: unknown) { - super( - { - code: 'GILERR003', - statusCode: StatusCodes.NOT_FOUND, - message: 'This member cannot be used to login to an item', - }, - data, - ); - } -} +export const MemberIdentifierNotFound = createError( + 'GILERR002', + 'Member identifier not found', + StatusCodes.NOT_FOUND, +); + +export const InvalidMember = createError( + 'GILERR003', + 'This member cannot be used to login to an item', + StatusCodes.NOT_FOUND, +); export class MissingItemLoginSchema extends GraaspItemLoginError { constructor(data?: unknown) { diff --git a/src/services/itemLogin/guestPassword.repository.test.ts b/src/services/itemLogin/guestPassword.repository.spec.ts similarity index 97% rename from src/services/itemLogin/guestPassword.repository.test.ts rename to src/services/itemLogin/guestPassword.repository.spec.ts index 4df5b921e2..79a9be8c00 100644 --- a/src/services/itemLogin/guestPassword.repository.test.ts +++ b/src/services/itemLogin/guestPassword.repository.spec.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; import { eq } from 'drizzle-orm'; +import { describe, expect, it } from 'vitest'; import { seedFromJson } from '../../../test/mocks/seed'; import { db } from '../../drizzle/db'; diff --git a/src/services/itemLogin/interfaces/item-login.ts b/src/services/itemLogin/interfaces/item-login.ts deleted file mode 100644 index 4ae0cc1f47..0000000000 --- a/src/services/itemLogin/interfaces/item-login.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Members -export interface ItemLoginMemberCredentials { - username?: string; - password?: string; -} diff --git a/src/services/itemLogin/itemLogin.controller.test.ts b/src/services/itemLogin/itemLogin.controller.test.ts index 4de94992dd..8d1cc047a8 100644 --- a/src/services/itemLogin/itemLogin.controller.test.ts +++ b/src/services/itemLogin/itemLogin.controller.test.ts @@ -19,7 +19,6 @@ import { assertIsDefined } from '../../utils/assertions'; import { ITEMS_ROUTE_PREFIX } from '../../utils/config'; import { MemberCannotAdminItem } from '../../utils/errors'; import { assertIsMemberForTest } from '../authentication'; -import { expectAccount } from '../member/test/fixtures/members'; import { CannotNestItemLoginSchema, ValidMemberSession } from './errors'; const getGuest = async ({ @@ -621,7 +620,8 @@ describe('Item Login Tests', () => { expect(res.statusCode).toBe(StatusCodes.OK); const member = res.json(); - expectAccount(member, guest); + expect(member.name).toEqual(guest.name); + expect(member.id).toEqual(guest.id); const guestInDb = await getGuest({ name: guest.name, @@ -662,7 +662,8 @@ describe('Item Login Tests', () => { }); const member = res.json(); - expectAccount(member, guest); + expect(member.name).toEqual(guest.name); + expect(member.id).toEqual(guest.id); expect(res.statusCode).toBe(StatusCodes.OK); }); @@ -730,7 +731,9 @@ describe('Item Login Tests', () => { payload: { username: 'bob', password: 'alice' }, }); expect(res.statusCode).toBe(StatusCodes.OK); - expectAccount(res.json(), guest); + const member = await res.json(); + expect(member.name).toEqual(guest.name); + expect(member.id).toEqual(guest.id); }); it('Throws if item login with username and wrong password', async () => { diff --git a/src/services/itemLogin/itemLogin.service.ts b/src/services/itemLogin/itemLogin.service.ts index 2ffcb5aa64..0b9cf39cb4 100644 --- a/src/services/itemLogin/itemLogin.service.ts +++ b/src/services/itemLogin/itemLogin.service.ts @@ -24,13 +24,17 @@ import { } from './errors'; import { GuestRepository } from './guest.repository'; import { GuestPasswordRepository } from './guestPassword.repository'; -import type { ItemLoginMemberCredentials } from './interfaces/item-login'; import { ItemLoginSchemaRepository, type ItemSchemaTypeOptions, } from './itemLoginSchema.repository'; import { loginSchemaRequiresPassword } from './utils'; +interface ItemLoginMemberCredentials { + username?: string; + password?: string; +} + @singleton() export class ItemLoginService { private readonly itemLoginSchemaRepository: ItemLoginSchemaRepository; diff --git a/src/services/itemLogin/strategies/guestAccountRole.test.ts b/src/services/itemLogin/strategies/guestAccountRole.spec.ts similarity index 95% rename from src/services/itemLogin/strategies/guestAccountRole.test.ts rename to src/services/itemLogin/strategies/guestAccountRole.spec.ts index 1813286359..715b608cc1 100644 --- a/src/services/itemLogin/strategies/guestAccountRole.test.ts +++ b/src/services/itemLogin/strategies/guestAccountRole.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import type { FastifyRequest } from 'fastify'; diff --git a/src/services/itemMembership/membership.controller.test.ts b/src/services/itemMembership/membership.controller.test.ts index ae8d988c88..b2c8724608 100644 --- a/src/services/itemMembership/membership.controller.test.ts +++ b/src/services/itemMembership/membership.controller.test.ts @@ -82,7 +82,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -96,7 +96,7 @@ describe('Membership routes tests', () => { it('Bad request for invalid id', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/invalid-id/memberships`, + url: `/api/items/invalid-id/memberships`, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); @@ -110,7 +110,7 @@ describe('Membership routes tests', () => { const itemId = v4(); const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${itemId}/memberships`, + url: `/api/items/${itemId}/memberships`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); @@ -127,7 +127,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); expect(response.json().message).toEqual(new MemberCannotAccess(item.id).message); @@ -154,7 +154,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, }); const data = response.json(); for (const m of itemMemberships) { @@ -179,7 +179,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -202,7 +202,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload, }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); @@ -246,7 +246,7 @@ describe('Membership routes tests', () => { const [_actorMembership, membership, anotherMembership] = itemMemberships; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${parent.id}/memberships`, + url: `/api/items/${parent.id}/memberships`, payload: newMembership, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -290,7 +290,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${targetItem.id}/memberships`, + url: `/api/items/${targetItem.id}/memberships`, payload, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -341,7 +341,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload: { permission: PermissionLevel.Read, itemId: item.id, @@ -381,7 +381,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload: newMembership, }); @@ -405,7 +405,7 @@ describe('Membership routes tests', () => { const id = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${id}/memberships`, + url: `/api/items/${id}/memberships`, payload: { permission: PermissionLevel.Read, itemId: item.id, @@ -431,7 +431,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, payload: { accountId: member.id, // missing permission @@ -454,7 +454,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}/memberships/${itemMembership.id}`, + url: `/api/items/${item.id}/memberships/${itemMembership.id}`, payload, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -490,7 +490,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${child.id}/memberships/${writeMembership.id}`, + url: `/api/items/${child.id}/memberships/${writeMembership.id}`, payload: newMembership, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -523,7 +523,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, payload: newMembership, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -563,7 +563,7 @@ describe('Membership routes tests', () => { const newMembership = { permission: PermissionLevel.Write }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${parent.id}/memberships/${readMembership.id}`, + url: `/api/items/${parent.id}/memberships/${readMembership.id}`, payload: newMembership, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -578,7 +578,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${v4()}/memberships/${v4()}`, + url: `/api/items/${v4()}/memberships/${v4()}`, payload: { permission: 'permission' }, }); expect(response.statusCode).toEqual(StatusCodes.BAD_REQUEST); @@ -592,7 +592,7 @@ describe('Membership routes tests', () => { const id = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${id}/memberships/${id}`, + url: `/api/items/${id}/memberships/${id}`, payload: { permission: PermissionLevel.Write }, }); expect(response.statusCode).toEqual(StatusCodes.BAD_REQUEST); @@ -624,7 +624,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${child.id}/memberships/${adminMembership.id}`, + url: `/api/items/${child.id}/memberships/${adminMembership.id}`, payload: { permission: PermissionLevel.Read, accountId: member.id, @@ -658,7 +658,7 @@ describe('Membership routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}/memberships/${guestMembership.id}`, + url: `/api/items/${item.id}/memberships/${guestMembership.id}`, payload: newMembership, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -676,7 +676,7 @@ describe('Membership routes tests', () => { }); const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${itemMembership.id}`, + url: `/api/items/${item.id}/memberships/${itemMembership.id}`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); @@ -705,7 +705,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); @@ -736,7 +736,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}?purgeBelow=true`, + url: `/api/items/${item.id}/memberships/${membership.id}?purgeBelow=true`, }); expect(response.statusCode).toEqual(StatusCodes.NO_CONTENT); @@ -752,7 +752,7 @@ describe('Membership routes tests', () => { const id = 'invalid-id'; const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${v4()}/memberships/${id}`, + url: `/api/items/${v4()}/memberships/${id}`, }); expect(response.statusMessage).toEqual(ReasonPhrases.BAD_REQUEST); expect(response.statusCode).toEqual(StatusCodes.BAD_REQUEST); @@ -768,7 +768,7 @@ describe('Membership routes tests', () => { const id = v4(); const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${id}`, + url: `/api/items/${item.id}/memberships/${id}`, }); expect(response.statusCode).toEqual(StatusCodes.NOT_FOUND); expect(response.json()).toEqual(new ItemMembershipNotFound({ id })); @@ -793,7 +793,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, }); expect(response.statusCode).toEqual(StatusCodes.FORBIDDEN); expect(response.json()).toEqual(new MemberCannotAdminItem(item.id)); @@ -819,7 +819,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, }); expect(response.statusCode).toEqual(StatusCodes.FORBIDDEN); expect(response.json()).toEqual(new MemberCannotAdminItem(item.id)); @@ -841,7 +841,7 @@ describe('Membership routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/${membership.id}`, + url: `/api/items/${item.id}/memberships/${membership.id}`, }); expect(response.statusCode).toEqual(StatusCodes.FORBIDDEN); expect(response.json()).toMatchObject(new CannotDeleteOnlyAdmin({ id: expect.anything() })); diff --git a/src/services/itemMembership/membership.repository.test.ts b/src/services/itemMembership/membership.repository.spec.ts similarity index 98% rename from src/services/itemMembership/membership.repository.test.ts rename to src/services/itemMembership/membership.repository.spec.ts index 6a6af97696..d1e07ad533 100644 --- a/src/services/itemMembership/membership.repository.test.ts +++ b/src/services/itemMembership/membership.repository.spec.ts @@ -1,6 +1,7 @@ +import { describe, expect, it } from 'vitest'; + import { PermissionLevel } from '@graasp/sdk'; -import { clearDatabase } from '../../../test/app'; import { seedFromJson } from '../../../test/mocks/seed'; import { db } from '../../drizzle/db'; import { assertIsDefined } from '../../utils/assertions'; @@ -10,11 +11,6 @@ import { ItemMembershipRepository } from './membership.repository'; const itemMembershipRepository = new ItemMembershipRepository(); describe('ItemMembership Repository', () => { - afterEach(async () => { - jest.clearAllMocks(); - await clearDatabase(db); - }); - describe('getAdminsForItem', () => { it('should return empty array when there is no corresponding permission', async () => { const { diff --git a/src/services/member/test/fixtures/members.ts b/src/services/member/test/fixtures/members.ts index 101c454179..a623ce7bd2 100644 --- a/src/services/member/test/fixtures/members.ts +++ b/src/services/member/test/fixtures/members.ts @@ -1,3 +1,5 @@ +import { expect } from 'vitest'; + import { AccountType, DEFAULT_LANG } from '@graasp/sdk'; import type { MemberRaw, MinimalAccount } from '../../../../drizzle/types'; diff --git a/src/services/member/test/register.test.ts b/src/services/member/test/register.test.ts index da13cff01b..372a598ae1 100644 --- a/src/services/member/test/register.test.ts +++ b/src/services/member/test/register.test.ts @@ -6,7 +6,14 @@ import waitForExpect from 'wait-for-expect'; import type { FastifyInstance } from 'fastify'; -import { HttpMethod, MAX_USERNAME_LENGTH, MemberFactory, RecaptchaAction } from '@graasp/sdk'; +import { + AccountType, + DEFAULT_LANG, + HttpMethod, + MAX_USERNAME_LENGTH, + MemberFactory, + RecaptchaAction, +} from '@graasp/sdk'; import build, { MOCK_CAPTCHA, clearDatabase, unmockAuthenticate } from '../../../../test/app'; import { seedFromJson } from '../../../../test/mocks/seed'; @@ -17,7 +24,6 @@ import { accountsTable, invitationsTable, itemMembershipsTable } from '../../../ import type { MemberRaw } from '../../../drizzle/types'; import { MailerService } from '../../../plugins/mailer/mailer.service'; import { assertIsDefined } from '../../../utils/assertions'; -import { expectMember } from './fixtures/members'; jest.mock('node-fetch'); (fetch as jest.MockedFunction).mockImplementation(async () => { @@ -31,6 +37,19 @@ const getMemberByEmail = async (lowercaseEmail: string) => { })) as MemberRaw | undefined; }; +const expectMember = ( + m: MemberRaw | undefined | null, + expectation: Partial> & Pick, +) => { + if (!m) { + throw 'member does not exist'; + } + expect(m.name).toEqual(expectation.name); + expect(m.email).toEqual(expectation.email); + expect(m.type).toEqual(expectation.type ?? AccountType.Individual); + expect(m.extra).toEqual(expectation.extra ?? { lang: DEFAULT_LANG }); +}; + describe('POST /register', () => { let app: FastifyInstance; let mailerService: MailerService; @@ -88,7 +107,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register?lang=${lang}`, + url: `/api/register?lang=${lang}`, payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -110,7 +129,7 @@ describe('POST /register', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -127,7 +146,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: '/api/register', payload: { email, name, captcha: MOCK_CAPTCHA, enableSaveActions }, }); @@ -152,7 +171,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: '/api/register', payload: { email, name, enableSaveActions, captcha: MOCK_CAPTCHA }, }); From d85ba3894df181f88f8600962a9331b8acb58b5c Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 11:11:02 +0000 Subject: [PATCH 11/24] fix: update tests --- .../magicLink/magicLink.controller.register.test.ts | 8 ++++---- .../plugins/app/appSetting/appSetting.controller.test.ts | 4 ++-- src/services/item/plugins/html/h5p/h5p.controller.test.ts | 4 ++-- src/services/item/plugins/html/h5p/test/helpers.ts | 2 +- src/services/tag/tag.service.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts index 9f9b735a10..d1363bce9c 100644 --- a/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts +++ b/src/services/auth/plugins/magicLink/magicLink.controller.register.test.ts @@ -107,7 +107,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register?lang=${lang}`, + url: `/api/register?lang=${lang}`, payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -129,7 +129,7 @@ describe('POST /register', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: `/api/register`, payload: { email, name, captcha: MOCK_CAPTCHA }, }); @@ -146,7 +146,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: `/api/register`, payload: { email, name, captcha: MOCK_CAPTCHA, enableSaveActions }, }); @@ -171,7 +171,7 @@ describe('POST /register', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/register`, + url: `/api/register`, payload: { email, name, enableSaveActions, captcha: MOCK_CAPTCHA }, }); diff --git a/src/services/item/plugins/app/appSetting/appSetting.controller.test.ts b/src/services/item/plugins/app/appSetting/appSetting.controller.test.ts index beec019805..0865abddc1 100644 --- a/src/services/item/plugins/app/appSetting/appSetting.controller.test.ts +++ b/src/services/item/plugins/app/appSetting/appSetting.controller.test.ts @@ -784,7 +784,7 @@ describe('Apps Settings Tests', () => { // copy item const response = await app.inject({ method: 'POST', - url: 'items/copy', + url: '/api/items/copy', query: { id: [item.id], }, @@ -836,7 +836,7 @@ describe('Apps Settings Tests', () => { // copy parent const response = await app.inject({ method: 'POST', - url: 'items/copy', + url: '/api/items/copy', query: { id: [parent.id], }, diff --git a/src/services/item/plugins/html/h5p/h5p.controller.test.ts b/src/services/item/plugins/html/h5p/h5p.controller.test.ts index d8aee7aa72..35cd8218d7 100644 --- a/src/services/item/plugins/html/h5p/h5p.controller.test.ts +++ b/src/services/item/plugins/html/h5p/h5p.controller.test.ts @@ -150,7 +150,7 @@ describe('Service plugin', () => { // delete item await app.inject({ method: 'DELETE', - url: `/items/`, + url: `/api/items`, query: { id: [item.id], }, @@ -191,7 +191,7 @@ describe('Service plugin', () => { // copy item await app.inject({ method: 'POST', - url: 'items/copy', + url: '/api/items/copy', query: { id: [item.id], }, diff --git a/src/services/item/plugins/html/h5p/test/helpers.ts b/src/services/item/plugins/html/h5p/test/helpers.ts index 6ab46a06bf..43f7272fcf 100644 --- a/src/services/item/plugins/html/h5p/test/helpers.ts +++ b/src/services/item/plugins/html/h5p/test/helpers.ts @@ -35,7 +35,7 @@ export function injectH5PImport( return app.inject({ method: 'POST', - url: '/items/h5p-import', + url: '/api/items/h5p-import', payload: formData, headers: formData.getHeaders(), query, diff --git a/src/services/tag/tag.service.ts b/src/services/tag/tag.service.ts index cd83e58ecd..c6e3b5b607 100644 --- a/src/services/tag/tag.service.ts +++ b/src/services/tag/tag.service.ts @@ -1,7 +1,7 @@ import { singleton } from 'tsyringe'; import { type DBConnection } from '../../drizzle/db'; -import { ItemTagRepository } from '../item/plugins/tag/ItemTag.repository'; +import { ItemTagRepository } from '../item/plugins/tag/itemTag.repository'; import type { TagCategoryOptions } from './tag.schemas'; @singleton() From 7f240ce4932ce0f6b4a66ace372671ca274cf8df Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 11:37:54 +0000 Subject: [PATCH 12/24] fix: more tests --- ...ls.test.ts => authorization.utils.spec.ts} | 54 ++++++++++++++----- .../etherpad/etherpad.controller.test.ts | 2 +- .../membership.controller.test.ts | 2 +- ...tory.test.ts => member.repository.spec.ts} | 1 + ...schemas.test.ts => member.schemas.spec.ts} | 2 + .../profile/memberProfile.controller.test.ts | 24 ++++----- ...ository.test.ts => tag.repository.spec.ts} | 1 + 7 files changed, 60 insertions(+), 26 deletions(-) rename src/services/{authorization.utils.test.ts => authorization.utils.spec.ts} (74%) rename src/services/member/{member.repository.test.ts => member.repository.spec.ts} (99%) rename src/services/member/{member.schemas.test.ts => member.schemas.spec.ts} (95%) rename src/services/tag/{tag.repository.test.ts => tag.repository.spec.ts} (98%) diff --git a/src/services/authorization.utils.test.ts b/src/services/authorization.utils.spec.ts similarity index 74% rename from src/services/authorization.utils.test.ts rename to src/services/authorization.utils.spec.ts index 64051705db..d349115c8d 100644 --- a/src/services/authorization.utils.test.ts +++ b/src/services/authorization.utils.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ItemVisibilityType, @@ -10,12 +11,12 @@ import { import { ItemFactory } from '../../test/factories/item.factory'; import { MemberFactory } from '../../test/factories/member.factory'; import type { DBConnection } from '../drizzle/db'; -import type { ItemVisibilityWithItem } from '../drizzle/types'; -import { AccountType } from '../types'; +import type { ItemRaw, ItemVisibilityRaw, ItemVisibilityWithItem } from '../drizzle/types'; +import { AccountType, MinimalMember } from '../types'; import { filterOutPackedDescendants } from './authorization.utils'; import type { PackedItem } from './item/ItemWrapper'; import { ItemVisibilityRepository } from './item/plugins/itemVisibility/itemVisibility.repository'; -import { expectPackedItem } from './item/test/fixtures/items'; +import { expectItem } from './item/test/fixtures/items.vitest'; import { ItemMembershipRepository } from './itemMembership/membership.repository'; const OWNER = { id: 'owner', name: 'owner', type: AccountType.Individual, isValidated: true }; @@ -58,9 +59,38 @@ const buildPackedDescendants = ( return arr as unknown as PackedItem[]; }; +const expectPackedItem = ( + newItem: Partial | undefined | null, + correctItem: + | (Partial> & + Pick) + | undefined + | null, + creator?: MinimalMember, + parent?: ItemRaw, + visibilities?: Pick[], +) => { + expectItem(newItem, correctItem, creator, parent); + + expect(newItem!.permission).toEqual(correctItem?.permission); + + const pVisibility = visibilities?.find((t) => t.type === ItemVisibilityType.Public); + if (pVisibility) { + expect(newItem!.public!.type).toEqual(pVisibility.type); + expect(newItem!.public!.id).toEqual(pVisibility.id); + expect(newItem!.public!.itemPath).toEqual(pVisibility.itemPath); + } + const hVisibility = visibilities?.find((t) => t.type === ItemVisibilityType.Hidden); + if (hVisibility) { + expect(newItem!.hidden!.type).toEqual(hVisibility.type); + expect(newItem!.hidden!.id).toEqual(hVisibility.id); + expect(newItem!.hidden!.itemPath).toEqual(hVisibility.itemPath); + } +}; + describe('filterOutPackedDescendants', () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('Admin returns all', async () => { @@ -72,10 +102,10 @@ describe('filterOutPackedDescendants', () => { const repositories = { itemMembershipRepository: { - getAllBelow: jest.fn(async () => memberships), + getAllBelow: vi.fn(async () => memberships), } as unknown as ItemMembershipRepository, itemVisibilityRepository: { - getManyBelowAndSelf: jest.fn(async () => [hiddenVisibility]), + getManyBelowAndSelf: vi.fn(async () => [hiddenVisibility]), } as unknown as ItemVisibilityRepository, }; @@ -102,10 +132,10 @@ describe('filterOutPackedDescendants', () => { const repositories = { itemMembershipRepository: { - getAllBelow: jest.fn(async () => memberships), + getAllBelow: vi.fn(async () => memberships), } as unknown as ItemMembershipRepository, itemVisibilityRepository: { - getManyBelowAndSelf: jest.fn(async () => [hiddenVisibility]), + getManyBelowAndSelf: vi.fn(async () => [hiddenVisibility]), } as unknown as ItemVisibilityRepository, }; @@ -132,10 +162,10 @@ describe('filterOutPackedDescendants', () => { const repositories = { itemMembershipRepository: { - getAllBelow: jest.fn(async () => memberships), + getAllBelow: vi.fn(async () => memberships), } as unknown as ItemMembershipRepository, itemVisibilityRepository: { - getManyBelowAndSelf: jest.fn(async () => [hiddenVisibility]), + getManyBelowAndSelf: vi.fn(async () => [hiddenVisibility]), } as unknown as ItemVisibilityRepository, }; @@ -161,10 +191,10 @@ describe('filterOutPackedDescendants', () => { const repositories = { itemMembershipRepository: { - getAllBelow: jest.fn(async () => []), + getAllBelow: vi.fn(async () => []), } as unknown as ItemMembershipRepository, itemVisibilityRepository: { - getManyBelowAndSelf: jest.fn(async () => [hiddenVisibility]), + getManyBelowAndSelf: vi.fn(async () => [hiddenVisibility]), } as unknown as ItemVisibilityRepository, }; diff --git a/src/services/item/plugins/etherpad/etherpad.controller.test.ts b/src/services/item/plugins/etherpad/etherpad.controller.test.ts index f9dc3d0f85..ba7e10ef7b 100644 --- a/src/services/item/plugins/etherpad/etherpad.controller.test.ts +++ b/src/services/item/plugins/etherpad/etherpad.controller.test.ts @@ -267,7 +267,7 @@ describe('Etherpad service API', () => { describe('view a pad', () => { const payloadView = (mode: 'read' | 'write', itemId: string) => ({ method: HttpMethod.Get, - url: `items/etherpad/view/${itemId}`, + url: `/api/items/etherpad/view/${itemId}`, query: { mode, }, diff --git a/src/services/itemMembership/membership.controller.test.ts b/src/services/itemMembership/membership.controller.test.ts index b2c8724608..477403eafb 100644 --- a/src/services/itemMembership/membership.controller.test.ts +++ b/src/services/itemMembership/membership.controller.test.ts @@ -61,7 +61,7 @@ describe('Membership routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships`, + url: `/api/items/${item.id}/memberships`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); diff --git a/src/services/member/member.repository.test.ts b/src/services/member/member.repository.spec.ts similarity index 99% rename from src/services/member/member.repository.test.ts rename to src/services/member/member.repository.spec.ts index 43996360d3..8930524b19 100644 --- a/src/services/member/member.repository.test.ts +++ b/src/services/member/member.repository.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import { EmailFrequency } from '@graasp/sdk'; diff --git a/src/services/member/member.schemas.test.ts b/src/services/member/member.schemas.spec.ts similarity index 95% rename from src/services/member/member.schemas.test.ts rename to src/services/member/member.schemas.spec.ts index ac14d26379..90d2b41e7a 100644 --- a/src/services/member/member.schemas.test.ts +++ b/src/services/member/member.schemas.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { EMAIL_REGEX } from './member.schemas'; describe('Email regex', () => { diff --git a/src/services/member/plugins/profile/memberProfile.controller.test.ts b/src/services/member/plugins/profile/memberProfile.controller.test.ts index 87409af185..fcfbc25ab9 100644 --- a/src/services/member/plugins/profile/memberProfile.controller.test.ts +++ b/src/services/member/plugins/profile/memberProfile.controller.test.ts @@ -53,7 +53,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -70,7 +70,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -88,7 +88,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -103,7 +103,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -114,7 +114,7 @@ describe('Profile Member routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/own`, }); expect(response.statusCode).toEqual(StatusCodes.UNAUTHORIZED); @@ -132,7 +132,7 @@ describe('Profile Member routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Post, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}`, payload, }); @@ -155,7 +155,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}`, payload, }); @@ -208,7 +208,7 @@ describe('Profile Member routes tests', () => { const memberId = member.id; const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/${memberId}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/${memberId}`, }); expect(response.statusCode).toEqual(StatusCodes.OK); expect(response.json()).toBeNull(); @@ -223,7 +223,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/${member.id}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/${member.id}`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -240,7 +240,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}/${member.id}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}/${member.id}`, }); expect(response.statusCode).toEqual(StatusCodes.OK); @@ -253,7 +253,7 @@ describe('Profile Member routes tests', () => { const payload = { bio: 'Random Bio' }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}`, payload, }); expect(response.statusCode).toEqual(StatusCodes.UNAUTHORIZED); @@ -267,7 +267,7 @@ describe('Profile Member routes tests', () => { const payload = { bio: 'Random Bio' }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}`, payload, }); diff --git a/src/services/tag/tag.repository.test.ts b/src/services/tag/tag.repository.spec.ts similarity index 98% rename from src/services/tag/tag.repository.test.ts rename to src/services/tag/tag.repository.spec.ts index 4d31b53f53..04cdc93481 100644 --- a/src/services/tag/tag.repository.test.ts +++ b/src/services/tag/tag.repository.spec.ts @@ -1,5 +1,6 @@ import { and, eq } from 'drizzle-orm/sql'; import { v4 } from 'uuid'; +import { afterEach, describe, expect, it } from 'vitest'; import { TagCategory, type TagCategoryType, TagFactory } from '@graasp/sdk'; From 40c74f9f4dbe78e75fae5ef8f24e07dfff651151 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 11:54:47 +0000 Subject: [PATCH 13/24] fix: tests --- .../magicLink/magicLink.controller.test.ts | 12 ++-- .../file/appSetting.file.controller.ts | 2 +- .../document/document.controller.test.ts | 14 ++--- .../embeddedLink/link.controller.test.ts | 22 +++---- .../etherpad/etherpad.controller.test.ts | 8 +-- .../membershipRequest.controller.test.ts | 62 +++++++++---------- src/services/member/member.controller.test.ts | 26 ++++---- .../profile/memberProfile.controller.test.ts | 2 +- 8 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/services/auth/plugins/magicLink/magicLink.controller.test.ts b/src/services/auth/plugins/magicLink/magicLink.controller.test.ts index dc715f2f63..8cb0e1af2f 100644 --- a/src/services/auth/plugins/magicLink/magicLink.controller.test.ts +++ b/src/services/auth/plugins/magicLink/magicLink.controller.test.ts @@ -87,7 +87,7 @@ describe('Auth routes tests', () => { const mockSendEmail = jest.spyOn(mailerService, 'sendRaw'); const response = await app.inject({ method: HttpMethod.Post, - url: `/login?lang=${lang}`, + url: `/api/login?lang=${lang}`, payload: { email: member.email, captcha: MOCK_CAPTCHA }, }); @@ -146,7 +146,7 @@ describe('Auth routes tests', () => { const t = sign({ sub: member.id }, JWT_SECRET); const response = await app.inject({ method: HttpMethod.Get, - url: `/auth?t=${t}`, + url: `/api/auth?t=${t}`, }); expect(response.statusCode).toEqual(StatusCodes.SEE_OTHER); expect(response.headers.location).not.toContain('error'); @@ -166,7 +166,7 @@ describe('Auth routes tests', () => { const t = sign({ sub: member.id, emailValidation: true }, JWT_SECRET); const response = await app.inject({ method: HttpMethod.Get, - url: `/auth?t=${t}`, + url: `/api/auth?t=${t}`, }); expect(response.statusCode).toEqual(StatusCodes.SEE_OTHER); expect(response.headers.location).not.toContain('error'); @@ -182,7 +182,7 @@ describe('Auth routes tests', () => { const t = sign({ sub: undefined }, JWT_SECRET); const response = await app.inject({ method: HttpMethod.Get, - url: `/auth?t=${t}`, + url: `/api/auth?t=${t}`, }); expect(response.statusCode).toEqual(StatusCodes.SEE_OTHER); const url = AUTH_CLIENT_HOST; @@ -194,7 +194,7 @@ describe('Auth routes tests', () => { const t = sign({ sub: v4() }, JWT_SECRET); const response = await app.inject({ method: HttpMethod.Get, - url: `/auth?t=${t}`, + url: `/api/auth?t=${t}`, }); expect(response.statusCode).toEqual(StatusCodes.SEE_OTHER); const url = AUTH_CLIENT_HOST; @@ -210,7 +210,7 @@ describe('Auth routes tests', () => { const t = sign({ sub: member.id }, 'secret'); const response = await app.inject({ method: HttpMethod.Get, - url: `/auth?t=${t}`, + url: `/api/auth?t=${t}`, }); expect(response.statusCode).toEqual(StatusCodes.SEE_OTHER); diff --git a/src/services/item/plugins/app/appSetting/plugins/file/appSetting.file.controller.ts b/src/services/item/plugins/app/appSetting/plugins/file/appSetting.file.controller.ts index 89db8a08da..fb8eabee95 100644 --- a/src/services/item/plugins/app/appSetting/plugins/file/appSetting.file.controller.ts +++ b/src/services/item/plugins/app/appSetting/plugins/file/appSetting.file.controller.ts @@ -100,7 +100,7 @@ const basePlugin: FastifyPluginAsyncTypebox = async (fa return appSettingFileService.upload(tx, account, file, app.item); }) .catch((e) => { - console.error(e); + request.log.error(e); // TODO rollback uploaded file diff --git a/src/services/item/plugins/document/document.controller.test.ts b/src/services/item/plugins/document/document.controller.test.ts index 003a4862ff..62f8a47141 100644 --- a/src/services/item/plugins/document/document.controller.test.ts +++ b/src/services/item/plugins/document/document.controller.test.ts @@ -145,7 +145,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload: { name: 'new name' }, }); @@ -162,7 +162,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload: { name: 'new name' }, }); @@ -182,7 +182,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload: { name: 'new name' }, }); @@ -202,7 +202,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: '' }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload, }); @@ -222,7 +222,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content', isRaw: 'value' }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload, }); @@ -242,7 +242,7 @@ describe('Document Item tests', () => { const payload = { name: 'name', content: 'content', flavor: 'value' }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload, }); @@ -285,7 +285,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/documents/${item.id}`, + url: `/api/items/documents/${item.id}`, payload, }); diff --git a/src/services/item/plugins/embeddedLink/link.controller.test.ts b/src/services/item/plugins/embeddedLink/link.controller.test.ts index f0ad645647..be28af8384 100644 --- a/src/services/item/plugins/embeddedLink/link.controller.test.ts +++ b/src/services/item/plugins/embeddedLink/link.controller.test.ts @@ -202,7 +202,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/embedded-links`, + url: `/api/items/embedded-links`, payload: { name: 'name', url: MOCK_URL }, }); @@ -215,7 +215,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/embedded-links`, + url: `/api/items/embedded-links`, payload: { name: 'name', url: MOCK_URL }, }); @@ -300,7 +300,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if name is invalid', async () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${v4()}`, + url: `/api/items/embedded-links/${v4()}`, payload: { name: '' }, }); @@ -309,7 +309,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if url is invalid', async () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${v4()}`, + url: `/api/items/embedded-links/${v4()}`, payload: { url: 'url' }, }); @@ -318,7 +318,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if id is invalid', async () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/invalid`, + url: `/api/items/embedded-links/invalid`, payload: { url: 'url' }, }); @@ -328,7 +328,7 @@ describe('Tests Embedded Link Controller', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${v4()}`, + url: `/api/items/embedded-links/${v4()}`, payload: { name: 'name', url: MOCK_URL }, }); @@ -351,7 +351,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${v4()}`, + url: `/api/items/embedded-links/${v4()}`, payload: { name: 'name', url: MOCK_URL }, }); @@ -364,7 +364,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${v4()}`, + url: `/api/items/embedded-links/${v4()}`, payload: { name: 'name', url: MOCK_URL }, }); @@ -388,7 +388,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${item.id}`, + url: `/api/items/embedded-links/${item.id}`, payload: { name: 'name', url: MOCK_URL }, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -415,7 +415,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${item.id}`, + url: `/api/items/embedded-links/${item.id}`, payload: { name: 'name', url: MOCK_URL }, }); @@ -443,7 +443,7 @@ describe('Tests Embedded Link Controller', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/embedded-links/${initialItem.id}`, + url: `/api/items/embedded-links/${initialItem.id}`, payload: { name: 'name', showLinkIframe: true, diff --git a/src/services/item/plugins/etherpad/etherpad.controller.test.ts b/src/services/item/plugins/etherpad/etherpad.controller.test.ts index ba7e10ef7b..116ca96d6e 100644 --- a/src/services/item/plugins/etherpad/etherpad.controller.test.ts +++ b/src/services/item/plugins/etherpad/etherpad.controller.test.ts @@ -937,7 +937,7 @@ describe('Etherpad service API', () => { }); await app.inject({ method: 'DELETE', - url: `/items/`, + url: `/api/items`, query: { id: [item.id], }, @@ -1046,7 +1046,7 @@ describe('Etherpad service API', () => { mockAuthenticate(actor); const res = await app.inject({ method: HttpMethod.Patch, - url: `items/etherpad/${item.id}`, + url: `/api/items/etherpad/${item.id}`, payload: { name: 'new-name', }, @@ -1076,7 +1076,7 @@ describe('Etherpad service API', () => { mockAuthenticate(actor); const res = await app.inject({ method: HttpMethod.Patch, - url: `items/etherpad/${item.id}`, + url: `/api/items/etherpad/${item.id}`, payload: { name: 'new-name', readerPermission: EtherpadPermission.Write, @@ -1090,7 +1090,7 @@ describe('Etherpad service API', () => { mockAuthenticate(actor); const res = await app.inject({ method: HttpMethod.Patch, - url: `items/etherpad/${v4()}`, + url: `/api/items/etherpad/${v4()}`, }); expect(res.statusCode).toEqual(StatusCodes.BAD_REQUEST); }); diff --git a/src/services/itemMembership/plugins/MembershipRequest/membershipRequest.controller.test.ts b/src/services/itemMembership/plugins/MembershipRequest/membershipRequest.controller.test.ts index d19376f13b..b3ee9f6cbd 100644 --- a/src/services/itemMembership/plugins/MembershipRequest/membershipRequest.controller.test.ts +++ b/src/services/itemMembership/plugins/MembershipRequest/membershipRequest.controller.test.ts @@ -72,7 +72,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -98,7 +98,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -133,7 +133,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -150,7 +150,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${uuid()}/memberships/requests`, + url: `/api/items/${uuid()}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); @@ -166,7 +166,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -183,7 +183,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -195,7 +195,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -215,7 +215,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests/own`, + url: `/api/items/${item.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.OK); const data = await response.json(); @@ -238,7 +238,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${childItem.id}/memberships/requests/own`, + url: `/api/items/${childItem.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -261,7 +261,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests/own`, + url: `/api/items/${item.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -284,7 +284,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests/own`, + url: `/api/items/${item.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -310,7 +310,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests/own`, + url: `/api/items/${item.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.OK); const data = await response.json(); @@ -323,7 +323,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${uuid()}/memberships/requests/own`, + url: `/api/items/${uuid()}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); }); @@ -334,7 +334,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/items/${item.id}/memberships/requests/own`, + url: `/api/items/${item.id}/memberships/requests/own`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); @@ -359,7 +359,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -410,7 +410,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${targetItem.id}/memberships/requests`, + url: `/api/items/${targetItem.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -440,7 +440,7 @@ describe('MembershipRequest', () => { const responseSecondPost = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(responseSecondPost.statusCode).toBe(StatusCodes.BAD_REQUEST); // do not send email @@ -453,7 +453,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); expect(mockSendEmail).toHaveBeenCalledTimes(0); @@ -461,7 +461,7 @@ describe('MembershipRequest', () => { it('rejects when unauthenticated with non-existing item id', async () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${uuid()}/memberships/requests`, + url: `/api/items/${uuid()}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); expect(mockSendEmail).toHaveBeenCalledTimes(0); @@ -474,7 +474,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${uuid()}/memberships/requests`, + url: `/api/items/${uuid()}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); expect(mockSendEmail).toHaveBeenCalledTimes(0); @@ -496,7 +496,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); const membershipRequest = await db.query.membershipRequestsTable.findFirst({ @@ -526,7 +526,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); const membershipRequest = await db.query.membershipRequestsTable.findFirst({ @@ -551,7 +551,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/memberships/requests`, + url: `/api/items/${item.id}/memberships/requests`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); expect(mockSendEmail).toHaveBeenCalledTimes(0); @@ -577,7 +577,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${member.id}`, + url: `/api/items/${item.id}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); const membershipRequest = await db.query.membershipRequestsTable.findFirst({ @@ -598,7 +598,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${uuid()}/memberships/requests/${member.id}`, + url: `/api/items/${uuid()}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); }); @@ -620,7 +620,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${member.id}`, + url: `/api/items/${item.id}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); }); @@ -640,7 +640,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${uuid()}`, + url: `/api/items/${item.id}/memberships/requests/${uuid()}`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); }); @@ -658,7 +658,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${member.id}`, + url: `/api/items/${item.id}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); }); @@ -680,7 +680,7 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${member.id}`, + url: `/api/items/${item.id}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); }); @@ -692,14 +692,14 @@ describe('MembershipRequest', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${item.id}/memberships/requests/${member.id}`, + url: `/api/items/${item.id}/memberships/requests/${member.id}`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); it('rejects unauthorized if unauthenticated with non-existing ids', async () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/items/${uuid()}/memberships/requests/${uuid()}`, + url: `/api/items/${uuid()}/memberships/requests/${uuid()}`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); diff --git a/src/services/member/member.controller.test.ts b/src/services/member/member.controller.test.ts index 4eab650ca3..cd1737392f 100644 --- a/src/services/member/member.controller.test.ts +++ b/src/services/member/member.controller.test.ts @@ -229,7 +229,7 @@ describe('Member routes tests', () => { } = await seedFromJson({ members: [{}] }); const response = await app.inject({ method: HttpMethod.Get, - url: `/members/${member.id}`, + url: `/api/members/${member.id}`, }); const m = response.json(); @@ -251,7 +251,7 @@ describe('Member routes tests', () => { const memberId = member.id; const response = await app.inject({ method: HttpMethod.Get, - url: `/members/${memberId}`, + url: `/api/members/${memberId}`, }); const m = response.json(); @@ -268,7 +268,7 @@ describe('Member routes tests', () => { mockAuthenticate(actor); const response = await app.inject({ method: HttpMethod.Get, - url: `/members/invalid-id`, + url: `/api/members/invalid-id`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -283,7 +283,7 @@ describe('Member routes tests', () => { const memberId = v4(); const response = await app.inject({ method: HttpMethod.Get, - url: `/members/${memberId}`, + url: `/api/members/${memberId}`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); expect(response.json().message).toEqual(new MemberNotFound({ id: memberId }).message); @@ -297,7 +297,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { name: newName, extra: { @@ -322,7 +322,7 @@ describe('Member routes tests', () => { }; const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { name: newName, extra: newExtra, @@ -350,7 +350,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { name: newName, }, @@ -372,7 +372,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { name: newName, }, @@ -393,7 +393,7 @@ describe('Member routes tests', () => { const enableSaveActions = true; const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { enableSaveActions }, }); @@ -413,7 +413,7 @@ describe('Member routes tests', () => { // Start by enabling save actions await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { enableSaveActions: true }, }); const memberBeforePatch = await db.query.accountsTable.findFirst({ @@ -424,7 +424,7 @@ describe('Member routes tests', () => { const enableSaveActions = false; const response = await app.inject({ method: HttpMethod.Patch, - url: `/members/current`, + url: `/api/members/current`, payload: { enableSaveActions }, }); @@ -441,7 +441,7 @@ describe('Member routes tests', () => { it('Throws if signed out', async () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/members/current`, + url: `/api/members/current`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -456,7 +456,7 @@ describe('Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Delete, - url: `/members/current`, + url: `/api/members/current`, }); const m = await getMemberUtil(actor.id); diff --git a/src/services/member/plugins/profile/memberProfile.controller.test.ts b/src/services/member/plugins/profile/memberProfile.controller.test.ts index fcfbc25ab9..834a23f217 100644 --- a/src/services/member/plugins/profile/memberProfile.controller.test.ts +++ b/src/services/member/plugins/profile/memberProfile.controller.test.ts @@ -180,7 +180,7 @@ describe('Profile Member routes tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/members${MEMBER_PROFILE_ROUTE_PREFIX}`, + url: `/api/members${MEMBER_PROFILE_ROUTE_PREFIX}`, payload, }); From db6627b5d0caf6119494a26d10dc2f9376fa19da Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 12:08:28 +0000 Subject: [PATCH 14/24] fix: tests --- .../plugins/embeddedLink/link.legacy.test.ts | 6 +++--- .../plugins/enroll/enroll.controller.test.ts | 20 +++++++++---------- .../maintenance.controller.test.ts | 4 ++-- ...test.ts => maintenance.repository.spec.ts} | 1 + .../memberExportData.controller.test.ts | 2 +- 5 files changed, 17 insertions(+), 16 deletions(-) rename src/services/maintenance/{maintenance.repository.test.ts => maintenance.repository.spec.ts} (97%) diff --git a/src/services/item/plugins/embeddedLink/link.legacy.test.ts b/src/services/item/plugins/embeddedLink/link.legacy.test.ts index 5f04b0633e..baa2265189 100644 --- a/src/services/item/plugins/embeddedLink/link.legacy.test.ts +++ b/src/services/item/plugins/embeddedLink/link.legacy.test.ts @@ -64,7 +64,7 @@ describe('Link Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload: { name: 'new name' }, }); @@ -101,7 +101,7 @@ describe('Link Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); @@ -136,7 +136,7 @@ describe('Link Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); diff --git a/src/services/item/plugins/enroll/enroll.controller.test.ts b/src/services/item/plugins/enroll/enroll.controller.test.ts index 113866f483..ceedac9428 100644 --- a/src/services/item/plugins/enroll/enroll.controller.test.ts +++ b/src/services/item/plugins/enroll/enroll.controller.test.ts @@ -54,7 +54,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -89,7 +89,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${anotherItem.id}/enroll`, + url: `/api/items/${anotherItem.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -115,7 +115,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${anotherItem.id}/enroll`, + url: `/api/items/${anotherItem.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); @@ -131,7 +131,7 @@ describe('Enroll', () => { }); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -140,7 +140,7 @@ describe('Enroll', () => { unmockAuthenticate(); const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${uuid()}/enroll`, + url: `/api/items/${uuid()}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); @@ -152,7 +152,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${uuid()}/enroll`, + url: `/api/items/${uuid()}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); @@ -170,7 +170,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -198,7 +198,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -215,7 +215,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -233,7 +233,7 @@ describe('Enroll', () => { const response = await app.inject({ method: HttpMethod.Post, - url: `/items/${item.id}/enroll`, + url: `/api/items/${item.id}/enroll`, }); expect(response.statusCode).toBe(StatusCodes.FORBIDDEN); diff --git a/src/services/maintenance/maintenance.controller.test.ts b/src/services/maintenance/maintenance.controller.test.ts index ccf07ccc18..f72883fdbb 100644 --- a/src/services/maintenance/maintenance.controller.test.ts +++ b/src/services/maintenance/maintenance.controller.test.ts @@ -38,7 +38,7 @@ describe('Maintenance Controller Tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `maintenance/next`, + url: `/api/maintenance/next`, }); expect(response.statusCode).toEqual(StatusCodes.OK); expect(response.json().slug).toEqual(value.slug); @@ -54,7 +54,7 @@ describe('Maintenance Controller Tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `maintenance/next`, + url: `/api/maintenance/next`, }); expect(response.statusCode).toEqual(StatusCodes.OK); expect(response.json()).toBeNull(); diff --git a/src/services/maintenance/maintenance.repository.test.ts b/src/services/maintenance/maintenance.repository.spec.ts similarity index 97% rename from src/services/maintenance/maintenance.repository.test.ts rename to src/services/maintenance/maintenance.repository.spec.ts index 5a020d52f7..c4efbacb76 100644 --- a/src/services/maintenance/maintenance.repository.test.ts +++ b/src/services/maintenance/maintenance.repository.spec.ts @@ -1,4 +1,5 @@ import { add, sub } from 'date-fns'; +import { afterEach, describe, expect, it } from 'vitest'; import { db } from '../../drizzle/db'; import { maintenanceTable } from '../../drizzle/schema'; diff --git a/src/services/member/plugins/export-data/memberExportData.controller.test.ts b/src/services/member/plugins/export-data/memberExportData.controller.test.ts index 4bc451163e..8012218b24 100644 --- a/src/services/member/plugins/export-data/memberExportData.controller.test.ts +++ b/src/services/member/plugins/export-data/memberExportData.controller.test.ts @@ -18,7 +18,7 @@ import { MailerService } from '../../../../plugins/mailer/mailer.service'; import { assertIsDefined } from '../../../../utils/assertions'; import { MEMBER_EXPORT_DATA_ROUTE_PREFIX } from '../../../../utils/config'; -const POST_URL = `/members${MEMBER_EXPORT_DATA_ROUTE_PREFIX}`; +const POST_URL = `/api/members${MEMBER_EXPORT_DATA_ROUTE_PREFIX}`; const uploadDoneMock = jest.fn(async () => console.debug('aws s3 storage upload')); const deleteObjectMock = jest.fn(async () => console.debug('deleteObjectMock')); From 38103cb22bae250d61ec006b2263ac9399727df7 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 12:12:25 +0000 Subject: [PATCH 15/24] fix: disable fail fast --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d6d55512b..8e79709e91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -129,6 +129,7 @@ jobs: test-node: needs: build-node strategy: + fail-fast: false matrix: shard: [1/4, 2/4, 3/4, 4/4] runs-on: ubuntu-latest From 1cfb6ec53cf08c3f4cc9f5e23c66ccd531fa0c61 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 12:26:01 +0000 Subject: [PATCH 16/24] fix: failed tests --- .../plugins/document/document.legacy.test.ts | 6 ++--- .../memberThumbnail.controller.test.ts | 24 +++++++++---------- src/services/tag/tag.controller.test.ts | 6 ++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/services/item/plugins/document/document.legacy.test.ts b/src/services/item/plugins/document/document.legacy.test.ts index de36fab2e4..f5a0d2b5dc 100644 --- a/src/services/item/plugins/document/document.legacy.test.ts +++ b/src/services/item/plugins/document/document.legacy.test.ts @@ -148,7 +148,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload: { name: 'new name' }, }); @@ -193,7 +193,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); // this test a bit how we deal with extra: it replaces existing keys @@ -242,7 +242,7 @@ describe('Document Item tests', () => { const response = await app.inject({ method: HttpMethod.Patch, - url: `/items/${item.id}`, + url: `/api/items/${item.id}`, payload, }); diff --git a/src/services/member/plugins/thumbnail/memberThumbnail.controller.test.ts b/src/services/member/plugins/thumbnail/memberThumbnail.controller.test.ts index 5bf6f9457f..ac91371116 100644 --- a/src/services/member/plugins/thumbnail/memberThumbnail.controller.test.ts +++ b/src/services/member/plugins/thumbnail/memberThumbnail.controller.test.ts @@ -83,7 +83,7 @@ describe('Thumbnail Plugin Tests', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${ThumbnailSize.Small}`, + url: `/api/members/${member.id}/avatar/${ThumbnailSize.Small}`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.body).toBe(MOCK_SIGNED_URL); @@ -97,7 +97,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.body).toBe(MOCK_SIGNED_URL); @@ -112,7 +112,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.body).toBe(MOCK_SIGNED_URL); @@ -127,7 +127,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.OK); expect(response.body).toBe(MOCK_SIGNED_URL); @@ -142,7 +142,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.OK); @@ -158,7 +158,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -176,7 +176,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${guest.id}/avatar/${size}`, + url: `/api/members/${guest.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.NO_CONTENT); @@ -187,7 +187,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${v4()}/avatar/${size}`, + url: `/api/members/${v4()}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); @@ -206,7 +206,7 @@ describe('Thumbnail Plugin Tests', () => { for (const size of Object.values(ThumbnailSize)) { const response = await app.inject({ method: HttpMethod.Get, - url: `members/${member.id}/avatar/${size}`, + url: `/api/members/${member.id}/avatar/${size}`, }); expect(response.statusCode).toBe(StatusCodes.INTERNAL_SERVER_ERROR); @@ -222,7 +222,7 @@ describe('Thumbnail Plugin Tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: 'members/avatar', + url: '/api/members/avatar', payload: form, headers: form.getHeaders(), }); @@ -242,7 +242,7 @@ describe('Thumbnail Plugin Tests', () => { const response = await app.inject({ method: HttpMethod.Post, - url: 'members/avatar', + url: '/api/members/avatar', payload: form2, headers: form2.getHeaders(), }); @@ -261,7 +261,7 @@ describe('Thumbnail Plugin Tests', () => { const res = await app.inject({ method: HttpMethod.Post, - url: 'members/avatar', + url: '/api/members/avatar', payload: form, headers: form.getHeaders(), }); diff --git a/src/services/tag/tag.controller.test.ts b/src/services/tag/tag.controller.test.ts index 38592a9fb1..41534863f8 100644 --- a/src/services/tag/tag.controller.test.ts +++ b/src/services/tag/tag.controller.test.ts @@ -31,7 +31,7 @@ describe('Tag Endpoints', () => { it('Throw for undefined search', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/tags`, + url: `/api/tags`, query: { category: TagCategory.Discipline }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -39,7 +39,7 @@ describe('Tag Endpoints', () => { it('Throw for empty search', async () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/tags`, + url: `/api/tags`, query: { search: '', category: TagCategory.Discipline }, }); expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST); @@ -62,7 +62,7 @@ describe('Tag Endpoints', () => { const response = await app.inject({ method: HttpMethod.Get, - url: `/tags`, + url: `/api/tags`, query: { search: commonString, category: TagCategory.Discipline }, }); From 3d951a996a9c06698df6aa700ad930b35ed2d114 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 12:42:00 +0000 Subject: [PATCH 17/24] fix: page tests --- .../item/plugins/page/page.controller.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/item/plugins/page/page.controller.test.ts b/src/services/item/plugins/page/page.controller.test.ts index a3f0c542b8..3928aa23d1 100644 --- a/src/services/item/plugins/page/page.controller.test.ts +++ b/src/services/item/plugins/page/page.controller.test.ts @@ -43,7 +43,7 @@ async function getAppPort(app: FastifyInstance) { async function expectServerToBeResponsive(app: FastifyInstance) { const result = await app.inject({ method: 'GET', - url: '/api/version', + url: '/health', }); expect(result.statusCode).toEqual(StatusCodes.OK); } @@ -59,8 +59,8 @@ async function connectToItemWs( // connect to ws with yjs specific websocket provider const doc = new Doc(); const provider = new WebsocketProvider( - `ws://localhost:${port}`, - `/api/items/pages/${itemId}/ws${readOnly ? '/read' : ''}`, + `ws://localhost:${port}/api`, + `items/pages/${itemId}/ws${readOnly ? '/read' : ''}`, doc, { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -89,7 +89,7 @@ describe('Page routes tests', () => { unmockAuthenticate(); }); - describe('POST /api/items/pages', () => { + describe('POST /items/pages', () => { it('Throws if signed out', async () => { const payload = FolderItemFactory(); const response = await app.inject({ @@ -125,7 +125,7 @@ describe('Page routes tests', () => { }); }); - describe('GET /api/items/pages/ws', () => { + describe('GET /items/pages/ws', () => { it('Throw if signed out', async () => { const response = await app.inject({ method: HttpMethod.Get, @@ -430,7 +430,7 @@ describe('Page routes tests', () => { }); }); - describe('GET /api/items/pages/ws/read', () => { + describe('GET /items/pages/ws/read', () => { it('Throws if id is incorrect', async () => { const response = await app.inject({ method: HttpMethod.Get, From 9755f3bf1f196eaf5c4bd167bce2eca44cde86ae Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:00:33 +0000 Subject: [PATCH 18/24] fix: migrate more tests to vitest --- ...service.test.ts => action.service.spec.ts} | 2 + ...ry.test.ts => password.repository.spec.ts} | 1 + ...Item.service.getPropertiesForItem.spec.ts} | 180 +++++++++--------- ...tem.service.getPropertiesForItems.spec.ts} | 90 ++++----- .../{service.test.ts => service.spec.ts} | 15 +- .../mentions/chatMention.controller.test.ts | 10 +- ...test.ts => chatMention.repository.spec.ts} | 1 + .../chat/test/chatMentions.expectations.ts | 21 +- ...emAction.requestExport.repository.spec.ts} | 2 + ...Service.test.ts => appItemService.spec.ts} | 17 +- ...rvice.test.ts => document.service.spec.ts} | 17 +- ...e.test.ts => memberAction.service.spec.ts} | 2 + ...ts => memberExportData.repository.spec.ts} | 11 +- .../plugins/export-data/test/fixtures.ts | 1 + ...ce.test.ts => searchIndex.service.spec.ts} | 74 ++++--- 15 files changed, 220 insertions(+), 224 deletions(-) rename src/services/action/{action.service.test.ts => action.service.spec.ts} (98%) rename src/services/auth/plugins/password/{password.repository.test.ts => password.repository.spec.ts} (97%) rename src/services/{authorizedItem.service.getPropertiesForItem.test.ts => authorizedItem.service.getPropertiesForItem.spec.ts} (76%) rename src/services/{authorizedItem.service.getPropertiesForItems.test.ts => authorizedItem.service.getPropertiesForItems.spec.ts} (86%) rename src/services/caching/{service.test.ts => service.spec.ts} (88%) rename src/services/chat/plugins/mentions/{chatMention.repository.test.ts => chatMention.repository.spec.ts} (99%) rename src/services/item/plugins/action/requestExport/{itemAction.requestExport.repository.test.ts => itemAction.requestExport.repository.spec.ts} (99%) rename src/services/item/plugins/app/{appItemService.test.ts => appItemService.spec.ts} (92%) rename src/services/item/plugins/document/{document.service.test.ts => document.service.spec.ts} (94%) rename src/services/member/plugins/action/{memberAction.service.test.ts => memberAction.service.spec.ts} (98%) rename src/services/member/plugins/export-data/{memberExportData.repository.test.ts => memberExportData.repository.spec.ts} (97%) rename src/workers/{searchIndex.service.test.ts => searchIndex.service.spec.ts} (72%) diff --git a/src/services/action/action.service.test.ts b/src/services/action/action.service.spec.ts similarity index 98% rename from src/services/action/action.service.test.ts rename to src/services/action/action.service.spec.ts index 246cc33f30..5576ef0169 100644 --- a/src/services/action/action.service.test.ts +++ b/src/services/action/action.service.spec.ts @@ -1,3 +1,5 @@ +import { expect, it } from 'vitest'; + // TODO // import type { FastifyInstance, FastifyRequest } from 'fastify'; diff --git a/src/services/auth/plugins/password/password.repository.test.ts b/src/services/auth/plugins/password/password.repository.spec.ts similarity index 97% rename from src/services/auth/plugins/password/password.repository.test.ts rename to src/services/auth/plugins/password/password.repository.spec.ts index 80a63fedae..3672092249 100644 --- a/src/services/auth/plugins/password/password.repository.test.ts +++ b/src/services/auth/plugins/password/password.repository.spec.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; import { eq } from 'drizzle-orm'; +import { describe, expect, it } from 'vitest'; import { seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; diff --git a/src/services/authorizedItem.service.getPropertiesForItem.test.ts b/src/services/authorizedItem.service.getPropertiesForItem.spec.ts similarity index 76% rename from src/services/authorizedItem.service.getPropertiesForItem.test.ts rename to src/services/authorizedItem.service.getPropertiesForItem.spec.ts index 7616b37a14..2d7bb04c70 100644 --- a/src/services/authorizedItem.service.getPropertiesForItem.test.ts +++ b/src/services/authorizedItem.service.getPropertiesForItem.spec.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { ItemVisibilityType, PermissionLevel, type PermissionLevelOptions } from '@graasp/sdk'; import { ItemFactory } from '../../test/factories/item.factory'; @@ -37,18 +39,16 @@ const authorizationService = new AuthorizedItemService( describe('getPropertiesForItem', () => { afterEach(() => { - jest.restoreAllMocks(); - jest.clearAllMocks(); + vi.restoreAllMocks(); + vi.clearAllMocks(); }); it('Invalid saved membership', async () => { - jest.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation( - async () => ({ permission: 'anything' }) as unknown as ItemMembershipWithItemAndAccount, - ); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => ({ permission: 'anything' }) as unknown as ItemMembershipWithItemAndAccount, + ); // any other member shouldn't access await expect( @@ -66,8 +66,8 @@ describe('getPropertiesForItem', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should throw', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); - jest.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); await expect( authorizationService.getPropertiesForItem(MOCK_DB, { @@ -81,10 +81,10 @@ describe('getPropertiesForItem', () => { describe('private item shared with read permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => readMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => readMembership, + ); }); it.each([PermissionLevel.Read])('request %s should return', async (permission) => { expect( @@ -111,10 +111,10 @@ describe('getPropertiesForItem', () => { describe('private item shared with write permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => writeMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => writeMembership, + ); }); it.each([PermissionLevel.Read, PermissionLevel.Write])( 'request %s should return', @@ -145,10 +145,10 @@ describe('getPropertiesForItem', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => adminMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockResolvedValue([]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => adminMembership, + ); expect( await authorizationService.getPropertiesForItem(MOCK_DB, { permission, @@ -162,10 +162,10 @@ describe('getPropertiesForItem', () => { describe('public item shared without permission', () => { beforeEach(() => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility]); - jest.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); }); it.each([PermissionLevel.Read])('request %s should return', async (permission) => { expect( @@ -198,12 +198,12 @@ describe('getPropertiesForItem', () => { describe('public item shared with read permission', () => { beforeEach(() => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => readMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => readMembership, + ); }); it.each([PermissionLevel.Read])('request %s should return', async (permission) => { expect( @@ -236,12 +236,12 @@ describe('getPropertiesForItem', () => { describe('public item shared with write permission', () => { beforeEach(() => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => writeMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => writeMembership, + ); }); it.each([PermissionLevel.Read, PermissionLevel.Write])( 'request %s should return', @@ -275,12 +275,12 @@ describe('getPropertiesForItem', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => adminMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => adminMembership, + ); expect( await authorizationService.getPropertiesForItem(MOCK_DB, { @@ -308,10 +308,10 @@ describe('getPropertiesForItem', () => { rejects: MemberCannotAccess, }, ])('request $permission should throw', async ({ permission, rejects }) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [hiddenVisibility]); - jest.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); await expect( authorizationService.getPropertiesForItem(MOCK_DB, { @@ -328,12 +328,12 @@ describe('getPropertiesForItem', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should throw', async ({ permission, rejects }) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => readMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => readMembership, + ); await expect( authorizationService.getPropertiesForItem(MOCK_DB, { @@ -346,12 +346,12 @@ describe('getPropertiesForItem', () => { }); describe('hidden item shared with write permission', () => { beforeEach(() => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => writeMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => writeMembership, + ); }); it.each([PermissionLevel.Read, PermissionLevel.Write])( 'request %s should return', @@ -382,12 +382,12 @@ describe('getPropertiesForItem', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => adminMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => adminMembership, + ); expect( await authorizationService.getPropertiesForItem(MOCK_DB, { @@ -406,10 +406,11 @@ describe('getPropertiesForItem', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should throw', async ({ permission, rejects }) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility, hiddenVisibility]); - jest.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation(async () => null); await expect( authorizationService.getPropertiesForItem(MOCK_DB, { @@ -426,12 +427,13 @@ describe('getPropertiesForItem', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should throw', async ({ permission, rejects }) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility, hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => readMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => readMembership, + ); await expect( authorizationService.getPropertiesForItem(MOCK_DB, { @@ -444,12 +446,13 @@ describe('getPropertiesForItem', () => { }); describe('public and hidden item shared with write permission', () => { beforeEach(() => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility, hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => writeMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => writeMembership, + ); }); it.each([PermissionLevel.Read, PermissionLevel.Write])( 'request %s should return', @@ -480,12 +483,13 @@ describe('getPropertiesForItem', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest - .spyOn(itemVisibilityRepository, 'getByItemPath') - .mockImplementation(async () => [publicVisibility, hiddenVisibility]); - jest - .spyOn(itemMembershipRepository, 'getInherited') - .mockImplementation(async () => adminMembership); + vi.spyOn(itemVisibilityRepository, 'getByItemPath').mockImplementation(async () => [ + publicVisibility, + hiddenVisibility, + ]); + vi.spyOn(itemMembershipRepository, 'getInherited').mockImplementation( + async () => adminMembership, + ); expect( await authorizationService.getPropertiesForItem(MOCK_DB, { diff --git a/src/services/authorizedItem.service.getPropertiesForItems.test.ts b/src/services/authorizedItem.service.getPropertiesForItems.spec.ts similarity index 86% rename from src/services/authorizedItem.service.getPropertiesForItems.test.ts rename to src/services/authorizedItem.service.getPropertiesForItems.spec.ts index 727b6e8815..8f3eaae1aa 100644 --- a/src/services/authorizedItem.service.getPropertiesForItems.test.ts +++ b/src/services/authorizedItem.service.getPropertiesForItems.spec.ts @@ -1,3 +1,5 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { ItemVisibilityType, PermissionLevel, type PermissionLevelOptions } from '@graasp/sdk'; import { ItemFactory } from '../../test/factories/item.factory'; @@ -37,16 +39,16 @@ const authorizationService = new AuthorizedItemService( describe('getPropertiesForItems for one item', () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('Invalid saved membership', async () => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockResolvedValue({ + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockResolvedValue({ data: { [ITEM.id]: { permission: 'anything' } as unknown as ItemMembershipWithItemAndAccount, }, @@ -69,12 +71,12 @@ describe('getPropertiesForItems for one item', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should return error', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: {}, errors: [] }; }); expect( @@ -91,12 +93,12 @@ describe('getPropertiesForItems for one item', () => { describe('private item shared with read permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: readMembership }, errors: [] }; }); }); @@ -131,12 +133,12 @@ describe('getPropertiesForItems for one item', () => { describe('private item shared with write permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: writeMembership }, errors: [] }; }); }); @@ -174,12 +176,12 @@ describe('getPropertiesForItems for one item', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: adminMembership }, errors: [] }; }); expect( @@ -197,12 +199,12 @@ describe('getPropertiesForItems for one item', () => { describe('public item shared without permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: {}, errors: [] }; }); }); @@ -235,12 +237,12 @@ describe('getPropertiesForItems for one item', () => { describe('public item shared with read permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: readMembership }, errors: [] }; }); }); @@ -273,12 +275,12 @@ describe('getPropertiesForItems for one item', () => { describe('public item shared with write permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: writeMembership }, errors: [] }; }); }); @@ -316,12 +318,12 @@ describe('getPropertiesForItems for one item', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: adminMembership }, errors: [] }; }); expect( @@ -343,12 +345,12 @@ describe('getPropertiesForItems for one item', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should return error', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: {}, errors: [] }; }); expect( @@ -369,12 +371,12 @@ describe('getPropertiesForItems for one item', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should return error', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: readMembership }, errors: [] }; }); expect( @@ -391,12 +393,12 @@ describe('getPropertiesForItems for one item', () => { describe('hidden item shared with write permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: writeMembership }, errors: [] }; }); }); @@ -434,12 +436,12 @@ describe('getPropertiesForItems for one item', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: adminMembership }, errors: [] }; }); expect( @@ -461,12 +463,12 @@ describe('getPropertiesForItems for one item', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should return error', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility, hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: {}, errors: [] }; }); expect( @@ -487,12 +489,12 @@ describe('getPropertiesForItems for one item', () => { { permission: PermissionLevel.Write, rejects: MemberCannotAccess }, { permission: PermissionLevel.Admin, rejects: MemberCannotAccess }, ])('request $permission should return error', async ({ permission, rejects }) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility, hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: readMembership }, errors: [] }; }); expect( @@ -509,12 +511,12 @@ describe('getPropertiesForItems for one item', () => { describe('public and hidden item shared with write permission', () => { beforeEach(() => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility, hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: writeMembership }, errors: [] }; }); }); @@ -552,12 +554,12 @@ describe('getPropertiesForItems for one item', () => { it.each([PermissionLevel.Read, PermissionLevel.Write, PermissionLevel.Admin])( 'request %s should return', async (permission) => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [publicVisibility, hiddenVisibility] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation(async () => { return { data: { [ITEM.id]: adminMembership }, errors: [] }; }); expect( @@ -581,11 +583,11 @@ describe('getPropertiesForItems for many items', () => { const sharedMembership = buildSharedMembership(PermissionLevel.Write, SHARED_ITEM); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('Public item & Shared write item', async () => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockImplementation(async () => ({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockImplementation(async () => ({ data: { [PUBLIC_ITEM.id]: [ItemVisibilityFactory({ type: ItemVisibilityType.Public, item: ITEM })], [SHARED_ITEM.id]: [], @@ -593,12 +595,12 @@ describe('getPropertiesForItems for many items', () => { errors: [], })); - jest - .spyOn(itemMembershipRepository, 'getInheritedMany') - .mockImplementation(async (_db, _items, _memberId) => ({ + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockImplementation( + async (_db, _items, _memberId) => ({ data: { [SHARED_ITEM.id]: sharedMembership }, errors: [], - })); + }), + ); // shared member can read both items const { itemMemberships: result } = await authorizationService.getPropertiesForItems(MOCK_DB, { permission: PermissionLevel.Read, @@ -632,15 +634,15 @@ describe('getPropertiesForItems for many items', () => { describe('getPropertiesForItems for no items', () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('Should return empty data', async () => { - jest.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ + vi.spyOn(itemVisibilityRepository, 'getManyForMany').mockResolvedValue({ data: { [ITEM.id]: [] }, errors: [], }); - jest.spyOn(itemMembershipRepository, 'getInheritedMany').mockResolvedValue({ + vi.spyOn(itemMembershipRepository, 'getInheritedMany').mockResolvedValue({ data: {}, errors: [], }); diff --git a/src/services/caching/service.test.ts b/src/services/caching/service.spec.ts similarity index 88% rename from src/services/caching/service.test.ts rename to src/services/caching/service.spec.ts index 86a24fcfa0..a36fb16c81 100644 --- a/src/services/caching/service.test.ts +++ b/src/services/caching/service.spec.ts @@ -1,14 +1,13 @@ import { faker } from '@faker-js/faker'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; -import type { FastifyInstance } from 'fastify'; - -import build from '../../../test/app'; +import { MOCK_LOGGER } from '../../../test/app.vitest'; import { FILE_SERVICE_URLS_CACHING_DI_KEY } from '../../di/constants'; +import { registerDependencies } from '../../di/container'; import { resolveDependency } from '../../di/utils'; import { CachingService } from './service'; describe('CachingService Tests', () => { - let app: FastifyInstance; let cache: CachingService; const MOCKED_PATH = '1b304da5_b342_46e8_a484_1712d5209e43'; const getRandomUrl = async () => faker.internet.url(); // returns a new URL at each run @@ -20,17 +19,13 @@ describe('CachingService Tests', () => { }; beforeAll(async () => { - ({ app } = await build()); + registerDependencies(MOCK_LOGGER); cache = resolveDependency(FILE_SERVICE_URLS_CACHING_DI_KEY); }); - afterAll(async () => { - app.close(); - }); - afterEach(async () => { await resetValidations(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('Empty cache returns given URL once, but then returns the cached URL', async () => { diff --git a/src/services/chat/plugins/mentions/chatMention.controller.test.ts b/src/services/chat/plugins/mentions/chatMention.controller.test.ts index dd7e98d933..a9fb901662 100644 --- a/src/services/chat/plugins/mentions/chatMention.controller.test.ts +++ b/src/services/chat/plugins/mentions/chatMention.controller.test.ts @@ -19,7 +19,6 @@ import type { MinimalMember } from '../../../../types'; import { assertIsDefined } from '../../../../utils/assertions'; import { ITEMS_ROUTE_PREFIX } from '../../../../utils/config'; import { MemberCannotAccessMention } from '../../errors'; -import { expectFullChatMentions } from '../../test/chatMentions.expectations'; // create item, chat messages from another member and members // as well as mentions of actor @@ -88,7 +87,14 @@ describe('Chat Mention tests', () => { const result = await response.json(); // check response value - expectFullChatMentions(result, chatMentions); + expect(result).toHaveLength(chatMentions.length); + for (const m of result) { + const correctMention = chatMentions.find(({ id }) => id === m.id)!; + + expect(m.message.id).toEqual(correctMention.messageId); + expect(m.message.creatorId).toBeDefined(); + expect(m.account.id).toEqual(correctMention.accountId); + } }); }); }); diff --git a/src/services/chat/plugins/mentions/chatMention.repository.test.ts b/src/services/chat/plugins/mentions/chatMention.repository.spec.ts similarity index 99% rename from src/services/chat/plugins/mentions/chatMention.repository.test.ts rename to src/services/chat/plugins/mentions/chatMention.repository.spec.ts index 53a3bdbd7e..bc51999e9e 100644 --- a/src/services/chat/plugins/mentions/chatMention.repository.test.ts +++ b/src/services/chat/plugins/mentions/chatMention.repository.spec.ts @@ -1,5 +1,6 @@ import { eq } from 'drizzle-orm/sql'; import { v4 } from 'uuid'; +import { describe, expect, it } from 'vitest'; import { MentionStatus } from '@graasp/sdk'; diff --git a/src/services/chat/test/chatMentions.expectations.ts b/src/services/chat/test/chatMentions.expectations.ts index 69e5e46990..e3593491e1 100644 --- a/src/services/chat/test/chatMentions.expectations.ts +++ b/src/services/chat/test/chatMentions.expectations.ts @@ -1,8 +1,6 @@ -import type { - ChatMentionRaw, - ChatMentionWithMessage, - ChatMentionWithMessageAndCreator, -} from '../../../drizzle/types'; +import { expect } from 'vitest'; + +import type { ChatMentionRaw, ChatMentionWithMessageAndCreator } from '../../../drizzle/types'; export const expectFullChatMentions = ( mentions: ChatMentionWithMessageAndCreator[], @@ -18,19 +16,6 @@ export const expectFullChatMentions = ( } }; -export const expectChatMentions = ( - mentions: ChatMentionWithMessage[], - correctMentions: ChatMentionRaw[], -) => { - expect(mentions).toHaveLength(correctMentions.length); - for (const m of mentions) { - const correctMention = correctMentions.find(({ id }) => id === m.id)!; - - expect(m.message.id).toEqual(correctMention.messageId); - expect(m.message.creatorId).toBeDefined(); - } -}; - export const expectRawChatMentions = ( mentions: ChatMentionRaw[], correctMentions: ChatMentionRaw[], diff --git a/src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.test.ts b/src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.spec.ts similarity index 99% rename from src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.test.ts rename to src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.spec.ts index 2a8325361b..aa54568cc7 100644 --- a/src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.test.ts +++ b/src/services/item/plugins/action/requestExport/itemAction.requestExport.repository.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { ItemType } from '@graasp/sdk'; import { seedFromJson } from '../../../../../../test/mocks/seed'; diff --git a/src/services/item/plugins/app/appItemService.test.ts b/src/services/item/plugins/app/appItemService.spec.ts similarity index 92% rename from src/services/item/plugins/app/appItemService.test.ts rename to src/services/item/plugins/app/appItemService.spec.ts index 17cbe86d13..24b4d4ac21 100644 --- a/src/services/item/plugins/app/appItemService.test.ts +++ b/src/services/item/plugins/app/appItemService.spec.ts @@ -1,8 +1,9 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { AppItemFactory, FolderItemFactory, ItemType } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { MemberFactory } from '../../../../../test/factories/member.factory'; import { db } from '../../../../drizzle/db'; import { type ItemRaw } from '../../../../drizzle/types'; @@ -50,12 +51,12 @@ const MOCK_MEMBER = MemberFactory(); describe('App Service', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('postWithOptions', () => { it('set correct default values for type, extra and settings', async () => { - const itemServicePostMock = jest + const itemServicePostMock = vi .spyOn(ItemService.prototype, 'post') .mockImplementation(async () => { return {} as ItemRaw; @@ -83,7 +84,7 @@ describe('App Service', () => { }); }); it('set defined values', async () => { - const itemServicePostMock = jest + const itemServicePostMock = vi .spyOn(ItemService.prototype, 'post') .mockImplementation(async () => { return {} as ItemRaw; @@ -127,9 +128,9 @@ describe('App Service', () => { ).rejects.toThrow(); }); it('patch item settings', async () => { - jest.spyOn(itemRepository, 'getOneOrThrow').mockResolvedValue(MOCK_ITEM); + vi.spyOn(itemRepository, 'getOneOrThrow').mockResolvedValue(MOCK_ITEM); - const itemServicePatchMock = jest + const itemServicePatchMock = vi .spyOn(ItemService.prototype, 'patch') .mockImplementation(async () => { return MOCK_ITEM; @@ -148,7 +149,7 @@ describe('App Service', () => { }); }); it('patch many properties without changing url', async () => { - const itemServicePatchMock = jest + const itemServicePatchMock = vi .spyOn(ItemService.prototype, 'patch') .mockImplementation(async () => { return MOCK_ITEM; @@ -172,7 +173,7 @@ describe('App Service', () => { }); it('Cannot update not found item given id', async () => { - jest.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { + vi.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { throw new Error(); }); diff --git a/src/services/item/plugins/document/document.service.test.ts b/src/services/item/plugins/document/document.service.spec.ts similarity index 94% rename from src/services/item/plugins/document/document.service.test.ts rename to src/services/item/plugins/document/document.service.spec.ts index df8a3f1895..c43dbf8630 100644 --- a/src/services/item/plugins/document/document.service.test.ts +++ b/src/services/item/plugins/document/document.service.spec.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { DocumentItemExtraFlavor, @@ -7,7 +8,7 @@ import { ItemType, } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../../../../test/app'; +import { MOCK_LOGGER } from '../../../../../test/app.vitest'; import { db } from '../../../../drizzle/db'; import type { ItemRaw } from '../../../../drizzle/types'; import type { MinimalMember } from '../../../../types'; @@ -52,12 +53,12 @@ const documentService = new DocumentItemService( describe('Document Service', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('postWithOptions', () => { it('set correct default values for type and extra', async () => { - const itemServicePostMock = jest + const itemServicePostMock = vi .spyOn(ItemService.prototype, 'post') .mockImplementation(async () => { return {} as ItemRaw; @@ -85,7 +86,7 @@ describe('Document Service', () => { }); }); it('sanitize content', async () => { - const itemServicePostMock = jest + const itemServicePostMock = vi .spyOn(ItemService.prototype, 'post') .mockImplementation(async () => { return {} as ItemRaw; @@ -113,7 +114,7 @@ describe('Document Service', () => { }); }); it('set defined values', async () => { - const itemServicePostMock = jest + const itemServicePostMock = vi .spyOn(ItemService.prototype, 'post') .mockImplementation(async () => { return {} as ItemRaw; @@ -161,7 +162,7 @@ describe('Document Service', () => { ).rejects.toThrow(); }); it('sanitize content', async () => { - const itemServicePatchMock = jest + const itemServicePatchMock = vi .spyOn(ItemService.prototype, 'patch') .mockImplementation(async () => { return MOCK_ITEM; @@ -188,7 +189,7 @@ describe('Document Service', () => { }); }); it('patch many properties', async () => { - const itemServicePatchMock = jest + const itemServicePatchMock = vi .spyOn(ItemService.prototype, 'patch') .mockImplementation(async () => { return MOCK_ITEM; @@ -222,7 +223,7 @@ describe('Document Service', () => { }); it('Cannot update not found item given id', async () => { - jest.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { + vi.spyOn(itemRepository, 'getOneOrThrow').mockImplementation(() => { throw new Error(); }); diff --git a/src/services/member/plugins/action/memberAction.service.test.ts b/src/services/member/plugins/action/memberAction.service.spec.ts similarity index 98% rename from src/services/member/plugins/action/memberAction.service.test.ts rename to src/services/member/plugins/action/memberAction.service.spec.ts index b01ec2d379..b2c1adb66b 100644 --- a/src/services/member/plugins/action/memberAction.service.test.ts +++ b/src/services/member/plugins/action/memberAction.service.spec.ts @@ -1,3 +1,5 @@ +import { expect, it } from 'vitest'; + // import build, { MOCK_LOGGER, clearDatabase } from '../../../../../../test/app'; // import { resolveDependency } from '../../../../../di/utils'; // import { asDefined } from '../../../../../utils/assertions'; diff --git a/src/services/member/plugins/export-data/memberExportData.repository.test.ts b/src/services/member/plugins/export-data/memberExportData.repository.spec.ts similarity index 97% rename from src/services/member/plugins/export-data/memberExportData.repository.test.ts rename to src/services/member/plugins/export-data/memberExportData.repository.spec.ts index 052ef39537..557cdf1c86 100644 --- a/src/services/member/plugins/export-data/memberExportData.repository.test.ts +++ b/src/services/member/plugins/export-data/memberExportData.repository.spec.ts @@ -1,4 +1,5 @@ -import { clearDatabase } from '../../../../../test/app'; +import { afterEach, describe, it, vi } from 'vitest'; + import { seedFromJson } from '../../../../../test/mocks/seed'; import { db } from '../../../../drizzle/db'; import { assertIsDefined } from '../../../../utils/assertions'; @@ -38,15 +39,9 @@ describe('DataMember Export', () => { // }); afterEach(async () => { - jest.clearAllMocks(); - await clearDatabase(db); + vi.clearAllMocks(); }); - // afterAll(async () => { - // await clearDatabase(db); - // app.close(); - // }); - describe('Actions', () => { it('get all Actions for the member', async () => { // save for exporting actor diff --git a/src/services/member/plugins/export-data/test/fixtures.ts b/src/services/member/plugins/export-data/test/fixtures.ts index a0f3c9388d..e1ae222e66 100644 --- a/src/services/member/plugins/export-data/test/fixtures.ts +++ b/src/services/member/plugins/export-data/test/fixtures.ts @@ -1,5 +1,6 @@ import { Ajv, type JSONSchemaType, type Schema } from 'ajv'; import fastJson from 'fast-json-stringify'; +import { expect } from 'vitest'; export const expectNoLeaksAndEquality = ( results: T[], diff --git a/src/workers/searchIndex.service.test.ts b/src/workers/searchIndex.service.spec.ts similarity index 72% rename from src/workers/searchIndex.service.test.ts rename to src/workers/searchIndex.service.spec.ts index 7f7cc4d316..1d80554914 100644 --- a/src/workers/searchIndex.service.test.ts +++ b/src/workers/searchIndex.service.spec.ts @@ -1,10 +1,10 @@ -import { expect, jest } from '@jest/globals'; import { EnqueuedTask, Index, MeiliSearch, Task, TaskStatus } from 'meilisearch'; import { v4 } from 'uuid'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { type IndexItem } from '@graasp/sdk'; -import { MOCK_LOGGER } from '../../test/app'; +import { MOCK_LOGGER } from '../../test/app.vitest'; import { ItemFactory } from '../../test/factories/item.factory'; import { db } from '../drizzle/db'; import { ItemPublishedWithItemWithCreator } from '../drizzle/types'; @@ -34,25 +34,25 @@ const mockItemPublished = ({ const mockIndex = { uid: ACTIVE_INDEX, - addDocuments: jest.fn(() => { + addDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - waitForTask: jest.fn(() => { + waitForTask: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), - waitForTasks: jest.fn(() => { + waitForTasks: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), - deleteDocuments: jest.fn(() => { + deleteDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - deleteAllDocuments: jest.fn(() => { + deleteAllDocuments: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - updateSettings: jest.fn(() => { + updateSettings: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), -} as unknown as jest.Mocked>; +} as unknown as Index; const fakeClient = new MeiliSearch({ host: 'fake', @@ -60,30 +60,28 @@ const fakeClient = new MeiliSearch({ httpClient: () => Promise.resolve(), }); -jest.spyOn(fakeClient, 'getIndex').mockResolvedValue(mockIndex); -jest.spyOn(fakeClient, 'index').mockReturnValue({ - updateFaceting: jest.fn(async () => { +vi.spyOn(fakeClient, 'getIndex').mockResolvedValue(mockIndex); +vi.spyOn(fakeClient, 'index').mockReturnValue({ + updateFaceting: vi.fn(async () => { return { taskUid: '1' } as unknown as EnqueuedTask; }), - updateSettings: jest.fn(() => { + updateSettings: vi.fn(() => { return Promise.resolve({ taskUid: '1' } as unknown as EnqueuedTask); }), - waitForTask: jest.fn(() => { + waitForTask: vi.fn(() => { return Promise.resolve({ status: TaskStatus.TASK_SUCCEEDED } as Task); }), } as never); -jest - .spyOn(fakeClient, 'swapIndexes') - .mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); -jest - .spyOn(fakeClient, 'waitForTask') - .mockResolvedValue({ status: TaskStatus.TASK_SUCCEEDED } as Task); +vi.spyOn(fakeClient, 'swapIndexes').mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); +vi.spyOn(fakeClient, 'waitForTask').mockResolvedValue({ + status: TaskStatus.TASK_SUCCEEDED, +} as Task); -jest.spyOn(db, 'transaction').mockImplementation(async (fn) => await fn({} as never)); +vi.spyOn(db, 'transaction').mockImplementation(async (fn) => await fn({} as never)); const itemPublishedRepository = new ItemPublishedRepository(); const meilisearchWrapper = { - index: jest.fn(), + index: vi.fn(), } as unknown as MeiliSearchWrapper; const meilisearch = new SearchIndexService( @@ -95,19 +93,18 @@ const meilisearch = new SearchIndexService( describe('SearchIndexService', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('builds index', () => { it('index all items', async () => { - jest.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [mockIndex], total: 1 }); - jest.spyOn(fakeClient, 'createIndex').mockResolvedValue({ taskUid: 1 } as EnqueuedTask); + vi.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [mockIndex], total: 1 }); + vi.spyOn(fakeClient, 'createIndex').mockResolvedValue({ taskUid: 1 } as EnqueuedTask); const publishedItemsInDb = Array.from({ length: 13 }, (_, index) => { return mockItemPublished({ id: index.toString(), path: index.toString() }); }); - jest - .spyOn(itemPublishedRepository, 'getPaginatedItems') - .mockImplementation(async (_db, page, _) => { + vi.spyOn(itemPublishedRepository, 'getPaginatedItems').mockImplementation( + async (_db, page, _) => { if (page === 1) { return [publishedItemsInDb.slice(0, 10), publishedItemsInDb.length]; } else if (page === 2) { @@ -115,8 +112,9 @@ describe('SearchIndexService', () => { } else { throw new Error(); } - }); - const indexSpy = jest + }, + ); + const indexSpy = vi .spyOn(meilisearchWrapper, 'index') .mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); @@ -129,15 +127,14 @@ describe('SearchIndexService', () => { it('index all items when active index does not exist', async () => { // throw error when getting active index // should create index and start indexing - jest.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [], total: 0 }); - jest.spyOn(fakeClient, 'createIndex').mockResolvedValue({ taskUid: 1 } as EnqueuedTask); + vi.spyOn(fakeClient, 'getIndexes').mockResolvedValue({ results: [], total: 0 }); + vi.spyOn(fakeClient, 'createIndex').mockResolvedValue({ taskUid: 1 } as EnqueuedTask); const publishedItemsInDb = Array.from({ length: 13 }, (_, index) => { return mockItemPublished({ id: index.toString(), path: index.toString() }); }); - jest - .spyOn(itemPublishedRepository, 'getPaginatedItems') - .mockImplementation(async (_db, page, _) => { + vi.spyOn(itemPublishedRepository, 'getPaginatedItems').mockImplementation( + async (_db, page, _) => { if (page === 1) { return [publishedItemsInDb.slice(0, 10), publishedItemsInDb.length]; } else if (page === 2) { @@ -145,8 +142,9 @@ describe('SearchIndexService', () => { } else { throw new Error(); } - }); - const indexSpy = jest + }, + ); + const indexSpy = vi .spyOn(meilisearchWrapper, 'index') .mockResolvedValue({ taskUid: '1' } as unknown as EnqueuedTask); @@ -158,7 +156,7 @@ describe('SearchIndexService', () => { }); it('throw for unexpected error on getting index', async () => { const error = new Error('some error'); - jest.spyOn(fakeClient, 'getIndex').mockRejectedValue(error); + vi.spyOn(fakeClient, 'getIndex').mockRejectedValue(error); await expect(meilisearch.buildIndex({ pageSize: 10 })).rejects.toThrow(error); }); From 8252cc110edfe39df1ebc3c2c9d98657d87d6ca3 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:06:21 +0000 Subject: [PATCH 19/24] fix: run build in parallel --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e79709e91..516792864c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ env: GEOLOCATION_API_HOST: http://localhost:12345 jobs: - build-node: + build-unit: runs-on: ubuntu-latest # Service containers to run with `container-job` @@ -127,7 +127,6 @@ jobs: run: yarn vitest test-node: - needs: build-node strategy: fail-fast: false matrix: From 4200f6820eb89b18a6876b6462ea8337ebdea5b9 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:18:57 +0000 Subject: [PATCH 20/24] chore: add summary job --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 516792864c..411ae35cb3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -203,3 +203,12 @@ jobs: - name: yarn test run: yarn test:ci --shard=${{ matrix.shard }} + + summary: + runs-on: ubuntu-latest + needs: + - test-node + - build-unit + steps: + - name: summary + run: echo "Done!" From 5c5f832fc369b6d17b89007ea3e342c898d43550 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:22:29 +0000 Subject: [PATCH 21/24] fix: empty commit From dfdd972810aacd5189f79c47ec3e19c98157bd44 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:24:56 +0000 Subject: [PATCH 22/24] fix: rename check --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 411ae35cb3..92a5291116 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -204,11 +204,11 @@ jobs: - name: yarn test run: yarn test:ci --shard=${{ matrix.shard }} - summary: + test-success: runs-on: ubuntu-latest needs: - test-node - build-unit steps: - - name: summary + - name: Summary run: echo "Done!" From bc46e8a0168a80c32ce717a791382972ec6c7a40 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 12 Nov 2025 13:31:31 +0000 Subject: [PATCH 23/24] fix: remove summary --- .github/workflows/test.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92a5291116..da837bfe00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,7 +128,8 @@ jobs: test-node: strategy: - fail-fast: false + # NOTE: switch this on if you want to have the result of all the shards to fix more tests at once. + # fail-fast: false matrix: shard: [1/4, 2/4, 3/4, 4/4] runs-on: ubuntu-latest @@ -203,12 +204,3 @@ jobs: - name: yarn test run: yarn test:ci --shard=${{ matrix.shard }} - - test-success: - runs-on: ubuntu-latest - needs: - - test-node - - build-unit - steps: - - name: Summary - run: echo "Done!" From f7c5cc265ae84bdc9efb575a982b35505c7de77f Mon Sep 17 00:00:00 2001 From: spaenleh Date: Wed, 19 Nov 2025 09:16:35 +0000 Subject: [PATCH 24/24] fix: exclude spec files from docker --- .dockerignore | 1 + tsconfig.build.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index e9d5c668ea..29b6b93f84 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ test tmp **/test **/*.test.ts +**/*.spec.ts types docker API Collections diff --git a/tsconfig.build.json b/tsconfig.build.json index dbac15fc5c..d3bd42eaad 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -10,6 +10,7 @@ "test", "src/**/test/*.ts", "src/**/*.test.ts", + "src/**/*.spec.ts", "src/**/test/*.ts", "src/**/test/fixtures/*.ts" ]