feat: add comprehensive testing infrastructure
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
- Add Vitest for unit testing across all packages - Add Playwright for E2E testing - Add sample tests for API, Web, and Shared packages - Configure Gitea Actions CI/CD workflows (ci, e2e, security, docker) - Add testing documentation (TESTING.md) - Add Gitea Actions setup guide - Update .gitignore for test artifacts - Add test environment configuration
This commit is contained in:
@@ -6,11 +6,18 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"keywords": ["basil", "shared", "types"],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.0",
|
||||
"@vitest/ui": "^1.2.0",
|
||||
"@vitest/coverage-v8": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
263
packages/shared/src/types.test.ts
Normal file
263
packages/shared/src/types.test.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type {
|
||||
Recipe,
|
||||
Ingredient,
|
||||
Instruction,
|
||||
RecipeImportRequest,
|
||||
RecipeImportResponse,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
StorageConfig,
|
||||
} from './types';
|
||||
|
||||
describe('Shared Types', () => {
|
||||
describe('Recipe Type', () => {
|
||||
it('should accept valid recipe object', () => {
|
||||
const recipe: Recipe = {
|
||||
id: '1',
|
||||
title: 'Test Recipe',
|
||||
description: 'A test recipe',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
expect(recipe.id).toBe('1');
|
||||
expect(recipe.title).toBe('Test Recipe');
|
||||
});
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const recipe: Recipe = {
|
||||
id: '1',
|
||||
title: 'Minimal Recipe',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
prepTime: 15,
|
||||
cookTime: 30,
|
||||
totalTime: 45,
|
||||
servings: 4,
|
||||
cuisine: 'Italian',
|
||||
category: 'Main Course',
|
||||
rating: 4.5,
|
||||
};
|
||||
|
||||
expect(recipe.prepTime).toBe(15);
|
||||
expect(recipe.servings).toBe(4);
|
||||
expect(recipe.rating).toBe(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ingredient Type', () => {
|
||||
it('should accept valid ingredient object', () => {
|
||||
const ingredient: Ingredient = {
|
||||
name: 'Flour',
|
||||
amount: '2',
|
||||
unit: 'cups',
|
||||
order: 0,
|
||||
};
|
||||
|
||||
expect(ingredient.name).toBe('Flour');
|
||||
expect(ingredient.amount).toBe('2');
|
||||
expect(ingredient.unit).toBe('cups');
|
||||
});
|
||||
|
||||
it('should allow optional notes', () => {
|
||||
const ingredient: Ingredient = {
|
||||
name: 'Sugar',
|
||||
order: 1,
|
||||
notes: 'Can substitute with honey',
|
||||
};
|
||||
|
||||
expect(ingredient.notes).toBe('Can substitute with honey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instruction Type', () => {
|
||||
it('should accept valid instruction object', () => {
|
||||
const instruction: Instruction = {
|
||||
step: 1,
|
||||
text: 'Mix all ingredients together',
|
||||
};
|
||||
|
||||
expect(instruction.step).toBe(1);
|
||||
expect(instruction.text).toBe('Mix all ingredients together');
|
||||
});
|
||||
|
||||
it('should allow optional imageUrl', () => {
|
||||
const instruction: Instruction = {
|
||||
step: 2,
|
||||
text: 'Bake for 30 minutes',
|
||||
imageUrl: '/uploads/instructions/step2.jpg',
|
||||
};
|
||||
|
||||
expect(instruction.imageUrl).toBe('/uploads/instructions/step2.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecipeImportRequest Type', () => {
|
||||
it('should accept valid import request', () => {
|
||||
const request: RecipeImportRequest = {
|
||||
url: 'https://example.com/recipe',
|
||||
};
|
||||
|
||||
expect(request.url).toBe('https://example.com/recipe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecipeImportResponse Type', () => {
|
||||
it('should accept successful import response', () => {
|
||||
const response: RecipeImportResponse = {
|
||||
success: true,
|
||||
recipe: {
|
||||
title: 'Imported Recipe',
|
||||
description: 'A recipe imported from URL',
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.recipe.title).toBe('Imported Recipe');
|
||||
});
|
||||
|
||||
it('should accept failed import response', () => {
|
||||
const response: RecipeImportResponse = {
|
||||
success: false,
|
||||
recipe: {},
|
||||
error: 'Failed to scrape recipe',
|
||||
};
|
||||
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error).toBe('Failed to scrape recipe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApiResponse Type', () => {
|
||||
it('should accept successful response with data', () => {
|
||||
const response: ApiResponse<Recipe> = {
|
||||
data: {
|
||||
id: '1',
|
||||
title: 'Test Recipe',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.data).toBeDefined();
|
||||
expect(response.data?.title).toBe('Test Recipe');
|
||||
});
|
||||
|
||||
it('should accept error response', () => {
|
||||
const response: ApiResponse<Recipe> = {
|
||||
error: 'Recipe not found',
|
||||
};
|
||||
|
||||
expect(response.error).toBe('Recipe not found');
|
||||
expect(response.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaginatedResponse Type', () => {
|
||||
it('should accept valid paginated response', () => {
|
||||
const response: PaginatedResponse<Recipe> = {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Recipe 1',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
total: 100,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
};
|
||||
|
||||
expect(response.data).toHaveLength(1);
|
||||
expect(response.total).toBe(100);
|
||||
expect(response.page).toBe(1);
|
||||
expect(response.pageSize).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StorageConfig Type', () => {
|
||||
it('should accept local storage config', () => {
|
||||
const config: StorageConfig = {
|
||||
type: 'local',
|
||||
localPath: './uploads',
|
||||
};
|
||||
|
||||
expect(config.type).toBe('local');
|
||||
expect(config.localPath).toBe('./uploads');
|
||||
});
|
||||
|
||||
it('should accept S3 storage config', () => {
|
||||
const config: StorageConfig = {
|
||||
type: 's3',
|
||||
s3Bucket: 'basil-recipes',
|
||||
s3Region: 'us-east-1',
|
||||
s3AccessKey: 'test-key',
|
||||
s3SecretKey: 'test-secret',
|
||||
};
|
||||
|
||||
expect(config.type).toBe('s3');
|
||||
expect(config.s3Bucket).toBe('basil-recipes');
|
||||
expect(config.s3Region).toBe('us-east-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Type guard helper functions (useful utilities to test)
|
||||
describe('Type Guard Utilities', () => {
|
||||
const isRecipe = (obj: any): obj is Recipe => {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
typeof obj.id === 'string' &&
|
||||
typeof obj.title === 'string' &&
|
||||
Array.isArray(obj.ingredients) &&
|
||||
Array.isArray(obj.instructions)
|
||||
);
|
||||
};
|
||||
|
||||
const isSuccessResponse = <T>(response: ApiResponse<T>): response is { data: T } => {
|
||||
return response.data !== undefined && response.error === undefined;
|
||||
};
|
||||
|
||||
it('should validate recipe objects with type guard', () => {
|
||||
const validRecipe = {
|
||||
id: '1',
|
||||
title: 'Test',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const invalidRecipe = {
|
||||
id: 1, // wrong type
|
||||
title: 'Test',
|
||||
};
|
||||
|
||||
expect(isRecipe(validRecipe)).toBe(true);
|
||||
expect(isRecipe(invalidRecipe)).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate success responses with type guard', () => {
|
||||
const successResponse: ApiResponse<string> = {
|
||||
data: 'Success',
|
||||
};
|
||||
|
||||
const errorResponse: ApiResponse<string> = {
|
||||
error: 'Failed',
|
||||
};
|
||||
|
||||
expect(isSuccessResponse(successResponse)).toBe(true);
|
||||
expect(isSuccessResponse(errorResponse)).toBe(false);
|
||||
});
|
||||
});
|
||||
25
packages/shared/vitest.config.ts
Normal file
25
packages/shared/vitest.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'**/*.config.ts',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user