diff --git a/__tests__/ai/__snapshots__/pull.test.js.snap b/__tests__/ai/__snapshots__/pull.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f90daf34947ac1a9ef79403ab267da7f7ff658da --- /dev/null +++ b/__tests__/ai/__snapshots__/pull.test.js.snap @@ -0,0 +1,40 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ai pull model > given a user tries to access > should respond with a proper body 1`] = ` +{ + "message": "Access Forbidden", +} +`; + +exports[`ai pull model > given no jwt sended > should respond with a proper body 1`] = ` +{ + "message": "No access token found. Access denied.", +} +`; + +exports[`ai pull model > given no matching model found > should respond with a proper body 1`] = ` +{ + "message": "pull model manifest: file does not exist", +} +`; + +exports[`ai pull model > given no valid jwt sended > should respond with a proper body 1`] = ` +{ + "message": "Access token is no longer valid. Access denied.", +} +`; + +exports[`ai pull model > given required fields are missing > should respond with a proper body 1`] = ` +{ + "message": "Validation errors. Please check the error messages.", + "validationErrors": { + "model": "Required", + }, +} +`; + +exports[`ai pull model > given the inputs are valid > should respond with a proper body 1`] = ` +{ + "status": "success", +} +`; diff --git a/__tests__/ai/pull.test.js b/__tests__/ai/pull.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8ef59fcf74eda454e715e363d9757caa5a75c517 --- /dev/null +++ b/__tests__/ai/pull.test.js @@ -0,0 +1,187 @@ +// import vitest, supertest & app +import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; +import supertest from "supertest"; +import app from "../../app.js"; +import jwt from 'jsonwebtoken'; + +// set route +const ROUTE = '/ai/models'; +// prepare response of each test +let response; + +// ############################ +// OBJECTS +// ############################ +const mockedVals = vi.hoisted(() => { + return { + foundUser: { + _id: '66a29da2942b3eb', + username: 'snoopy', + name: 'My User', + email: 'user@mail.local', + verified: true, + role: 4, + createdAt: '2024-07 - 25T18: 46: 58.982Z', + updatedAt: '2024-07 - 25T18: 46: 58.982Z', + __v: 0, + fullname: '', + id: '66a29da2942b3ebcaf047f07' + }, + validInput: { + model: 'llama3' + } + }; +}); + +// ############################ +// MOCKS +// ############################ +import * as dbService from '../../utils/handleDB.js'; +// mock dbService +vi.mock('../../utils/handleDB.js', async (importOriginal) => { + return { + ...await importOriginal(), + dbConnection: vi.fn(() => 'mocked'), + findOneRecord: vi.fn(() => mockedVals.foundUser), + }; +}); + +// import AI Service +import * as aiService from '../../utils/handleAI.js'; +// mock aiService +vi.mock('../../utils/handleAI.js', async (importOriginal) => { + return { + ...await importOriginal(), + aiInstallModel: vi.fn().mockImplementation(() => { return { status: 'success' }; }), + }; +}); + +// ############################ +// TESTS +// ############################ + +describe('ai pull model', () => { + const _jwt = (id, role) => { + return jwt.sign({ id, role }, process.env.JWT_SECRET_KEY, { expiresIn: process.env.JWT_TTL }); + }; + + describe('given the inputs are valid', async () => { + beforeAll(async () => { + response = await supertest(app) + .put(ROUTE) + .set('Authorization', `Bearer ${_jwt(mockedVals.foundUser.id, mockedVals.foundUser.role)}`) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(200); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + + // ############################ + + describe('given no matching model found', () => { + beforeAll(async () => { + const input = { ...mockedVals.validInput, model: 'unknownModel' }; + + let error = new Error("pull model manifest: file does not exist"); + error.name = 'ResponseError'; + error.status = 500; + aiService.aiInstallModel.mockImplementation(() => { throw error; }); + + response = await supertest(app) + .put(ROUTE) + .set('Authorization', `Bearer ${_jwt(mockedVals.foundUser.id, mockedVals.foundUser.role)}`) + .send(input); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(500); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given required fields are missing', () => { + beforeAll(async () => { + const { model, ...input } = mockedVals.validInput; + + response = await supertest(app) + .put(ROUTE) + .set('Authorization', `Bearer ${_jwt(mockedVals.foundUser.id, mockedVals.foundUser.role)}`) + .send(input); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(400); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given a user tries to access', () => { + beforeAll(async () => { + + dbService.findOneRecord.mockImplementationOnce(async () => { + return { ...mockedVals.foundUser, role: 0 }; + }); + + response = await supertest(app) + .put(ROUTE) + .set('Authorization', `Bearer ${_jwt(mockedVals.foundUser.id, 0)}`) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(403); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given no valid jwt sended', () => { + beforeAll(async () => { + response = await supertest(app) + .put(ROUTE) + .set('Authorization', `Bearer invalid`) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(403); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); + + // ############################ + + describe('given no jwt sended', () => { + beforeAll(async () => { + response = await supertest(app) + .put(ROUTE) + .send(mockedVals.validInput); + }); + + it('should return a proper status code status', () => { + expect(response.status).toBe(401); + }); + it('should respond with a proper body', () => { + expect(response.body).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/manualREST/ollama.rest b/__tests__/manualREST/ollama.rest index c3484fb5713daa15a99b1d134cfe36af51fb9f7e..043dd5aa89e1f8ba6d00561e7a42beec84727103 100644 --- a/__tests__/manualREST/ollama.rest +++ b/__tests__/manualREST/ollama.rest @@ -64,7 +64,7 @@ Content-Type: application/json ### pull a specific model # @name pull_model -POST {{host}}/ai/models/pull +PUT {{host}}/ai/models Authorization: Bearer {{token}} Accept: application/json Content-Type: application/json diff --git a/logs/__tests__/ai/pull.test.js b/logs/__tests__/ai/pull.test.js deleted file mode 100644 index 8792df059d1d885f94581b9de2695b2c63bb786d..0000000000000000000000000000000000000000 --- a/logs/__tests__/ai/pull.test.js +++ /dev/null @@ -1,156 +0,0 @@ -// import vitest, supertest & app -import { vi, beforeAll, beforeEach, describe, expect, expectTypeOf, test, it, afterEach } from 'vitest'; -import supertest from "supertest"; -import app from "../../app.js"; -// ignore expiration of the (self-signed) certificate -process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; -// set timeout -const BEFORE_ALL_TIMEOUT = 30000; // 30 sec -// set route -const ROUTE = '/ai/models/pull'; -// prepare response of each test -let response; - -// ############################ -// OBJECTS -// ############################ - -const noModelFoundResponse = { - code: 500, - message: "pull model manifest: file does not exist", - data: {} -}; -// ############################ -// MOCKS -// ############################ -// import PocketBase Service -import * as pbService from '../../utils/pocketbase/handlePocketBase.js'; -// mock pbService -vi.mock('../../utils/pocketbase/handlePocketBase.js', async (importOriginal) => { - return { - ...await importOriginal(), - pbVerifyAccessToken: vi.fn().mockImplementation((req, res, next) => { - next(); - }), - gateKeeper: vi.fn().mockImplementation((req, res, next) => { - return next(); - }) - }; -}); - -// import AI Service -import * as aiService from '../../utils/handleAI.js'; -// mock aiService -vi.mock('../../utils/handleAI.js', async (importOriginal) => { - return { - ...await importOriginal(), - aiInstallModel: vi.fn().mockImplementation(() => { return { status: 'success' }; }), - }; -}); - -// ############################ -// TESTS -// ############################ - -describe('ai pull model', () => { - describe('given the inputs are valid', async () => { - response = await supertest(app) - .post(ROUTE) - .set('Authorization', 'Bearer 123valid') - .send({ model: 'validModelName' }); - it('should have called the gateKeeper', () => { - expect(pbService.gateKeeper).toHaveBeenCalledTimes(1); - }); - it('should return a proper status code', () => { - expect(response.status).toBe(200); - }); - it('should respond with a proper message', () => { - expect(response.body).toEqual({ status: 'success' }); - }); - }); - - // ############################ - - describe('given no valid JWT sent', () => { - beforeAll(async () => { - pbService.pbVerifyAccessToken.mockImplementationOnce((req, res, next) => { - return res.status(403).json({ message: 'You are not logged in.' }); - }); - - response = await supertest(app) - .post(ROUTE) - .send({ model: 'validModelName' }); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(403); - }); - it('should respond with a proper message', () => { - expect(response.body.message).toEqual('You are not logged in.'); - }); - }); - - - // ############################ - - describe('given no access granted', () => { - beforeAll(async () => { - pbService.gateKeeper.mockImplementationOnce((req, res, next) => { - return res.status(403).json({ message: 'Access Forbidden' }); - }); - - response = await supertest(app) - .post(ROUTE) - .set('Authorization', 'Bearer 123valid'); - }, BEFORE_ALL_TIMEOUT); - - it('should return a proper status code', () => { - expect(response.status).toBe(403); - }); - it('should respond with a proper message', () => { - expect(response.body.message).toEqual('Access Forbidden'); - }); - }); - - // ############################ - - describe('given no model name sent', () => { - beforeAll(async () => { - response = await supertest(app) - .post(ROUTE) - .set('Authorization', 'Bearer 123valid'); - }, BEFORE_ALL_TIMEOUT); - it('should return a proper status code', () => { - expect(response.status).toBe(400); - }); - it('should respond with a proper message', () => { - expect(response.body.validationErrors.model).toEqual('Required'); - }); - }); - - // ############################ - - describe('given no matching model found', () => { - beforeAll(async () => { - let error = new Error('pull model manifest: file does not exist'); - error.name = 'ResponseError'; - error.response = noModelFoundResponse; - error.status = 500; - aiService.aiInstallModel.mockImplementation(() => { throw error; }); - - response = await supertest(app) - .post(ROUTE) - .set('Authorization', 'Bearer 123valid') - .send({ model: 'invalidModelName' }); - }, BEFORE_ALL_TIMEOUT); - it('should force aiInstallModel to throw an error', () => { - expect(aiService.aiInstallModel).toThrowError(); - }); - it('should return a proper status code', () => { - expect(response.status).toBe(500); - }); - it('should respond with a proper message', () => { - expect(response.body.message).toEqual('pull model manifest: file does not exist'); - }); - }); - -}); \ No newline at end of file diff --git a/routes/ai.js b/routes/ai.js index 105b7004bedd94c8f934b406d4636ccb25bffeda..bfbbecdd3f08044977c66acb16a97cdf66387e68 100644 --- a/routes/ai.js +++ b/routes/ai.js @@ -50,7 +50,7 @@ router.post('/model', verifyAccessToken, validate(getModelSchema), getModel); * * @return {string} installation response */ -router.post('/models/pull', verifyAccessToken, gateKeeper, validate(installModelSchema), installModel); +router.put('/models', verifyAccessToken, gateKeeper, validate(installModelSchema), installModel); /**