diff --git a/backend/src/controller/chatController.ts b/backend/src/controller/chatController.ts index 8571064cf..a9388c6e5 100644 --- a/backend/src/controller/chatController.ts +++ b/backend/src/controller/chatController.ts @@ -8,7 +8,6 @@ import { import { OpenAiAddInfoToChatHistoryRequest } from '@src/models/api/OpenAiAddInfoToChatHistoryRequest'; import { OpenAiChatRequest } from '@src/models/api/OpenAiChatRequest'; import { OpenAiClearRequest } from '@src/models/api/OpenAiClearRequest'; -import { OpenAiGetHistoryRequest } from '@src/models/api/OpenAiGetHistoryRequest'; import { DefenceReport, ChatHttpResponse, @@ -359,16 +358,6 @@ function addErrorToChatHistory( }); } -function handleGetChatHistory(req: OpenAiGetHistoryRequest, res: Response) { - const level: number | undefined = req.query.level as number | undefined; - if (level !== undefined) { - res.send(req.session.levelState[level].chatHistory); - } else { - res.status(400); - res.send('Missing level'); - } -} - function handleAddInfoToChatHistory( req: OpenAiAddInfoToChatHistoryRequest, res: Response @@ -407,9 +396,4 @@ function handleClearChatHistory(req: OpenAiClearRequest, res: Response) { } } -export { - handleChatToGPT, - handleGetChatHistory, - handleAddInfoToChatHistory as handleAddInfoToChatHistory, - handleClearChatHistory, -}; +export { handleChatToGPT, handleAddInfoToChatHistory, handleClearChatHistory }; diff --git a/backend/src/controller/defenceController.ts b/backend/src/controller/defenceController.ts index eefd7aa42..5389045c4 100644 --- a/backend/src/controller/defenceController.ts +++ b/backend/src/controller/defenceController.ts @@ -9,7 +9,6 @@ import { import { DefenceActivateRequest } from '@src/models/api/DefenceActivateRequest'; import { DefenceConfigResetRequest } from '@src/models/api/DefenceConfigResetRequest'; import { DefenceConfigureRequest } from '@src/models/api/DefenceConfigureRequest'; -import { DefenceStatusRequest } from '@src/models/api/DefenceStatusRequest'; import { DefenceConfigItem } from '@src/models/defence'; import { LEVEL_NAMES } from '@src/models/level'; @@ -112,20 +111,9 @@ function handleResetSingleDefence( } } -function handleGetDefenceStatus(req: DefenceStatusRequest, res: Response) { - const level: number | undefined = req.query.level as number | undefined; - if (level !== undefined) { - res.send(req.session.levelState[level].defences); - } else { - res.status(400); - res.send('Missing level'); - } -} - export { handleDefenceActivation, handleDefenceDeactivation, handleConfigureDefence, handleResetSingleDefence, - handleGetDefenceStatus, }; diff --git a/backend/src/controller/emailController.ts b/backend/src/controller/emailController.ts index f6e69080a..1c277a3be 100644 --- a/backend/src/controller/emailController.ts +++ b/backend/src/controller/emailController.ts @@ -1,19 +1,8 @@ import { Response } from 'express'; import { EmailClearRequest } from '@src/models/api/EmailClearRequest'; -import { EmailGetRequest } from '@src/models/api/EmailGetRequest'; import { LEVEL_NAMES } from '@src/models/level'; -function handleGetEmails(req: EmailGetRequest, res: Response) { - const level: number | undefined = req.query.level as number | undefined; - if (level !== undefined) { - res.send(req.session.levelState[level].sentEmails); - } else { - res.status(400); - res.send('Missing level'); - } -} - function handleClearEmails(req: EmailClearRequest, res: Response) { const level = req.body.level; if (level !== undefined && level >= LEVEL_NAMES.LEVEL_1) { @@ -26,4 +15,4 @@ function handleClearEmails(req: EmailClearRequest, res: Response) { } } -export { handleGetEmails, handleClearEmails }; +export { handleClearEmails }; diff --git a/backend/src/controller/levelController.ts b/backend/src/controller/levelController.ts new file mode 100644 index 000000000..e7251537f --- /dev/null +++ b/backend/src/controller/levelController.ts @@ -0,0 +1,26 @@ +import { Response } from 'express'; + +import { LevelGetRequest } from '@src/models/api/LevelGetRequest'; +import { isValidLevel } from '@src/models/level'; + +function handleLoadLevel(req: LevelGetRequest, res: Response) { + const { level } = req.query; + + if (level === undefined) { + res.status(400).send('Level not provided'); + return; + } + + if (!isValidLevel(level)) { + res.status(400).send('Invalid level'); + return; + } + + res.send({ + emails: req.session.levelState[level].sentEmails, + chatHistory: req.session.levelState[level].chatHistory, + defences: req.session.levelState[level].defences, + }); +} + +export { handleLoadLevel }; diff --git a/backend/src/controller/startController.ts b/backend/src/controller/startController.ts index 676c92686..fd7bd30c5 100644 --- a/backend/src/controller/startController.ts +++ b/backend/src/controller/startController.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; -import { GetStartRequest } from '@src/models/api/getStartRequest'; -import { LEVEL_NAMES } from '@src/models/level'; +import { StartGetRequest } from '@src/models/api/StartGetRequest'; +import { LEVEL_NAMES, isValidLevel } from '@src/models/level'; import { getValidOpenAIModels } from '@src/openai'; import { systemRoleLevel1, @@ -9,9 +9,21 @@ import { systemRoleLevel3, } from '@src/promptTemplates'; -function handleStart(req: GetStartRequest, res: Response) { +import { sendErrorResponse } from './handleError'; + +function handleStart(req: StartGetRequest, res: Response) { const { level } = req.query; + if (level === undefined) { + sendErrorResponse(res, 400, 'Level not provided'); + return; + } + + if (!isValidLevel(level)) { + sendErrorResponse(res, 400, 'Invalid level'); + return; + } + const systemRoles = [ { level: LEVEL_NAMES.LEVEL_1, systemRole: systemRoleLevel1 }, { level: LEVEL_NAMES.LEVEL_2, systemRole: systemRoleLevel2 }, diff --git a/backend/src/models/api/DefenceStatusRequest.ts b/backend/src/models/api/DefenceStatusRequest.ts deleted file mode 100644 index cfa452248..000000000 --- a/backend/src/models/api/DefenceStatusRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Request } from 'express'; - -import { Defence } from '@src/models/defence'; - -export type DefenceStatusRequest = Request< - never, - Defence[] | string, - never, - { - level?: string; - } ->; diff --git a/backend/src/models/api/EmailGetRequest.ts b/backend/src/models/api/EmailGetRequest.ts deleted file mode 100644 index 31de94f52..000000000 --- a/backend/src/models/api/EmailGetRequest.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request } from 'express'; - -import { EmailInfo } from '@src/models/email'; -import { LEVEL_NAMES } from '@src/models/level'; - -export type EmailGetRequest = Request< - never, - EmailInfo[] | string, - never, - { - level?: LEVEL_NAMES; - } ->; diff --git a/backend/src/models/api/LevelGetRequest.ts b/backend/src/models/api/LevelGetRequest.ts new file mode 100644 index 000000000..70326d143 --- /dev/null +++ b/backend/src/models/api/LevelGetRequest.ts @@ -0,0 +1,19 @@ +import { Request } from 'express'; + +import { ChatMessage } from '@src/models/chatMessage'; +import { Defence } from '@src/models/defence'; +import { EmailInfo } from '@src/models/email'; +import { LEVEL_NAMES } from '@src/models/level'; + +export type LevelGetRequest = Request< + never, + { + emails: EmailInfo[]; + chatHistory: ChatMessage[]; + defences: Defence[]; + }, + never, + { + level?: LEVEL_NAMES; + } +>; diff --git a/backend/src/models/api/OpenAiGetHistoryRequest.ts b/backend/src/models/api/OpenAiGetHistoryRequest.ts deleted file mode 100644 index 81aa7ef59..000000000 --- a/backend/src/models/api/OpenAiGetHistoryRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Request } from 'express'; - -import { ChatMessage } from '@src/models/chatMessage'; - -export type OpenAiGetHistoryRequest = Request< - never, - ChatMessage[] | string, - never, - { - level?: string; - } ->; diff --git a/backend/src/models/api/getStartRequest.ts b/backend/src/models/api/StartGetRequest.ts similarity index 86% rename from backend/src/models/api/getStartRequest.ts rename to backend/src/models/api/StartGetRequest.ts index 86d8c9dd2..188b6a310 100644 --- a/backend/src/models/api/getStartRequest.ts +++ b/backend/src/models/api/StartGetRequest.ts @@ -5,7 +5,7 @@ import { Defence } from '@src/models/defence'; import { EmailInfo } from '@src/models/email'; import { LEVEL_NAMES } from '@src/models/level'; -export type GetStartRequest = Request< +export type StartGetRequest = Request< never, { emails: EmailInfo[]; @@ -16,6 +16,6 @@ export type GetStartRequest = Request< }, never, { - level: LEVEL_NAMES; + level?: LEVEL_NAMES; } >; diff --git a/backend/src/models/level.ts b/backend/src/models/level.ts index 96e3956c6..59f818e19 100644 --- a/backend/src/models/level.ts +++ b/backend/src/models/level.ts @@ -13,6 +13,10 @@ const LEVEL_NAMES = { type LEVEL_NAMES = (typeof LEVEL_NAMES)[keyof typeof LEVEL_NAMES]; +function isValidLevel(levelValue: unknown) { + return Object.values(LEVEL_NAMES).includes(levelValue as LEVEL_NAMES); +} + interface LevelState { level: LEVEL_NAMES; chatHistory: ChatMessage[]; @@ -32,5 +36,5 @@ function getInitialLevelStates() { ); } -export { getInitialLevelStates, LEVEL_NAMES }; +export { getInitialLevelStates, LEVEL_NAMES, isValidLevel }; export type { LevelState }; diff --git a/backend/src/sessionRoutes.ts b/backend/src/sessionRoutes.ts index 02f658f06..1949bd1c9 100644 --- a/backend/src/sessionRoutes.ts +++ b/backend/src/sessionRoutes.ts @@ -5,7 +5,6 @@ import memoryStoreFactory from 'memorystore'; import { handleChatToGPT, - handleGetChatHistory, handleAddInfoToChatHistory, handleClearChatHistory, } from './controller/chatController'; @@ -13,13 +12,10 @@ import { handleConfigureDefence, handleDefenceActivation, handleDefenceDeactivation, - handleGetDefenceStatus, handleResetSingleDefence, } from './controller/defenceController'; -import { - handleClearEmails, - handleGetEmails, -} from './controller/emailController'; +import { handleClearEmails } from './controller/emailController'; +import { handleLoadLevel } from './controller/levelController'; import { handleConfigureModel, handleGetModel, @@ -28,16 +24,8 @@ import { import { handleResetProgress } from './controller/resetController'; import { handleStart } from './controller/startController'; import { handleTest } from './controller/testController'; -import { ChatModel, defaultChatModel } from './models/chat'; -import { LevelState, getInitialLevelStates } from './models/level'; - -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } -} +import { defaultChatModel } from './models/chat'; +import { getInitialLevelStates } from './models/level'; const sessionSigningSecret = process.env.SESSION_SECRET; if (!sessionSigningSecret) { @@ -97,19 +85,18 @@ router.use((req, _res, next) => { // handshake router.get('/start', handleStart); +router.get('/level', handleLoadLevel); + // defences -router.get('/defence/status', handleGetDefenceStatus); router.post('/defence/activate', handleDefenceActivation); router.post('/defence/deactivate', handleDefenceDeactivation); router.post('/defence/configure', handleConfigureDefence); router.post('/defence/resetConfig', handleResetSingleDefence); // emails -router.get('/email/get', handleGetEmails); router.post('/email/clear', handleClearEmails); // chat -router.get('/openai/history', handleGetChatHistory); router.post('/openai/chat', handleChatToGPT); router.post('/openai/addInfoToHistory', handleAddInfoToChatHistory); router.post('/openai/clear', handleClearChatHistory); diff --git a/backend/test/integration/chatController.test.ts b/backend/test/integration/chatController.test.ts index 51f582eaf..bea7ba5ba 100644 --- a/backend/test/integration/chatController.test.ts +++ b/backend/test/integration/chatController.test.ts @@ -3,27 +3,12 @@ import { Response } from 'express'; import { handleChatToGPT } from '@src/controller/chatController'; import { OpenAiChatRequest } from '@src/models/api/OpenAiChatRequest'; -import { ChatModel } from '@src/models/chat'; import { ChatMessage } from '@src/models/chatMessage'; import { Defence } from '@src/models/defence'; import { EmailInfo } from '@src/models/email'; import { LEVEL_NAMES, LevelState } from '@src/models/level'; import { systemRoleLevel1 } from '@src/promptTemplates'; -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } - interface LevelState { - level: LEVEL_NAMES; - chatHistory: ChatMessage[]; - defences: Defence[]; - sentEmails: EmailInfo[]; - } -} - // mock the api call const mockCreateChatCompletion = jest.fn< diff --git a/backend/test/tsconfig.json b/backend/test/tsconfig.json index 4f2ef6c8f..634d839bf 100644 --- a/backend/test/tsconfig.json +++ b/backend/test/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["./**/*.ts", "../jest.config.ts"] + "include": ["./**/*.ts", "../typings", "../jest.config.ts"] } diff --git a/backend/test/unit/controller/chatController.test.ts b/backend/test/unit/controller/chatController.test.ts index 57cb24384..308556406 100644 --- a/backend/test/unit/controller/chatController.test.ts +++ b/backend/test/unit/controller/chatController.test.ts @@ -5,16 +5,13 @@ import { handleAddInfoToChatHistory, handleChatToGPT, handleClearChatHistory, - handleGetChatHistory, } from '@src/controller/chatController'; import { detectTriggeredInputDefences, transformMessage } from '@src/defence'; import { OpenAiAddInfoToChatHistoryRequest } from '@src/models/api/OpenAiAddInfoToChatHistoryRequest'; import { OpenAiChatRequest } from '@src/models/api/OpenAiChatRequest'; import { OpenAiClearRequest } from '@src/models/api/OpenAiClearRequest'; -import { OpenAiGetHistoryRequest } from '@src/models/api/OpenAiGetHistoryRequest'; import { DefenceReport, - ChatModel, ChatResponse, MessageTransformation, } from '@src/models/chat'; @@ -28,20 +25,6 @@ import { setSystemRoleInChatHistory, } from '@src/utils/chat'; -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } - interface LevelState { - level: LEVEL_NAMES; - chatHistory: ChatMessage[]; - defences: Defence[]; - sentEmails: EmailInfo[]; - } -} - jest.mock('@src/openai'); const mockChatGptSendMessage = chatGptSendMessage as jest.MockedFunction< typeof chatGptSendMessage @@ -741,55 +724,6 @@ describe('handleChatToGPT unit tests', () => { }); }); -describe('handleGetChatHistory', () => { - function getRequestMock(level?: LEVEL_NAMES, chatHistory?: ChatMessage[]) { - return { - query: { - level: level ?? undefined, - }, - session: { - levelState: [ - { - chatHistory: chatHistory ?? [], - }, - ], - }, - } as OpenAiGetHistoryRequest; - } - - const chatHistory: ChatMessage[] = [ - { - completion: { role: 'system', content: 'You are a helpful chatbot' }, - chatMessageType: 'SYSTEM', - }, - { - completion: { role: 'assistant', content: 'Hello human' }, - chatMessageType: 'BOT', - }, - { - completion: { role: 'user', content: 'How are you?' }, - chatMessageType: 'USER', - }, - ]; - test('GIVEN a valid level WHEN handleGetChatHistory called THEN return chat history', () => { - const req = getRequestMock(LEVEL_NAMES.LEVEL_1, chatHistory); - const res = responseMock(); - - handleGetChatHistory(req, res); - expect(res.send).toHaveBeenCalledWith(chatHistory); - }); - - test('GIVEN undefined level WHEN handleGetChatHistory called THEN return 400', () => { - const req = getRequestMock(); - const res = responseMock(); - - handleGetChatHistory(req, res); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.send).toHaveBeenCalledWith('Missing level'); - }); -}); - describe('handleAddInfoToChatHistory', () => { function getAddInfoToChatHistoryRequestMock( infoMessage: string, diff --git a/backend/test/unit/controller/defenceController.test.ts b/backend/test/unit/controller/defenceController.test.ts index 5ed392cf9..753bb0907 100644 --- a/backend/test/unit/controller/defenceController.test.ts +++ b/backend/test/unit/controller/defenceController.test.ts @@ -4,26 +4,11 @@ import { Response } from 'express'; import { handleConfigureDefence } from '@src/controller/defenceController'; import { configureDefence } from '@src/defence'; import { DefenceConfigureRequest } from '@src/models/api/DefenceConfigureRequest'; -import { ChatModel } from '@src/models/chat'; import { ChatMessage } from '@src/models/chatMessage'; import { DEFENCE_ID, Defence } from '@src/models/defence'; import { EmailInfo } from '@src/models/email'; import { LEVEL_NAMES } from '@src/models/level'; -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } - interface LevelState { - level: LEVEL_NAMES; - chatHistory: ChatMessage[]; - defences: Defence[]; - sentEmails: EmailInfo[]; - } -} - jest.mock('@src/defence'); const mockConfigureDefence = configureDefence as jest.MockedFunction< typeof configureDefence diff --git a/backend/test/unit/controller/emailController.test.ts b/backend/test/unit/controller/emailController.test.ts index 1341fb400..2131b5b7f 100644 --- a/backend/test/unit/controller/emailController.test.ts +++ b/backend/test/unit/controller/emailController.test.ts @@ -1,31 +1,9 @@ import { expect, test, jest, describe } from '@jest/globals'; import { Response } from 'express'; -import { - handleClearEmails, - handleGetEmails, -} from '@src/controller/emailController'; +import { handleClearEmails } from '@src/controller/emailController'; import { EmailClearRequest } from '@src/models/api/EmailClearRequest'; -import { EmailGetRequest } from '@src/models/api/EmailGetRequest'; -import { ChatModel } from '@src/models/chat'; -import { ChatMessage } from '@src/models/chatMessage'; -import { Defence } from '@src/models/defence'; import { EmailInfo } from '@src/models/email'; -import { LEVEL_NAMES } from '@src/models/level'; - -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } - interface LevelState { - level: LEVEL_NAMES; - chatHistory: ChatMessage[]; - defences: Defence[]; - sentEmails: EmailInfo[]; - } -} function responseMock() { return { @@ -47,41 +25,6 @@ const emails: EmailInfo[] = [ }, ]; -describe('handleGetEmails', () => { - test('GIVEN valid level WHEN handleGetEmails called THEN returns sent emails', () => { - const req = { - query: { - level: 0, - }, - session: { - levelState: [ - { - sentEmails: emails, - }, - ], - }, - } as EmailGetRequest; - - const res = responseMock(); - - handleGetEmails(req, res); - - expect(res.send).toHaveBeenCalledWith(emails); - }); - - test('GIVEN missing level WHEN handleGetEmails called THEN returns 400 ', () => { - const req = { - query: {}, - } as EmailGetRequest; - - const res = responseMock(); - handleGetEmails(req, res); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.send).toHaveBeenCalledWith('Missing level'); - }); -}); - describe('handleClearEmails', () => { test('GIVEN valid level WHEN handleClearEmails called THEN sets emails to empty', () => { const req = { diff --git a/backend/test/unit/controller/levelController.test.ts b/backend/test/unit/controller/levelController.test.ts new file mode 100644 index 000000000..7e898610b --- /dev/null +++ b/backend/test/unit/controller/levelController.test.ts @@ -0,0 +1,119 @@ +import { expect, test, jest, afterEach } from '@jest/globals'; +import { Response } from 'express'; + +import { handleLoadLevel } from '@src/controller/levelController'; +import { LevelGetRequest } from '@src/models/api/LevelGetRequest'; +import { LEVEL_NAMES } from '@src/models/level'; + +jest.mock('@src/promptTemplates', () => ({ + systemRoleLevel1: 'systemRoleLevel1', + systemRoleLevel2: 'systemRoleLevel2', + systemRoleLevel3: 'systemRoleLevel3', +})); + +const mockSend = jest.fn(); + +function responseMock() { + return { + send: mockSend, + status: jest.fn().mockReturnValue({ send: mockSend }), + } as unknown as Response; +} + +afterEach(() => { + mockSend.mockClear(); +}); + +[ + LEVEL_NAMES.LEVEL_1, + LEVEL_NAMES.LEVEL_2, + LEVEL_NAMES.LEVEL_3, + LEVEL_NAMES.SANDBOX, +].forEach((level) => { + test(`GIVEN level ${ + level + 1 + } WHEN client asks to load the level THEN the backend sends the correct level information`, () => { + const req = { + query: { + level, + }, + session: { + levelState: [ + { + sentEmails: 'level 1 emails', + chatHistory: 'level 1 chat history', + defences: 'level 1 defences', + }, + { + sentEmails: 'level 2 emails', + chatHistory: 'level 2 chat history', + defences: 'level 2 defences', + }, + { + sentEmails: 'level 3 emails', + chatHistory: 'level 3 chat history', + defences: 'level 3 defences', + }, + { + sentEmails: 'level 4 emails', + chatHistory: 'level 4 chat history', + defences: 'level 4 defences', + }, + ], + }, + } as unknown as LevelGetRequest; + const res = responseMock(); + + handleLoadLevel(req, res); + + expect(mockSend).toHaveBeenCalledWith({ + emails: `level ${level + 1} emails`, + chatHistory: `level ${level + 1} chat history`, + defences: `level ${level + 1} defences`, + }); + }); +}); + +test('WHEN client does not provide a level THEN the backend responds with BadRequest', () => { + const req = { + query: {}, + session: { + levelState: [ + {}, + { + sentEmails: [], + chatHistory: [], + defences: [], + }, + ], + }, + } as unknown as LevelGetRequest; + const res = responseMock(); + + handleLoadLevel(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockSend).toHaveBeenCalledWith('Level not provided'); +}); + +test('WHEN client provides an invalid level THEN the backend responds with BadRequest', () => { + const req = { + query: { level: 5 }, + session: { + levelState: [ + {}, + { + sentEmails: [], + chatHistory: [], + defences: [], + }, + ], + }, + } as unknown as LevelGetRequest; + const res = responseMock(); + + handleLoadLevel(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockSend).toHaveBeenCalledWith('Invalid level'); +}); diff --git a/backend/test/unit/controller/resetController.test.ts b/backend/test/unit/controller/resetController.test.ts index 6a5807f4e..0764933b4 100644 --- a/backend/test/unit/controller/resetController.test.ts +++ b/backend/test/unit/controller/resetController.test.ts @@ -3,7 +3,6 @@ import { Request, Response } from 'express'; import { handleResetProgress } from '@src/controller/resetController'; import { defaultDefences } from '@src/defaultDefences'; -import { ChatModel } from '@src/models/chat'; import { ChatMessage } from '@src/models/chatMessage'; import { DEFENCE_ID, Defence, DefenceConfigItem } from '@src/models/defence'; import { EmailInfo } from '@src/models/email'; @@ -13,19 +12,6 @@ import { getInitialLevelStates, } from '@src/models/level'; -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } - interface LevelState { - level: LEVEL_NAMES; - chatHistory: ChatMessage[]; - defences: Defence[]; - sentEmails: EmailInfo[]; - } -} function responseMock() { return { send: jest.fn(), diff --git a/backend/test/unit/controller/startController.test.ts b/backend/test/unit/controller/startController.test.ts new file mode 100644 index 000000000..bf061e661 --- /dev/null +++ b/backend/test/unit/controller/startController.test.ts @@ -0,0 +1,126 @@ +import { expect, test, jest, afterEach } from '@jest/globals'; +import { Response } from 'express'; + +import { handleStart } from '@src/controller/startController'; +import { StartGetRequest } from '@src/models/api/StartGetRequest'; +import { LEVEL_NAMES } from '@src/models/level'; + +jest.mock('@src/promptTemplates', () => ({ + systemRoleLevel1: 'systemRoleLevel1', + systemRoleLevel2: 'systemRoleLevel2', + systemRoleLevel3: 'systemRoleLevel3', +})); + +const mockSend = jest.fn(); + +function responseMock() { + return { + send: mockSend, + status: jest.fn().mockReturnValue({ send: mockSend }), + } as unknown as Response; +} + +afterEach(() => { + mockSend.mockClear(); +}); + +[ + LEVEL_NAMES.LEVEL_1, + LEVEL_NAMES.LEVEL_2, + LEVEL_NAMES.LEVEL_3, + LEVEL_NAMES.SANDBOX, +].forEach((level) => { + test(`GIVEN level ${ + level + 1 + } provided WHEN user starts the frontend THEN the backend sends the correct initial information`, () => { + const req = { + query: { + level, + }, + session: { + levelState: [ + { + sentEmails: 'level 1 emails', + chatHistory: 'level 1 chat history', + defences: 'level 1 defences', + }, + { + sentEmails: 'level 2 emails', + chatHistory: 'level 2 chat history', + defences: 'level 2 defences', + }, + { + sentEmails: 'level 3 emails', + chatHistory: 'level 3 chat history', + defences: 'level 3 defences', + }, + { + sentEmails: 'level 4 emails', + chatHistory: 'level 4 chat history', + defences: 'level 4 defences', + }, + ], + systemRoles: [], + }, + } as unknown as StartGetRequest; + const res = responseMock(); + + handleStart(req, res); + + expect(mockSend).toHaveBeenCalledWith({ + emails: `level ${level + 1} emails`, + chatHistory: `level ${level + 1} chat history`, + defences: `level ${level + 1} defences`, + availableModels: [], + systemRoles: [ + { level: 0, systemRole: 'systemRoleLevel1' }, + { level: 1, systemRole: 'systemRoleLevel2' }, + { level: 2, systemRole: 'systemRoleLevel3' }, + ], + }); + }); +}); + +test('GIVEN no level provided WHEN user starts the frontend THEN the backend responds with error message', () => { + const req = { + query: {}, + session: { + levelState: [ + { + sentEmails: [], + chatHistory: [], + defences: [], + }, + ], + systemRoles: [], + }, + } as unknown as StartGetRequest; + const res = responseMock(); + + handleStart(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockSend).toHaveBeenCalledWith('Level not provided'); +}); + +test('GIVEN invalid level provided WHEN user starts the frontend THEN the backend responds with error message', () => { + const req = { + query: { level: 5 }, + session: { + levelState: [ + { + sentEmails: [], + chatHistory: [], + defences: [], + }, + ], + systemRoles: [], + }, + } as unknown as StartGetRequest; + const res = responseMock(); + + handleStart(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockSend).toHaveBeenCalledWith('Invalid level'); +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 9e7f3301a..a62188759 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -19,5 +19,5 @@ } }, "exclude": ["node_modules"], - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "typings"] } diff --git a/backend/typings/express-session.d.ts b/backend/typings/express-session.d.ts new file mode 100644 index 000000000..62b870dfa --- /dev/null +++ b/backend/typings/express-session.d.ts @@ -0,0 +1,10 @@ +import { ChatModel } from '@src/models/chat'; +import { LevelState } from '@src/models/level'; + +declare module 'express-session' { + interface Session { + initialised: boolean; + chatModel: ChatModel; + levelState: LevelState[]; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1f710a856..36a583070 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,7 @@ import OverlayWelcome from './components/Overlay/OverlayWelcome'; import ResetProgressOverlay from './components/Overlay/ResetProgress'; import useLocalStorage from './hooks/useLocalStorage'; import { LEVEL_NAMES } from './models/level'; -import { levelService } from './service'; +import { resetService } from './service'; import './App.css'; import './Theme.css'; @@ -155,7 +155,7 @@ function App() { console.log('resetting progress for all levels'); // reset on the backend - await levelService.resetAllLevelProgress(); + await resetService.resetAllLevelProgress(); resetCompletedLevels(); // set as new user so welcome modal shows diff --git a/frontend/src/components/MainComponent/MainComponent.tsx b/frontend/src/components/MainComponent/MainComponent.tsx index d5259152e..91f00b110 100644 --- a/frontend/src/components/MainComponent/MainComponent.tsx +++ b/frontend/src/components/MainComponent/MainComponent.tsx @@ -12,6 +12,7 @@ import { chatService, defenceService, emailService, + levelService, startService, } from '@src/service'; @@ -66,10 +67,6 @@ function MainComponent({ // facilitate level change useEffect(() => { - console.log( - 'useEffect currentLevel. isFirstRender.current:', - isFirstRender.current - ); if (!isFirstRender.current) { console.log('Loading backend data for level', currentLevel); void setNewLevel(currentLevel); @@ -79,11 +76,11 @@ function MainComponent({ async function loadBackendData() { try { - const { availableModels, defences, emails, history, systemRoles } = + const { availableModels, defences, emails, chatHistory, systemRoles } = await startService.start(currentLevel); setChatModels(availableModels); setSystemRoles(systemRoles); - processBackendLevelData(currentLevel, emails, history, defences); + processBackendLevelData(currentLevel, emails, chatHistory, defences); } catch (err) { console.warn(err); setMessages([ @@ -150,9 +147,9 @@ function MainComponent({ // for going switching level without clearing progress async function setNewLevel(newLevel: LEVEL_NAMES) { - const emails = await emailService.getSentEmails(newLevel); - const chatHistory = await chatService.getChatHistory(newLevel); - const defences = await defenceService.getDefences(newLevel); + const { emails, chatHistory, defences } = await levelService.loadLevel( + newLevel + ); processBackendLevelData(newLevel, emails, chatHistory, defences); } diff --git a/frontend/src/models/combined.ts b/frontend/src/models/combined.ts index b99fa0c61..34b84903d 100644 --- a/frontend/src/models/combined.ts +++ b/frontend/src/models/combined.ts @@ -11,4 +11,10 @@ type StartReponse = { systemRoles: LevelSystemRole[]; }; -export type { StartReponse }; +type LoadLevelResponse = { + emails: EmailInfo[]; + chatHistory: ChatMessageDTO[]; + defences: DefenceDTO[]; +}; + +export type { StartReponse, LoadLevelResponse }; diff --git a/frontend/src/service/chatService.ts b/frontend/src/service/chatService.ts index 9bd7b749f..2ed55c7b0 100644 --- a/frontend/src/service/chatService.ts +++ b/frontend/src/service/chatService.ts @@ -60,15 +60,6 @@ function chatMessageDTOIsConvertible(chatMessageDTO: ChatMessageDTO) { ); } -async function getChatHistory(level: number): Promise { - const response = await sendRequest(`${PATH}history?level=${level}`, { - method: 'GET', - }); - const chatMessageDTOs = (await response.json()) as ChatMessageDTO[]; - - return getChatMessagesFromDTOResponse(chatMessageDTOs); -} - function getChatMessagesFromDTOResponse(chatMessageDTOs: ChatMessageDTO[]) { return chatMessageDTOs .filter(chatMessageDTOIsConvertible) @@ -124,7 +115,6 @@ export { configureGptModel, getGptModel, setGptModel, - getChatHistory, addInfoMessageToChatHistory, getChatMessagesFromDTOResponse, }; diff --git a/frontend/src/service/defenceService.ts b/frontend/src/service/defenceService.ts index 460a8a8f6..9ded6c7fb 100644 --- a/frontend/src/service/defenceService.ts +++ b/frontend/src/service/defenceService.ts @@ -10,14 +10,6 @@ import { sendRequest } from './backendService'; const PATH = 'defence/'; -async function getDefences(level: number) { - const response = await sendRequest(`${PATH}status?level=${level}`, { - method: 'GET', - }); - const defenceDTOs = (await response.json()) as DefenceDTO[]; - return getDefencesFromDTOs(defenceDTOs); -} - function getDefencesFromDTOs(defenceDTOs: DefenceDTO[]) { return DEFAULT_DEFENCES.map((defence) => { const defenceDTO = defenceDTOs.find( @@ -122,7 +114,6 @@ function validateDefence( } export { - getDefences, toggleDefence, configureDefence, resetDefenceConfig, diff --git a/frontend/src/service/emailService.ts b/frontend/src/service/emailService.ts index c036db216..29ccc8197 100644 --- a/frontend/src/service/emailService.ts +++ b/frontend/src/service/emailService.ts @@ -1,5 +1,3 @@ -import { EmailInfo } from '@src/models/email'; - import { sendRequest } from './backendService'; const PATH = 'email/'; @@ -15,11 +13,4 @@ async function clearEmails(level: number): Promise { return response.status === 200; } -async function getSentEmails(level: number) { - const response = await sendRequest(`${PATH}get?level=${level}`, { - method: 'GET', - }); - return (await response.json()) as EmailInfo[]; -} - -export { clearEmails, getSentEmails }; +export { clearEmails }; diff --git a/frontend/src/service/index.ts b/frontend/src/service/index.ts index eb03f0626..02b9ed9a4 100644 --- a/frontend/src/service/index.ts +++ b/frontend/src/service/index.ts @@ -3,6 +3,7 @@ import * as defenceService from './defenceService'; import * as documentService from './documentService'; import * as emailService from './emailService'; import * as levelService from './levelService'; +import * as resetService from './resetService'; import * as startService from './startService'; export { @@ -10,6 +11,7 @@ export { defenceService, documentService, emailService, - levelService, + resetService, startService, + levelService, }; diff --git a/frontend/src/service/levelService.ts b/frontend/src/service/levelService.ts index e0d080909..643f3a9a1 100644 --- a/frontend/src/service/levelService.ts +++ b/frontend/src/service/levelService.ts @@ -1,15 +1,23 @@ +import { LoadLevelResponse } from '@src/models/combined'; + import { sendRequest } from './backendService'; +import { getChatMessagesFromDTOResponse } from './chatService'; +import { getDefencesFromDTOs } from './defenceService'; -const PATH = 'reset'; +const PATH = 'level'; -async function resetAllLevelProgress(): Promise { - const response = await sendRequest(PATH, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, +async function loadLevel(level: number) { + const response = await sendRequest(`${PATH}?level=${level}`, { + method: 'GET', }); - return response.status === 200; + const { defences, emails, chatHistory } = + (await response.json()) as LoadLevelResponse; + + return { + emails, + chatHistory: getChatMessagesFromDTOResponse(chatHistory), + defences: getDefencesFromDTOs(defences), + }; } -export { resetAllLevelProgress }; +export { loadLevel }; diff --git a/frontend/src/service/resetService.ts b/frontend/src/service/resetService.ts new file mode 100644 index 000000000..e0d080909 --- /dev/null +++ b/frontend/src/service/resetService.ts @@ -0,0 +1,15 @@ +import { sendRequest } from './backendService'; + +const PATH = 'reset'; + +async function resetAllLevelProgress(): Promise { + const response = await sendRequest(PATH, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.status === 200; +} + +export { resetAllLevelProgress }; diff --git a/frontend/src/service/startService.ts b/frontend/src/service/startService.ts index d1315dbfc..ef61eb559 100644 --- a/frontend/src/service/startService.ts +++ b/frontend/src/service/startService.ts @@ -15,7 +15,7 @@ async function start(level: number) { return { emails, - history: getChatMessagesFromDTOResponse(chatHistory), + chatHistory: getChatMessagesFromDTOResponse(chatHistory), defences: getDefencesFromDTOs(defences), availableModels, systemRoles,