diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f8962c3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,65 @@ +name: Unit Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test-shared: + name: Test @mono/shared + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9.1.0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - run: pnpm install --frozen-lockfile + - run: pnpm --filter @mono/shared test + + test-github: + name: Test @mono/github + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9.1.0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - run: pnpm install --frozen-lockfile + - run: pnpm --filter @mono/github test + + test-api-gateway: + name: Test @mono/api-gateway + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9.1.0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - run: pnpm install --frozen-lockfile + - run: pnpm --filter @mono/api-gateway test diff --git a/apps/api-gateway/src/middleware/auth.test.ts b/apps/api-gateway/src/middleware/auth.test.ts new file mode 100644 index 0000000..42a3b28 --- /dev/null +++ b/apps/api-gateway/src/middleware/auth.test.ts @@ -0,0 +1,108 @@ +jest.mock('../config/env.js', () => ({ + env: { + NODE_ENV: 'development', + PORT: 3000, + DATABASE_URL: 'postgresql://test:test@localhost:5432/test', + GITHUB_APP_ID: 'test-app-id', + GITHUB_APP_PRIVATE_KEY: 'dGVzdA==', + GITHUB_CLIENT_ID: 'test-client-id', + GITHUB_CLIENT_SECRET: 'test-client-secret', + JWT_SECRET: 'test-jwt-secret-that-is-at-least-32-chars-long', + APP_URL: 'http://localhost:3000', + }, + getGitHubPrivateKey: () => 'test', +})); + +import type { Request, Response, NextFunction } from 'express'; + +import { requireAuth, signToken, JwtPayload } from './auth'; + +describe('signToken', () => { + it('should return a JWT string', () => { + const payload: JwtPayload = { + userId: 'user-1', + githubUsername: 'octocat', + role: 'user', + }; + + const token = signToken(payload); + + expect(typeof token).toBe('string'); + expect(token.split('.')).toHaveLength(3); // JWT has 3 parts + }); +}); + +describe('requireAuth', () => { + let mockRes: Partial; + let mockNext: NextFunction; + let jsonMock: jest.Mock; + let statusMock: jest.Mock; + + beforeEach(() => { + jsonMock = jest.fn(); + statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + mockRes = { status: statusMock, json: jsonMock }; + mockNext = jest.fn(); + }); + + it('should return 401 when no Authorization header', () => { + const mockReq = { headers: {} } as Request; + + requireAuth(mockReq, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(401); + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Missing or invalid Authorization header', + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 401 when Authorization header does not start with Bearer', () => { + const mockReq = { headers: { authorization: 'Basic abc' } } as Request; + + requireAuth(mockReq, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(401); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 401 for invalid token', () => { + const mockReq = { + headers: { authorization: 'Bearer invalid-token' }, + } as Request; + + requireAuth(mockReq, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(401); + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Invalid or expired token', + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should call next and attach user for valid token', () => { + const payload: JwtPayload = { + userId: 'user-1', + githubUsername: 'octocat', + role: 'user', + }; + const token = signToken(payload); + const mockReq = { + headers: { authorization: `Bearer ${token}` }, + } as Request; + + requireAuth(mockReq, mockRes as Response, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockReq.user).toBeDefined(); + expect(mockReq.user!.userId).toBe('user-1'); + expect(mockReq.user!.githubUsername).toBe('octocat'); + expect(mockReq.user!.role).toBe('user'); + }); +}); diff --git a/apps/api-gateway/src/middleware/error-handler.test.ts b/apps/api-gateway/src/middleware/error-handler.test.ts new file mode 100644 index 0000000..152595d --- /dev/null +++ b/apps/api-gateway/src/middleware/error-handler.test.ts @@ -0,0 +1,83 @@ +jest.mock('../config/env.js', () => ({ + env: { + NODE_ENV: 'development', + PORT: 3000, + DATABASE_URL: 'postgresql://test:test@localhost:5432/test', + GITHUB_APP_ID: 'test-app-id', + GITHUB_APP_PRIVATE_KEY: 'dGVzdA==', + GITHUB_CLIENT_ID: 'test-client-id', + GITHUB_CLIENT_SECRET: 'test-client-secret', + JWT_SECRET: 'test-jwt-secret-that-is-at-least-32-chars-long', + APP_URL: 'http://localhost:3000', + }, + getGitHubPrivateKey: () => 'test', +})); + +import type { Request, Response, NextFunction } from 'express'; + +import { errorHandler, AppError } from './error-handler'; + +describe('errorHandler', () => { + let mockReq: Partial; + let mockRes: Partial; + let mockNext: NextFunction; + let jsonMock: jest.Mock; + let statusMock: jest.Mock; + + beforeEach(() => { + jsonMock = jest.fn(); + statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + mockReq = {}; + mockRes = { status: statusMock, json: jsonMock }; + mockNext = jest.fn(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should respond with 500 and default message when error has no statusCode', () => { + const err: AppError = new Error('Unexpected failure'); + + errorHandler(err, mockReq as Request, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Unexpected failure', + }) + ); + }); + + it('should use error statusCode when provided', () => { + const err: AppError = new Error('Not found'); + err.statusCode = 404; + + errorHandler(err, mockReq as Request, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(404); + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Not found', + }) + ); + }); + + it('should use "Internal Server Error" when error has no message', () => { + const err: AppError = new Error(); + err.message = ''; + + errorHandler(err, mockReq as Request, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: 'Internal Server Error', + }) + ); + }); +}); diff --git a/packages/github/jest.config.js b/packages/github/jest.config.js new file mode 100644 index 0000000..894ce66 --- /dev/null +++ b/packages/github/jest.config.js @@ -0,0 +1,17 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], + coverageDirectory: 'coverage', + verbose: true, +}; diff --git a/packages/github/package.json b/packages/github/package.json index b06d013..4dd4c7e 100644 --- a/packages/github/package.json +++ b/packages/github/package.json @@ -9,16 +9,21 @@ "clean": "rm -rf dist", "dev": "tsc --watch", "lint": "eslint src/", + "test": "jest", + "test:coverage": "jest --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { - "@octokit/core": "^6.1.0", - "@octokit/auth-app": "^7.1.0" + "@octokit/auth-app": "^7.1.0", + "@octokit/core": "^6.1.0" }, "devDependencies": { "@mono/eslint-config": "workspace:*", "@mono/typescript-config": "workspace:*", + "@types/jest": "^29.5.12", "@types/node": "^20.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", "typescript": "^5.3.3" } } diff --git a/packages/github/src/app-auth.test.ts b/packages/github/src/app-auth.test.ts new file mode 100644 index 0000000..152a73d --- /dev/null +++ b/packages/github/src/app-auth.test.ts @@ -0,0 +1,50 @@ +jest.mock('@octokit/core', () => ({ + Octokit: jest.fn().mockImplementation((opts) => ({ auth: opts.auth })), +})); +jest.mock('@octokit/auth-app', () => ({ + createAppAuth: jest.fn().mockReturnValue('app-auth-strategy'), +})); + +import { createAppOctokit, createInstallationOctokit } from './app-auth'; +import { Octokit } from '@octokit/core'; + +describe('createAppOctokit', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an Octokit instance with app auth', () => { + const result = createAppOctokit('app-123', 'private-key'); + + expect(Octokit).toHaveBeenCalledWith( + expect.objectContaining({ + auth: { + appId: 'app-123', + privateKey: 'private-key', + }, + }) + ); + expect(result).toBeDefined(); + }); +}); + +describe('createInstallationOctokit', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an Octokit instance with installation auth', () => { + const result = createInstallationOctokit('app-123', 'private-key', 456); + + expect(Octokit).toHaveBeenCalledWith( + expect.objectContaining({ + auth: { + appId: 'app-123', + privateKey: 'private-key', + installationId: 456, + }, + }) + ); + expect(result).toBeDefined(); + }); +}); diff --git a/packages/github/src/oauth.test.ts b/packages/github/src/oauth.test.ts new file mode 100644 index 0000000..09b3102 --- /dev/null +++ b/packages/github/src/oauth.test.ts @@ -0,0 +1,127 @@ +import { getAuthorizationUrl } from './oauth'; + +describe('getAuthorizationUrl', () => { + it('should build the correct OAuth URL with required params', () => { + const url = getAuthorizationUrl('client-123', 'http://localhost/callback', 'state-abc'); + + expect(url).toContain('https://github.com/login/oauth/authorize'); + expect(url).toContain('client_id=client-123'); + expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%2Fcallback'); + expect(url).toContain('state=state-abc'); + }); + + it('should include scopes when provided', () => { + const url = getAuthorizationUrl('client-123', 'http://localhost/callback', 'state-abc', [ + 'read:user', + 'repo', + ]); + + expect(url).toContain('scope=read%3Auser+repo'); + }); + + it('should not include scope param when scopes array is empty', () => { + const url = getAuthorizationUrl('client-123', 'http://localhost/callback', 'state-abc', []); + + expect(url).not.toContain('scope='); + }); +}); + +describe('exchangeCodeForToken', () => { + const { exchangeCodeForToken } = require('./oauth'); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should exchange code for token successfully', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ + access_token: 'ghu_test_token', + token_type: 'bearer', + scope: '', + }), + }; + jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse as unknown as Response); + + const result = await exchangeCodeForToken('client-id', 'client-secret', 'code-123'); + + expect(result.access_token).toBe('ghu_test_token'); + expect(fetch).toHaveBeenCalledWith( + 'https://github.com/login/oauth/access_token', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Accept: 'application/json', + }), + }) + ); + }); + + it('should throw when response is not ok', async () => { + jest.spyOn(global, 'fetch').mockResolvedValue({ + ok: false, + status: 500, + } as Response); + + await expect(exchangeCodeForToken('a', 'b', 'c')).rejects.toThrow( + 'GitHub token exchange failed: 500' + ); + }); + + it('should throw when no access_token in response', async () => { + jest.spyOn(global, 'fetch').mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({}), + } as unknown as Response); + + await expect(exchangeCodeForToken('a', 'b', 'c')).rejects.toThrow( + 'GitHub token exchange returned no access_token' + ); + }); +}); + +describe('getGitHubUser', () => { + const { getGitHubUser } = require('./oauth'); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should fetch GitHub user profile', async () => { + const mockUser = { + id: 1, + login: 'testuser', + name: 'Test', + email: 'test@example.com', + avatar_url: 'https://example.com/avatar.png', + }; + jest.spyOn(global, 'fetch').mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockUser), + } as unknown as Response); + + const result = await getGitHubUser('token-123'); + + expect(result).toEqual(mockUser); + expect(fetch).toHaveBeenCalledWith( + 'https://api.github.com/user', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer token-123', + }), + }) + ); + }); + + it('should throw when response is not ok', async () => { + jest.spyOn(global, 'fetch').mockResolvedValue({ + ok: false, + status: 401, + } as Response); + + await expect(getGitHubUser('bad-token')).rejects.toThrow( + 'Failed to fetch GitHub user: 401' + ); + }); +}); diff --git a/packages/github/src/user-auth.test.ts b/packages/github/src/user-auth.test.ts new file mode 100644 index 0000000..d23144b --- /dev/null +++ b/packages/github/src/user-auth.test.ts @@ -0,0 +1,21 @@ +jest.mock('@octokit/core', () => ({ + Octokit: jest.fn().mockImplementation((opts) => ({ auth: opts.auth })), +})); + +import { createUserOctokit } from './user-auth'; +import { Octokit } from '@octokit/core'; + +describe('createUserOctokit', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an Octokit instance with user access token', () => { + const result = createUserOctokit('ghu_user_token'); + + expect(Octokit).toHaveBeenCalledWith({ + auth: 'ghu_user_token', + }); + expect(result).toBeDefined(); + }); +}); diff --git a/packages/github/src/webhook.test.ts b/packages/github/src/webhook.test.ts new file mode 100644 index 0000000..ae35ecd --- /dev/null +++ b/packages/github/src/webhook.test.ts @@ -0,0 +1,34 @@ +import { verifyWebhookSignature } from './webhook'; +import { createHmac } from 'node:crypto'; + +describe('verifyWebhookSignature', () => { + const secret = 'my-webhook-secret'; + const payload = '{"action":"opened"}'; + + function makeSignature(body: string, key: string): string { + return `sha256=${createHmac('sha256', key).update(body).digest('hex')}`; + } + + it('should return true for a valid signature', () => { + const signature = makeSignature(payload, secret); + expect(verifyWebhookSignature(payload, signature, secret)).toBe(true); + }); + + it('should return false for an invalid signature', () => { + expect(verifyWebhookSignature(payload, 'sha256=invalid', secret)).toBe(false); + }); + + it('should return false when signature length differs', () => { + expect(verifyWebhookSignature(payload, 'sha256=short', secret)).toBe(false); + }); + + it('should return false for a different secret', () => { + const signature = makeSignature(payload, 'wrong-secret'); + expect(verifyWebhookSignature(payload, signature, secret)).toBe(false); + }); + + it('should return false for a different payload', () => { + const signature = makeSignature('other-payload', secret); + expect(verifyWebhookSignature(payload, signature, secret)).toBe(false); + }); +}); diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js new file mode 100644 index 0000000..894ce66 --- /dev/null +++ b/packages/shared/jest.config.js @@ -0,0 +1,17 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], + coverageDirectory: 'coverage', + verbose: true, +}; diff --git a/packages/shared/package.json b/packages/shared/package.json index 0fa0e03..f0676d2 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -9,6 +9,8 @@ "clean": "rm -rf dist", "dev": "tsc --watch", "lint": "eslint src/", + "test": "jest", + "test:coverage": "jest --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -17,7 +19,10 @@ "devDependencies": { "@mono/eslint-config": "workspace:*", "@mono/typescript-config": "workspace:*", + "@types/jest": "^29.5.12", "@types/node": "^20.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", "typescript": "^5.3.3" } } diff --git a/packages/shared/src/constants/constants.test.ts b/packages/shared/src/constants/constants.test.ts new file mode 100644 index 0000000..b72732d --- /dev/null +++ b/packages/shared/src/constants/constants.test.ts @@ -0,0 +1,49 @@ +import { HTTP_STATUS, ENVIRONMENTS } from './index'; +import type { Environment } from './index'; + +describe('HTTP_STATUS', () => { + it('should have correct status codes', () => { + expect(HTTP_STATUS.OK).toBe(200); + expect(HTTP_STATUS.CREATED).toBe(201); + expect(HTTP_STATUS.BAD_REQUEST).toBe(400); + expect(HTTP_STATUS.UNAUTHORIZED).toBe(401); + expect(HTTP_STATUS.FORBIDDEN).toBe(403); + expect(HTTP_STATUS.NOT_FOUND).toBe(404); + expect(HTTP_STATUS.INTERNAL_SERVER_ERROR).toBe(500); + }); + + it('should have all expected keys', () => { + const keys = Object.keys(HTTP_STATUS); + expect(keys).toEqual( + expect.arrayContaining([ + 'OK', + 'CREATED', + 'BAD_REQUEST', + 'UNAUTHORIZED', + 'FORBIDDEN', + 'NOT_FOUND', + 'INTERNAL_SERVER_ERROR', + ]) + ); + }); +}); + +describe('ENVIRONMENTS', () => { + it('should have correct environment values', () => { + expect(ENVIRONMENTS.DEVELOPMENT).toBe('development'); + expect(ENVIRONMENTS.STAGING).toBe('staging'); + expect(ENVIRONMENTS.PRODUCTION).toBe('production'); + }); + + it('should have all expected keys', () => { + const keys = Object.keys(ENVIRONMENTS); + expect(keys).toEqual( + expect.arrayContaining(['DEVELOPMENT', 'STAGING', 'PRODUCTION']) + ); + }); + + it('should allow type-safe environment assignment', () => { + const env: Environment = ENVIRONMENTS.DEVELOPMENT; + expect(env).toBe('development'); + }); +}); diff --git a/packages/shared/src/types/types.test.ts b/packages/shared/src/types/types.test.ts new file mode 100644 index 0000000..426282a --- /dev/null +++ b/packages/shared/src/types/types.test.ts @@ -0,0 +1,76 @@ +import { paginationSchema } from './index'; +import type { ApiResponse, PaginationParams, PaginatedResponse } from './index'; + +describe('paginationSchema', () => { + it('should parse valid pagination input', () => { + const result = paginationSchema.parse({ page: 2, limit: 50 }); + expect(result).toEqual({ page: 2, limit: 50 }); + }); + + it('should apply default values', () => { + const result = paginationSchema.parse({}); + expect(result).toEqual({ page: 1, limit: 20 }); + }); + + it('should coerce string numbers', () => { + const result = paginationSchema.parse({ page: '3', limit: '10' }); + expect(result).toEqual({ page: 3, limit: 10 }); + }); + + it('should reject page less than 1', () => { + expect(() => paginationSchema.parse({ page: 0 })).toThrow(); + }); + + it('should reject limit greater than 100', () => { + expect(() => paginationSchema.parse({ limit: 101 })).toThrow(); + }); + + it('should reject limit less than 1', () => { + expect(() => paginationSchema.parse({ limit: 0 })).toThrow(); + }); +}); + +describe('ApiResponse type', () => { + it('should allow creating a typed success response', () => { + const response: ApiResponse = { + success: true, + data: 'hello', + }; + expect(response.success).toBe(true); + expect(response.data).toBe('hello'); + }); + + it('should allow creating a typed error response', () => { + const response: ApiResponse = { + success: false, + error: 'something failed', + }; + expect(response.success).toBe(false); + expect(response.error).toBe('something failed'); + }); +}); + +describe('PaginationParams type', () => { + it('should allow creating pagination params', () => { + const params: PaginationParams = { page: 1, limit: 20 }; + expect(params.page).toBe(1); + expect(params.limit).toBe(20); + }); +}); + +describe('PaginatedResponse type', () => { + it('should allow creating a paginated response', () => { + const response: PaginatedResponse = { + success: true, + data: [1, 2, 3], + pagination: { + page: 1, + limit: 20, + total: 3, + totalPages: 1, + }, + }; + expect(response.data).toHaveLength(3); + expect(response.pagination.total).toBe(3); + }); +}); diff --git a/packages/shared/src/utils/utils.test.ts b/packages/shared/src/utils/utils.test.ts new file mode 100644 index 0000000..73bdec6 --- /dev/null +++ b/packages/shared/src/utils/utils.test.ts @@ -0,0 +1,72 @@ +import { successResponse, errorResponse, sleep, generateId } from './index'; + +describe('successResponse', () => { + it('should create a success response with data', () => { + const result = successResponse({ id: 1 }); + expect(result).toEqual({ + success: true, + data: { id: 1 }, + message: undefined, + }); + }); + + it('should include an optional message', () => { + const result = successResponse('ok', 'All good'); + expect(result).toEqual({ + success: true, + data: 'ok', + message: 'All good', + }); + }); +}); + +describe('errorResponse', () => { + it('should create an error response', () => { + const result = errorResponse('Something went wrong'); + expect(result).toEqual({ + success: false, + error: 'Something went wrong', + message: undefined, + }); + }); + + it('should include an optional message', () => { + const result = errorResponse('Not found', 'Resource missing'); + expect(result).toEqual({ + success: false, + error: 'Not found', + message: 'Resource missing', + }); + }); +}); + +describe('sleep', () => { + it('should resolve after specified time', async () => { + const start = Date.now(); + await sleep(50); + const elapsed = Date.now() - start; + expect(elapsed).toBeGreaterThanOrEqual(40); + }); +}); + +describe('generateId', () => { + it('should generate an id of default length 12', () => { + const id = generateId(); + expect(id).toHaveLength(12); + }); + + it('should generate an id of specified length', () => { + const id = generateId(20); + expect(id).toHaveLength(20); + }); + + it('should only contain lowercase alphanumeric characters', () => { + const id = generateId(100); + expect(id).toMatch(/^[a-z0-9]+$/); + }); + + it('should generate unique ids', () => { + const ids = new Set(Array.from({ length: 50 }, () => generateId())); + expect(ids.size).toBe(50); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36bf174..1ba4f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,9 +161,18 @@ importers: '@mono/typescript-config': specifier: workspace:* version: link:../typescript-config + '@types/jest': + specifier: ^29.5.12 + version: 29.5.14 '@types/node': specifier: ^20.11.0 version: 20.19.28 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.28) + ts-jest: + specifier: ^29.1.2 + version: 29.4.6(@babel/core@7.28.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.28))(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 @@ -180,9 +189,18 @@ importers: '@mono/typescript-config': specifier: workspace:* version: link:../typescript-config + '@types/jest': + specifier: ^29.5.12 + version: 29.5.14 '@types/node': specifier: ^20.11.0 version: 20.19.28 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.28) + ts-jest: + specifier: ^29.1.2 + version: 29.4.6(@babel/core@7.28.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.28))(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3