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:
224
packages/api/src/routes/recipes.routes.test.ts
Normal file
224
packages/api/src/routes/recipes.routes.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import recipesRouter from './recipes.routes';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../config/database', () => ({
|
||||
default: {
|
||||
recipe: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
recipeImage: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
ingredient: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
instruction: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
recipeTag: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../services/storage.service', () => ({
|
||||
StorageService: {
|
||||
getInstance: vi.fn(() => ({
|
||||
saveFile: vi.fn().mockResolvedValue('/uploads/recipes/test-image.jpg'),
|
||||
deleteFile: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../services/scraper.service', () => ({
|
||||
ScraperService: vi.fn(() => ({
|
||||
scrapeRecipe: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
recipe: {
|
||||
title: 'Scraped Recipe',
|
||||
description: 'A scraped recipe',
|
||||
sourceUrl: 'https://example.com/recipe',
|
||||
},
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Recipes Routes - Integration Tests', () => {
|
||||
let app: express.Application;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/recipes', recipesRouter);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /recipes', () => {
|
||||
it('should return paginated recipes', async () => {
|
||||
const mockRecipes = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Recipe 1',
|
||||
description: 'Description 1',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
images: [],
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findMany).mockResolvedValue(mockRecipes as any);
|
||||
vi.mocked(prisma.default.recipe.count).mockResolvedValue(1);
|
||||
|
||||
const response = await request(app).get('/recipes').expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('data');
|
||||
expect(response.body).toHaveProperty('total', 1);
|
||||
expect(response.body).toHaveProperty('page', 1);
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should support search query parameter', async () => {
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.default.recipe.count).mockResolvedValue(0);
|
||||
|
||||
await request(app).get('/recipes?search=pasta').expect(200);
|
||||
|
||||
expect(prisma.default.recipe.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: expect.any(Array),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should support pagination parameters', async () => {
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.default.recipe.count).mockResolvedValue(0);
|
||||
|
||||
await request(app).get('/recipes?page=2&limit=10').expect(200);
|
||||
|
||||
expect(prisma.default.recipe.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
skip: 10,
|
||||
take: 10,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /recipes/:id', () => {
|
||||
it('should return single recipe by id', async () => {
|
||||
const mockRecipe = {
|
||||
id: '1',
|
||||
title: 'Test Recipe',
|
||||
description: 'Test Description',
|
||||
ingredients: [],
|
||||
instructions: [],
|
||||
images: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findUnique).mockResolvedValue(mockRecipe as any);
|
||||
|
||||
const response = await request(app).get('/recipes/1').expect(200);
|
||||
|
||||
expect(response.body.data).toHaveProperty('title', 'Test Recipe');
|
||||
});
|
||||
|
||||
it('should return 404 when recipe not found', async () => {
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findUnique).mockResolvedValue(null);
|
||||
|
||||
const response = await request(app).get('/recipes/nonexistent').expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error', 'Recipe not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /recipes', () => {
|
||||
it('should create new recipe', async () => {
|
||||
const newRecipe = {
|
||||
title: 'New Recipe',
|
||||
description: 'New Description',
|
||||
ingredients: [{ name: 'Flour', amount: '2 cups' }],
|
||||
instructions: [{ step: 1, text: 'Mix ingredients' }],
|
||||
};
|
||||
|
||||
const mockCreatedRecipe = {
|
||||
id: '1',
|
||||
...newRecipe,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.create).mockResolvedValue(mockCreatedRecipe as any);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/recipes')
|
||||
.send(newRecipe)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.data).toHaveProperty('title', 'New Recipe');
|
||||
expect(prisma.default.recipe.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /recipes/import', () => {
|
||||
it('should import recipe from URL', async () => {
|
||||
const response = await request(app)
|
||||
.post('/recipes/import')
|
||||
.send({ url: 'https://example.com/recipe' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.recipe).toHaveProperty('title', 'Scraped Recipe');
|
||||
});
|
||||
|
||||
it('should return 400 when URL is missing', async () => {
|
||||
const response = await request(app).post('/recipes/import').send({}).expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error', 'URL is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /recipes/:id', () => {
|
||||
it('should delete recipe and associated images', async () => {
|
||||
const mockRecipe = {
|
||||
id: '1',
|
||||
title: 'Recipe to Delete',
|
||||
imageUrl: '/uploads/recipes/main.jpg',
|
||||
images: [{ url: '/uploads/recipes/image1.jpg' }],
|
||||
};
|
||||
|
||||
const prisma = await import('../config/database');
|
||||
vi.mocked(prisma.default.recipe.findUnique).mockResolvedValue(mockRecipe as any);
|
||||
vi.mocked(prisma.default.recipe.delete).mockResolvedValue(mockRecipe as any);
|
||||
|
||||
const response = await request(app).delete('/recipes/1').expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message', 'Recipe deleted successfully');
|
||||
expect(prisma.default.recipe.delete).toHaveBeenCalledWith({
|
||||
where: { id: '1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user