feat: add Jest testing framework and CI/CD configuration (#187)

Co-authored-by: samanhappy@qq.com <my6051199>
This commit is contained in:
samanhappy
2025-06-18 14:02:52 +08:00
committed by GitHub
parent 1bd4fd6d9c
commit 1e308ec4c5
19 changed files with 1332 additions and 41 deletions

View File

@@ -0,0 +1,180 @@
// Test for path utilities functionality
import fs from 'fs';
import path from 'path';
// Mock fs module
jest.mock('fs');
const mockFs = fs as jest.Mocked<typeof fs>;
describe('Path Utilities Logic', () => {
beforeEach(() => {
jest.clearAllMocks();
delete process.env.MCPHUB_SETTING_PATH;
});
// Test the core logic of path resolution
const findConfigFile = (filename: string): string => {
const envPath = process.env.MCPHUB_SETTING_PATH;
const potentialPaths = [
...(envPath ? [envPath] : []),
path.resolve(process.cwd(), filename),
path.join(process.cwd(), filename),
];
for (const filePath of potentialPaths) {
if (fs.existsSync(filePath)) {
return filePath;
}
}
return path.resolve(process.cwd(), filename);
};
describe('Configuration File Resolution', () => {
it('should find existing file in current directory', () => {
const filename = 'test-config.json';
const expectedPath = path.resolve(process.cwd(), filename);
mockFs.existsSync.mockImplementation((filePath) => {
return filePath === expectedPath;
});
const result = findConfigFile(filename);
expect(result).toBe(expectedPath);
expect(mockFs.existsSync).toHaveBeenCalled();
});
it('should prioritize environment variable path', () => {
const filename = 'test-config.json';
const envPath = '/custom/path/test-config.json';
process.env.MCPHUB_SETTING_PATH = envPath;
mockFs.existsSync.mockImplementation((filePath) => {
return filePath === envPath;
});
const result = findConfigFile(filename);
expect(result).toBe(envPath);
expect(mockFs.existsSync).toHaveBeenCalledWith(envPath);
});
it('should return default path when file does not exist', () => {
const filename = 'nonexistent-config.json';
const expectedDefaultPath = path.resolve(process.cwd(), filename);
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(filename);
expect(result).toBe(expectedDefaultPath);
});
it('should handle different file types', () => {
const testFiles = [
'config.json',
'settings.yaml',
'data.xml',
'servers.json'
];
testFiles.forEach(filename => {
const expectedPath = path.resolve(process.cwd(), filename);
mockFs.existsSync.mockImplementation((filePath) => {
return filePath === expectedPath;
});
const result = findConfigFile(filename);
expect(result).toBe(expectedPath);
expect(path.isAbsolute(result)).toBe(true);
});
});
});
describe('Path Operations', () => {
it('should generate absolute paths', () => {
const filename = 'test.json';
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(filename);
expect(path.isAbsolute(result)).toBe(true);
expect(result).toContain(filename);
}); it('should handle path normalization', () => {
const filename = './config/../settings.json';
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(filename);
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});
it('should work consistently across multiple calls', () => {
const filename = 'consistent-test.json';
const expectedPath = path.resolve(process.cwd(), filename);
mockFs.existsSync.mockImplementation((filePath) => {
return filePath === expectedPath;
});
const result1 = findConfigFile(filename);
const result2 = findConfigFile(filename);
expect(result1).toBe(result2);
expect(result1).toBe(expectedPath);
});
});
describe('Environment Variable Handling', () => {
it('should handle missing environment variable gracefully', () => {
const filename = 'test.json';
delete process.env.MCPHUB_SETTING_PATH;
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(filename);
expect(typeof result).toBe('string');
expect(result).toContain(filename);
});
it('should handle empty environment variable', () => {
const filename = 'test.json';
process.env.MCPHUB_SETTING_PATH = '';
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(filename);
expect(typeof result).toBe('string');
expect(result).toContain(filename);
});
});
describe('Error Handling', () => {
it('should handle fs.existsSync errors gracefully', () => {
const filename = 'test.json';
mockFs.existsSync.mockImplementation(() => {
throw new Error('File system error');
});
expect(() => findConfigFile(filename)).toThrow('File system error');
});
it('should validate input parameters', () => {
const emptyFilename = '';
mockFs.existsSync.mockReturnValue(false);
const result = findConfigFile(emptyFilename);
expect(typeof result).toBe('string');
// Should still return a path, even for empty filename
});
});
});

176
tests/utils/testHelpers.ts Normal file
View File

@@ -0,0 +1,176 @@
// Test utilities and helpers
import express from 'express';
import request from 'supertest';
import jwt from 'jsonwebtoken';
export interface TestUser {
username: string;
password: string;
isAdmin?: boolean;
}
export interface AuthTokens {
accessToken: string;
refreshToken?: string;
}
/**
* Create a test Express app instance
*/
export const createTestApp = (): express.Application => {
const app = express();
app.use(express.json());
return app;
};
/**
* Generate a test JWT token
*/
export const generateTestToken = (payload: any, secret = 'test-jwt-secret-key'): string => {
return jwt.sign(payload, secret, { expiresIn: '1h' });
};
/**
* Create a test user token with default claims
*/
export const createUserToken = (username = 'testuser', isAdmin = false): string => {
const payload = {
user: {
username,
isAdmin,
},
};
return generateTestToken(payload);
};
/**
* Create an admin user token
*/
export const createAdminToken = (username = 'admin'): string => {
return createUserToken(username, true);
};
/**
* Make authenticated request helper
*/
export const makeAuthenticatedRequest = (app: express.Application, token: string) => {
return {
get: (url: string) => request(app).get(url).set('Authorization', `Bearer ${token}`),
post: (url: string) => request(app).post(url).set('Authorization', `Bearer ${token}`),
put: (url: string) => request(app).put(url).set('Authorization', `Bearer ${token}`),
delete: (url: string) => request(app).delete(url).set('Authorization', `Bearer ${token}`),
patch: (url: string) => request(app).patch(url).set('Authorization', `Bearer ${token}`),
};
};
/**
* Common test data generators
*/
export const TestData = {
user: (overrides: Partial<TestUser> = {}): TestUser => ({
username: 'testuser',
password: 'password123',
isAdmin: false,
...overrides,
}),
adminUser: (overrides: Partial<TestUser> = {}): TestUser => ({
username: 'admin',
password: 'admin123',
isAdmin: true,
...overrides,
}),
serverConfig: (overrides: any = {}) => ({
type: 'openapi',
openapi: {
url: 'https://api.example.com/openapi.json',
version: '3.1.0',
security: {
type: 'none',
},
},
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
...overrides,
}),
};
/**
* Mock response helpers
*/
export const MockResponse = {
success: (data: any = {}) => ({
success: true,
data,
}),
error: (message: string, code = 400) => ({
success: false,
message,
code,
}),
validation: (errors: any[]) => ({
success: false,
errors,
}),
};
/**
* Database test helpers
*/
export const DbHelpers = {
/**
* Clear all test data from database
*/
clearDatabase: async (): Promise<void> => {
// TODO: Implement based on your database setup
console.log('Clearing test database...');
},
/**
* Seed test data
*/
seedTestData: async (): Promise<void> => {
// TODO: Implement based on your database setup
console.log('Seeding test data...');
},
};
/**
* Wait for async operations to complete
*/
export const waitFor = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* Assert API response structure
*/
export const expectApiResponse = (response: any) => ({
toBeSuccess: (expectedData?: any) => {
expect(response.body).toHaveProperty('success', true);
if (expectedData) {
expect(response.body.data).toEqual(expectedData);
}
},
toBeError: (expectedMessage?: string, expectedCode?: number) => {
expect(response.body).toHaveProperty('success', false);
if (expectedMessage) {
expect(response.body.message).toContain(expectedMessage);
}
if (expectedCode) {
expect(response.status).toBe(expectedCode);
}
},
toHaveValidationErrors: () => {
expect(response.body).toHaveProperty('success', false);
expect(response.body).toHaveProperty('errors');
expect(Array.isArray(response.body.errors)).toBe(true);
},
});