diff --git a/apps/bot/package.json b/apps/bot/package.json index f4ffe0c..4bdbf71 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -16,7 +16,8 @@ "start": "node dist/src/index.js", "dev": "dotenv -e ../../.env.local tsx watch src/index.ts", "lint": "eslint", - "lint-staged": "lint-staged" + "lint-staged": "lint-staged", + "test:unit": "vitest" }, "devDependencies": { "@repo/eslint-config": "workspace:*", @@ -29,6 +30,8 @@ "lint-staged": "catalog:", "rimraf": "catalog:", "tsx": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vite-tsconfig-paths": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/bot/src/api/query.ts b/apps/bot/src/api/query.ts index 88e60c5..19f0241 100644 --- a/apps/bot/src/api/query.ts +++ b/apps/bot/src/api/query.ts @@ -1,4 +1,5 @@ -import { getClientWithToken } from '../apollo/client'; +import { createApolloClient, getClientWithToken } from '../apollo/client'; +import { env as environment } from '../config/env'; import * as GQL from '@repo/graphql/types'; export async function createCustomer(variables: GQL.CreateCustomerMutationVariables) { @@ -18,3 +19,17 @@ export async function getCustomer(variables: GQL.GetCustomerQueryVariables) { variables, }); } + +export async function login() { + const { mutate } = createApolloClient(); + + const response = await mutate({ + mutation: GQL.LoginDocument, + variables: { + identifier: environment.LOGIN_GRAPHQL, + password: environment.PASSWORD_GRAPHQL, + }, + }); + + return response; +} diff --git a/apps/bot/src/apollo/client.ts b/apps/bot/src/apollo/client.ts index e583e75..7ee265f 100644 --- a/apps/bot/src/apollo/client.ts +++ b/apps/bot/src/apollo/client.ts @@ -1,5 +1,5 @@ import { env as environment } from '../config/env'; -import { getToken } from '../utils/jwt'; +import { getToken } from '../config/token'; import { ApolloClient, InMemoryCache } from '@apollo/client/core'; type Parameters_ = { token: null | string | undefined }; diff --git a/apps/bot/src/config/token.ts b/apps/bot/src/config/token.ts new file mode 100644 index 0000000..eb29fa1 --- /dev/null +++ b/apps/bot/src/config/token.ts @@ -0,0 +1,13 @@ +import { login } from '../api/query'; +import { isTokenExpired } from '../utils/jwt'; + +export const token: null | string = null; + +export async function getToken() { + if (!token || isTokenExpired(token)) { + const response = await login(); + return response?.data?.login.jwt; + } + + return token; +} diff --git a/apps/bot/src/utils/jwt.test.ts b/apps/bot/src/utils/jwt.test.ts new file mode 100644 index 0000000..77fbbe4 --- /dev/null +++ b/apps/bot/src/utils/jwt.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable unicorn/consistent-function-scoping */ +import { isTokenExpired } from './jwt'; +import * as jwt from 'jsonwebtoken'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +describe('isTokenExpired', () => { + const mockDateNow = (timestamp: number) => { + vi.spyOn(Date, 'now').mockReturnValue(timestamp); + }; + + afterEach(() => { + vi.restoreAllMocks(); // Сбрасываем все моки после каждого теста + }); + + it('should return true if the token is expired', () => { + const token = jwt.sign({}, 'secret', { expiresIn: -10 }); // Токен с истекшим временем + mockDateNow(Date.now()); + + const result = isTokenExpired(token); + expect(result).toBe(true); + }); + + it('should return false if the token is not expired', () => { + const token = jwt.sign({}, 'secret', { expiresIn: 3_600 }); // Токен с временем жизни 1 час + mockDateNow(Date.now()); + + const result = isTokenExpired(token); + expect(result).toBe(false); + }); + + it('should return true if the token will expire within the threshold', () => { + const threshold = 300; // 5 минут + const token = jwt.sign({}, 'secret', { expiresIn: threshold - 10 }); // Токен с временем жизни чуть меньше порога + mockDateNow(Date.now()); + + const result = isTokenExpired(token, threshold); + expect(result).toBe(true); + }); + + it('should return false if the token will expire outside the threshold', () => { + const threshold = 300; // 5 минут + const token = jwt.sign({}, 'secret', { expiresIn: threshold + 100 }); // Токен с временем жизни больше порога + mockDateNow(Date.now()); + + const result = isTokenExpired(token, threshold); + expect(result).toBe(false); + }); + + it('should return true if the token is invalid', () => { + const invalidToken = 'invalid.token.string'; + + const result = isTokenExpired(invalidToken); + expect(result).toBe(true); + }); + + it("should throw an error if the token doesn't have an exp field", () => { + const token = jwt.sign({ data: 'no exp' }, 'secret', { noTimestamp: true }); // Токен без exp + const result = isTokenExpired(token); + expect(result).toBe(true); // Ожидается, что вернётся true + }); +}); diff --git a/apps/bot/src/utils/jwt.ts b/apps/bot/src/utils/jwt.ts index f5d784c..72c6add 100644 --- a/apps/bot/src/utils/jwt.ts +++ b/apps/bot/src/utils/jwt.ts @@ -1,22 +1,6 @@ -import { createApolloClient } from '../apollo/client'; -import { env as environment } from '../config/env'; -import * as GQL from '@repo/graphql/types'; import * as jwt from 'jsonwebtoken'; -const token: null | string = null; - -export async function getToken() { - if (!token || isTokenExpired()) { - const response = await login(); - return response?.data?.login.jwt; - } - - return token; -} - -function isTokenExpired(threshold: number = 300) { - if (!token) throw new Error('Token is missing'); - +export function isTokenExpired(token: string, threshold: number = 300) { try { const decoded = jwt.decode(token); @@ -32,17 +16,3 @@ function isTokenExpired(threshold: number = 300) { return true; } } - -async function login() { - const { mutate } = createApolloClient(); - - const response = await mutate({ - mutation: GQL.LoginDocument, - variables: { - identifier: environment.LOGIN_GRAPHQL, - password: environment.PASSWORD_GRAPHQL, - }, - }); - - return response; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e23d32..8e9f200 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,12 @@ importers: typescript: specifier: 'catalog:' version: 5.7.2 + vite-tsconfig-paths: + specifier: 'catalog:' + version: 5.1.4(typescript@5.7.2)(vite@5.4.11(@types/node@20.17.8)) + vitest: + specifier: 'catalog:' + version: 2.1.8(@types/node@20.17.8)(jsdom@25.0.1) apps/web: dependencies: