@oalacea/daemon 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/Dockerfile +4 -4
- package/dist/prompts/DEPS_EFFICIENCY.md +558 -0
- package/dist/prompts/E2E.md +491 -0
- package/dist/prompts/EXECUTE.md +1060 -0
- package/dist/prompts/INTEGRATION_API.md +484 -0
- package/dist/prompts/INTEGRATION_DB.md +425 -0
- package/dist/prompts/PERF_API.md +433 -0
- package/dist/prompts/PERF_DB.md +430 -0
- package/dist/prompts/PERF_FRONT.md +357 -0
- package/dist/prompts/REMEDIATION.md +482 -0
- package/dist/prompts/UNIT.md +260 -0
- package/dist/templates/README.md +221 -0
- package/dist/templates/k6/load-test.js +54 -0
- package/dist/templates/nestjs/controller.spec.ts +203 -0
- package/dist/templates/nestjs/e2e/api.e2e-spec.ts +451 -0
- package/dist/templates/nestjs/e2e/auth.e2e-spec.ts +533 -0
- package/dist/templates/nestjs/fixtures/test-module.ts +311 -0
- package/dist/templates/nestjs/guard.spec.ts +314 -0
- package/dist/templates/nestjs/interceptor.spec.ts +458 -0
- package/dist/templates/nestjs/module.spec.ts +173 -0
- package/dist/templates/nestjs/pipe.spec.ts +474 -0
- package/dist/templates/nestjs/service.spec.ts +296 -0
- package/dist/templates/playwright/e2e.spec.ts +61 -0
- package/dist/templates/rust/Cargo.toml +72 -0
- package/dist/templates/rust/actix-controller.test.rs +114 -0
- package/dist/templates/rust/axum-handler.test.rs +117 -0
- package/dist/templates/rust/integration.test.rs +63 -0
- package/dist/templates/rust/rocket-route.test.rs +106 -0
- package/dist/templates/rust/unit.test.rs +38 -0
- package/dist/templates/vitest/angular-component.test.ts +38 -0
- package/dist/templates/vitest/api.test.ts +51 -0
- package/dist/templates/vitest/component.test.ts +27 -0
- package/dist/templates/vitest/hook.test.ts +36 -0
- package/dist/templates/vitest/solid-component.test.ts +34 -0
- package/dist/templates/vitest/svelte-component.test.ts +33 -0
- package/dist/templates/vitest/vue-component.test.ts +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS Service Test Template
|
|
3
|
+
*
|
|
4
|
+
* Tests for NestJS services following best practices:
|
|
5
|
+
* - Business logic coverage
|
|
6
|
+
* - Repository/database mocking
|
|
7
|
+
* - Error handling
|
|
8
|
+
* - Edge cases
|
|
9
|
+
*
|
|
10
|
+
* @package test
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
14
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
15
|
+
import { Repository } from 'typeorm';
|
|
16
|
+
import { GameService } from './game.service';
|
|
17
|
+
import { Game } from './entities/game.entity';
|
|
18
|
+
import { CreateGameDto, UpdateGameDto } from './dto';
|
|
19
|
+
import { NotFoundException, ConflictException } from '@nestjs/common';
|
|
20
|
+
|
|
21
|
+
describe('GameService', () => {
|
|
22
|
+
let service: GameService;
|
|
23
|
+
let repository: Repository<Game>;
|
|
24
|
+
|
|
25
|
+
// Mock repository
|
|
26
|
+
const mockRepository = {
|
|
27
|
+
find: jest.fn(),
|
|
28
|
+
findOne: jest.fn(),
|
|
29
|
+
create: jest.fn(),
|
|
30
|
+
save: jest.fn(),
|
|
31
|
+
update: jest.fn(),
|
|
32
|
+
delete: jest.fn(),
|
|
33
|
+
findOneBy: jest.fn(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
38
|
+
providers: [
|
|
39
|
+
GameService,
|
|
40
|
+
{
|
|
41
|
+
provide: getRepositoryToken(Game),
|
|
42
|
+
useValue: mockRepository,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}).compile();
|
|
46
|
+
|
|
47
|
+
service = module.get<GameService>(GameService);
|
|
48
|
+
repository = module.get<Repository<Game>>(getRepositoryToken(Game));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.clearAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should be defined', () => {
|
|
56
|
+
expect(service).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('findAll', () => {
|
|
60
|
+
it('should return an array of games', async () => {
|
|
61
|
+
const expectedGames = [
|
|
62
|
+
{ id: '1', name: 'Game 1', active: true },
|
|
63
|
+
{ id: '2', name: 'Game 2', active: true },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
mockRepository.find.mockResolvedValue(expectedGames);
|
|
67
|
+
|
|
68
|
+
const result = await service.findAll();
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual(expectedGames);
|
|
71
|
+
expect(repository.find).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(repository.find).toHaveBeenCalledWith();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should support filtering options', async () => {
|
|
76
|
+
const expectedGames = [{ id: '1', name: 'Game 1', active: true }];
|
|
77
|
+
const options = { where: { active: true } };
|
|
78
|
+
|
|
79
|
+
mockRepository.find.mockResolvedValue(expectedGames);
|
|
80
|
+
|
|
81
|
+
const result = await service.findAll(options);
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual(expectedGames);
|
|
84
|
+
expect(repository.find).toHaveBeenCalledWith(options);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle empty results', async () => {
|
|
88
|
+
mockRepository.find.mockResolvedValue([]);
|
|
89
|
+
|
|
90
|
+
const result = await service.findAll();
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle database errors', async () => {
|
|
96
|
+
mockRepository.find.mockRejectedValue(new Error('Database error'));
|
|
97
|
+
|
|
98
|
+
await expect(service.findAll()).rejects.toThrow('Database error');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('findOne', () => {
|
|
103
|
+
it('should return a single game by id', async () => {
|
|
104
|
+
const expectedGame = { id: '1', name: 'Game 1', active: true };
|
|
105
|
+
|
|
106
|
+
mockRepository.findOneBy.mockResolvedValue(expectedGame);
|
|
107
|
+
|
|
108
|
+
const result = await service.findOne('1');
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual(expectedGame);
|
|
111
|
+
expect(repository.findOneBy).toHaveBeenCalledWith({ id: '1' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should throw NotFoundException when game not found', async () => {
|
|
115
|
+
mockRepository.findOneBy.mockResolvedValue(null);
|
|
116
|
+
|
|
117
|
+
await expect(service.findOne('999')).rejects.toThrow(NotFoundException);
|
|
118
|
+
await expect(service.findOne('999')).rejects.toThrow('Game not found');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle database errors', async () => {
|
|
122
|
+
mockRepository.findOneBy.mockRejectedValue(new Error('Database error'));
|
|
123
|
+
|
|
124
|
+
await expect(service.findOne('1')).rejects.toThrow('Database error');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('create', () => {
|
|
129
|
+
const createGameDto: CreateGameDto = {
|
|
130
|
+
name: 'New Game',
|
|
131
|
+
type: 'action',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
it('should create a new game', async () => {
|
|
135
|
+
const savedGame = { id: '1', ...createGameDto };
|
|
136
|
+
|
|
137
|
+
mockRepository.create.mockReturnValue(savedGame);
|
|
138
|
+
mockRepository.save.mockResolvedValue(savedGame);
|
|
139
|
+
|
|
140
|
+
const result = await service.create(createGameDto);
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual(savedGame);
|
|
143
|
+
expect(repository.create).toHaveBeenCalledWith(createGameDto);
|
|
144
|
+
expect(repository.save).toHaveBeenCalledWith(savedGame);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle unique constraint violations', async () => {
|
|
148
|
+
const duplicateError = { code: '23505' }; // PostgreSQL unique violation
|
|
149
|
+
|
|
150
|
+
mockRepository.create.mockReturnValue(createGameDto);
|
|
151
|
+
mockRepository.save.mockRejectedValue(duplicateError);
|
|
152
|
+
|
|
153
|
+
await expect(service.create(createGameDto)).rejects.toThrow(
|
|
154
|
+
ConflictException
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should validate DTO before creation', async () => {
|
|
159
|
+
const invalidDto = { name: '' }; // Invalid DTO
|
|
160
|
+
|
|
161
|
+
mockRepository.create.mockReturnValue(invalidDto);
|
|
162
|
+
mockRepository.save.mockResolvedValue(invalidDto);
|
|
163
|
+
|
|
164
|
+
// Validation should happen before repository call
|
|
165
|
+
// This test assumes class-validator is used
|
|
166
|
+
const result = await service.create(invalidDto as any);
|
|
167
|
+
|
|
168
|
+
expect(result).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('update', () => {
|
|
173
|
+
const updateGameDto: UpdateGameDto = {
|
|
174
|
+
name: 'Updated Game',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
it('should update an existing game', async () => {
|
|
178
|
+
const existingGame = { id: '1', name: 'Old Name', active: true };
|
|
179
|
+
const updatedGame = { ...existingGame, ...updateGameDto };
|
|
180
|
+
|
|
181
|
+
mockRepository.findOneBy.mockResolvedValue(existingGame);
|
|
182
|
+
mockRepository.save.mockResolvedValue(updatedGame);
|
|
183
|
+
|
|
184
|
+
const result = await service.update('1', updateGameDto);
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual(updatedGame);
|
|
187
|
+
expect(repository.findOneBy).toHaveBeenCalledWith({ id: '1' });
|
|
188
|
+
expect(repository.save).toHaveBeenCalledWith(updatedGame);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should throw NotFoundException when updating non-existent game', async () => {
|
|
192
|
+
mockRepository.findOneBy.mockResolvedValue(null);
|
|
193
|
+
|
|
194
|
+
await expect(service.update('999', updateGameDto)).rejects.toThrow(
|
|
195
|
+
NotFoundException
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle partial updates', async () => {
|
|
200
|
+
const existingGame = {
|
|
201
|
+
id: '1',
|
|
202
|
+
name: 'Game Name',
|
|
203
|
+
type: 'action',
|
|
204
|
+
active: true,
|
|
205
|
+
};
|
|
206
|
+
const partialDto: UpdateGameDto = { name: 'New Name' };
|
|
207
|
+
|
|
208
|
+
mockRepository.findOneBy.mockResolvedValue(existingGame);
|
|
209
|
+
mockRepository.save.mockResolvedValue({
|
|
210
|
+
...existingGame,
|
|
211
|
+
...partialDto,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await service.update('1', partialDto);
|
|
215
|
+
|
|
216
|
+
expect(result.name).toBe('New Name');
|
|
217
|
+
expect(result.type).toBe('action'); // Unchanged
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('remove', () => {
|
|
222
|
+
it('should delete a game', async () => {
|
|
223
|
+
const existingGame = { id: '1', name: 'Game 1', active: true };
|
|
224
|
+
|
|
225
|
+
mockRepository.findOneBy.mockResolvedValue(existingGame);
|
|
226
|
+
mockRepository.delete.mockResolvedValue({ affected: 1 });
|
|
227
|
+
|
|
228
|
+
await service.remove('1');
|
|
229
|
+
|
|
230
|
+
expect(repository.findOneBy).toHaveBeenCalledWith({ id: '1' });
|
|
231
|
+
expect(repository.delete).toHaveBeenCalledWith('1');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw NotFoundException when deleting non-existent game', async () => {
|
|
235
|
+
mockRepository.findOneBy.mockResolvedValue(null);
|
|
236
|
+
|
|
237
|
+
await expect(service.remove('999')).rejects.toThrow(NotFoundException);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should handle delete with no affected rows', async () => {
|
|
241
|
+
const existingGame = { id: '1', name: 'Game 1', active: true };
|
|
242
|
+
|
|
243
|
+
mockRepository.findOneBy.mockResolvedValue(existingGame);
|
|
244
|
+
mockRepository.delete.mockResolvedValue({ affected: 0 });
|
|
245
|
+
|
|
246
|
+
await service.remove('1');
|
|
247
|
+
|
|
248
|
+
// Should not throw even if no rows affected
|
|
249
|
+
expect(repository.delete).toHaveBeenCalledWith('1');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('business logic', () => {
|
|
254
|
+
it('should calculate derived properties correctly', async () => {
|
|
255
|
+
const games = [
|
|
256
|
+
{ id: '1', name: 'Game 1', score: 100 },
|
|
257
|
+
{ id: '2', name: 'Game 2', score: 200 },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
mockRepository.find.mockResolvedValue(games);
|
|
261
|
+
|
|
262
|
+
const result = await service.getHighScoreGames();
|
|
263
|
+
|
|
264
|
+
expect(result).toBeDefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should enforce business rules', async () => {
|
|
268
|
+
const createGameDto: CreateGameDto = {
|
|
269
|
+
name: 'Game with invalid business rule',
|
|
270
|
+
type: 'invalid',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Business rule validation
|
|
274
|
+
mockRepository.create.mockReturnValue(createGameDto);
|
|
275
|
+
mockRepository.save.mockResolvedValue(createGameDto);
|
|
276
|
+
|
|
277
|
+
const result = await service.create(createGameDto);
|
|
278
|
+
|
|
279
|
+
expect(result).toBeDefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('transactions', () => {
|
|
284
|
+
it('should handle operations requiring transactions', async () => {
|
|
285
|
+
// For complex operations requiring multiple updates
|
|
286
|
+
mockRepository.findOneBy.mockResolvedValue({ id: '1', name: 'Game' });
|
|
287
|
+
mockRepository.save.mockResolvedValue({ id: '1', name: 'Updated' });
|
|
288
|
+
|
|
289
|
+
const result = await service.complexUpdate('1', {
|
|
290
|
+
name: 'Updated',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('Authentication Flow', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto('/login');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('should login with valid credentials', async ({ page }) => {
|
|
9
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
10
|
+
await page.fill('input[name="password"]', 'password123');
|
|
11
|
+
await page.click('button[type="submit"]');
|
|
12
|
+
|
|
13
|
+
// Should redirect to dashboard
|
|
14
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
15
|
+
await expect(page.locator('h1')).toContainText('Dashboard');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should show error with invalid credentials', async ({ page }) => {
|
|
19
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
20
|
+
await page.fill('input[name="password"]', 'wrong-password');
|
|
21
|
+
await page.click('button[type="submit"]');
|
|
22
|
+
|
|
23
|
+
await expect(page.locator('.error')).toContainText('Invalid credentials');
|
|
24
|
+
await expect(page).toHaveURL('/login');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test.describe('Navigation', () => {
|
|
29
|
+
test('should navigate between pages', async ({ page }) => {
|
|
30
|
+
await page.goto('/');
|
|
31
|
+
|
|
32
|
+
await page.click('text=About');
|
|
33
|
+
await expect(page).toHaveURL('/about');
|
|
34
|
+
|
|
35
|
+
await page.goBack();
|
|
36
|
+
await expect(page).toHaveURL('/');
|
|
37
|
+
|
|
38
|
+
await page.goForward();
|
|
39
|
+
await expect(page).toHaveURL('/about');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test.describe('Form Interaction', () => {
|
|
44
|
+
test('should submit form successfully', async ({ page }) => {
|
|
45
|
+
await page.goto('/form');
|
|
46
|
+
|
|
47
|
+
await page.fill('input[name="name"]', 'Test User');
|
|
48
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
49
|
+
await page.click('button[type="submit"]');
|
|
50
|
+
|
|
51
|
+
await expect(page.locator('.success')).toContainText('Form submitted');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should show validation errors', async ({ page }) => {
|
|
55
|
+
await page.goto('/form');
|
|
56
|
+
await page.click('button[type="submit"]');
|
|
57
|
+
|
|
58
|
+
await expect(page.locator('input[name="name"]')).toHaveAttribute('aria-invalid', 'true');
|
|
59
|
+
await expect(page.locator('input[name="email"]')).toHaveAttribute('aria-invalid', 'true');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "my-project"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
# Web frameworks (uncomment the one you're using)
|
|
8
|
+
# axum = "0.7"
|
|
9
|
+
# actix-web = "4.9"
|
|
10
|
+
# rocket = "0.5"
|
|
11
|
+
|
|
12
|
+
# Async runtime
|
|
13
|
+
tokio = { version = "1.40", features = ["full"] }
|
|
14
|
+
|
|
15
|
+
# Serialization
|
|
16
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
17
|
+
serde_json = "1.0"
|
|
18
|
+
|
|
19
|
+
# Database (uncomment as needed)
|
|
20
|
+
# sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "sqlite", "mysql"] }
|
|
21
|
+
# diesel = { version = "2.2", features = ["postgres", "sqlite"] }
|
|
22
|
+
|
|
23
|
+
# HTTP client (if needed)
|
|
24
|
+
# reqwest = { version = "0.12", features = ["json"] }
|
|
25
|
+
|
|
26
|
+
# Environment variables
|
|
27
|
+
# dotenv = "0.15"
|
|
28
|
+
|
|
29
|
+
# Logging
|
|
30
|
+
# tracing = "0.1"
|
|
31
|
+
# tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
32
|
+
|
|
33
|
+
# Error handling
|
|
34
|
+
# anyhow = "1.0"
|
|
35
|
+
# thiserror = "1.0"
|
|
36
|
+
|
|
37
|
+
[dev-dependencies]
|
|
38
|
+
# Testing utilities
|
|
39
|
+
tokio-test = "0.4"
|
|
40
|
+
|
|
41
|
+
# HTTP testing for Axum
|
|
42
|
+
# axum-test = "16.0"
|
|
43
|
+
|
|
44
|
+
# HTTP testing for Actix
|
|
45
|
+
# actix-rt = "2.10"
|
|
46
|
+
|
|
47
|
+
# Database testing
|
|
48
|
+
# sqlx-cli = "0.8"
|
|
49
|
+
|
|
50
|
+
# Mocking
|
|
51
|
+
# mockall = "0.13"
|
|
52
|
+
|
|
53
|
+
[features]
|
|
54
|
+
# Default features
|
|
55
|
+
default = []
|
|
56
|
+
|
|
57
|
+
# Test features
|
|
58
|
+
test-utils = []
|
|
59
|
+
|
|
60
|
+
[profile.dev]
|
|
61
|
+
# Optimize dependencies in dev mode for faster compilation
|
|
62
|
+
opt-level = 0
|
|
63
|
+
|
|
64
|
+
[profile.release]
|
|
65
|
+
# Optimize for size
|
|
66
|
+
opt-level = "z"
|
|
67
|
+
lto = true
|
|
68
|
+
codegen-units = 1
|
|
69
|
+
|
|
70
|
+
[profile.test]
|
|
71
|
+
# Optimize tests for speed
|
|
72
|
+
opt-level = 3
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#[cfg(test)]
|
|
2
|
+
mod tests {
|
|
3
|
+
use super::*;
|
|
4
|
+
use actix_web::{
|
|
5
|
+
body::MessageBody,
|
|
6
|
+
dev::{Service, ServiceResponse},
|
|
7
|
+
http::{header, StatusCode},
|
|
8
|
+
test, web, App,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/// Helper function to create test app
|
|
12
|
+
async fn create_app() -> App<
|
|
13
|
+
impl Service<
|
|
14
|
+
actix_web::dev::ServiceRequest,
|
|
15
|
+
Response = ServiceResponse<impl MessageBody>,
|
|
16
|
+
Error = actix_web::Error,
|
|
17
|
+
>,
|
|
18
|
+
> {
|
|
19
|
+
test::init_service(
|
|
20
|
+
App::new()
|
|
21
|
+
.route("/health", web::get().to(health_check))
|
|
22
|
+
.route("/api/users", web::get().to(get_users).post(create_user)),
|
|
23
|
+
)
|
|
24
|
+
.await
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Test health check endpoint
|
|
28
|
+
#[actix_web::test]
|
|
29
|
+
async fn test_health_check() {
|
|
30
|
+
let app = create_app().await;
|
|
31
|
+
let req = test::TestRequest::get().uri("/health").to_request();
|
|
32
|
+
|
|
33
|
+
let resp = test::call_service(&app, req).await;
|
|
34
|
+
assert_eq!(resp.status(), StatusCode::OK);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Test GET /api/users returns 200
|
|
38
|
+
#[actix_web::test]
|
|
39
|
+
async fn test_get_users_returns_200() {
|
|
40
|
+
let app = create_app().await;
|
|
41
|
+
let req = test::TestRequest::get()
|
|
42
|
+
.uri("/api/users")
|
|
43
|
+
.to_request();
|
|
44
|
+
|
|
45
|
+
let resp = test::call_service(&app, req).await;
|
|
46
|
+
assert_eq!(resp.status(), StatusCode::OK);
|
|
47
|
+
|
|
48
|
+
// Optionally check response body
|
|
49
|
+
let body = test::read_body(resp).await;
|
|
50
|
+
assert!(!body.is_empty());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Test GET /api/users with pagination
|
|
54
|
+
#[actix_web::test]
|
|
55
|
+
async fn test_get_users_with_pagination() {
|
|
56
|
+
let app = create_app().await;
|
|
57
|
+
let req = test::TestRequest::get()
|
|
58
|
+
.uri("/api/users?page=1&limit=10")
|
|
59
|
+
.to_request();
|
|
60
|
+
|
|
61
|
+
let resp = test::call_service(&app, req).await;
|
|
62
|
+
assert_eq!(resp.status(), StatusCode::OK);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Test POST /api/users with valid data returns 201
|
|
66
|
+
#[actix_web::test]
|
|
67
|
+
async fn test_create_user_with_valid_data_returns_201() {
|
|
68
|
+
let app = create_app().await;
|
|
69
|
+
let payload = serde_json::json!({
|
|
70
|
+
"name": "Test User",
|
|
71
|
+
"email": "test@example.com"
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let req = test::TestRequest::post()
|
|
75
|
+
.uri("/api/users")
|
|
76
|
+
.insert_header((header::CONTENT_TYPE, "application/json"))
|
|
77
|
+
.set_json(&payload)
|
|
78
|
+
.to_request();
|
|
79
|
+
|
|
80
|
+
let resp = test::call_service(&app, req).await;
|
|
81
|
+
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Test POST /api/users with invalid data returns 400
|
|
85
|
+
#[actix_web::test]
|
|
86
|
+
async fn test_create_user_with_invalid_data_returns_400() {
|
|
87
|
+
let app = create_app().await;
|
|
88
|
+
let payload = serde_json::json!({
|
|
89
|
+
"name": ""
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let req = test::TestRequest::post()
|
|
93
|
+
.uri("/api/users")
|
|
94
|
+
.insert_header((header::CONTENT_TYPE, "application/json"))
|
|
95
|
+
.set_json(&payload)
|
|
96
|
+
.to_request();
|
|
97
|
+
|
|
98
|
+
let resp = test::call_service(&app, req).await;
|
|
99
|
+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// TODO: Implement handler functions
|
|
103
|
+
async fn health_check() -> &'static str {
|
|
104
|
+
"OK"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async fn get_users() -> &'static str {
|
|
108
|
+
"[]"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async fn create_user() -> &'static str {
|
|
112
|
+
"Created"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#[cfg(test)]
|
|
2
|
+
mod tests {
|
|
3
|
+
use super::*;
|
|
4
|
+
use axum::{
|
|
5
|
+
body::Body,
|
|
6
|
+
http::{Method, Request, StatusCode},
|
|
7
|
+
};
|
|
8
|
+
use tower::ServiceExt;
|
|
9
|
+
|
|
10
|
+
/// Helper function to create test app
|
|
11
|
+
async fn create_app() -> Router {
|
|
12
|
+
Router::new()
|
|
13
|
+
.route("/health", get(health_check))
|
|
14
|
+
.route("/api/users", get(get_users).post(create_user))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Test health check endpoint
|
|
18
|
+
#[tokio::test]
|
|
19
|
+
async fn test_health_check() {
|
|
20
|
+
let app = create_app().await;
|
|
21
|
+
|
|
22
|
+
let response = app
|
|
23
|
+
.oneshot(
|
|
24
|
+
Request::builder()
|
|
25
|
+
.uri("/health")
|
|
26
|
+
.body(Body::empty())
|
|
27
|
+
.unwrap(),
|
|
28
|
+
)
|
|
29
|
+
.await
|
|
30
|
+
.unwrap();
|
|
31
|
+
|
|
32
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Test GET /api/users returns 200
|
|
36
|
+
#[tokio::test]
|
|
37
|
+
async fn test_get_users_returns_200() {
|
|
38
|
+
let app = create_app().await;
|
|
39
|
+
|
|
40
|
+
let response = app
|
|
41
|
+
.oneshot(
|
|
42
|
+
Request::builder()
|
|
43
|
+
.method(Method::GET)
|
|
44
|
+
.uri("/api/users")
|
|
45
|
+
.body(Body::empty())
|
|
46
|
+
.unwrap(),
|
|
47
|
+
)
|
|
48
|
+
.await
|
|
49
|
+
.unwrap();
|
|
50
|
+
|
|
51
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Test POST /api/users with valid data returns 201
|
|
55
|
+
#[tokio::test]
|
|
56
|
+
async fn test_create_user_with_valid_data_returns_201() {
|
|
57
|
+
let app = create_app().await;
|
|
58
|
+
|
|
59
|
+
let body = serde_json::json!({
|
|
60
|
+
"name": "Test User",
|
|
61
|
+
"email": "test@example.com"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let response = app
|
|
65
|
+
.oneshot(
|
|
66
|
+
Request::builder()
|
|
67
|
+
.method(Method::POST)
|
|
68
|
+
.uri("/api/users")
|
|
69
|
+
.header("content-type", "application/json")
|
|
70
|
+
.body(Body::from(body.to_string()))
|
|
71
|
+
.unwrap(),
|
|
72
|
+
)
|
|
73
|
+
.await
|
|
74
|
+
.unwrap();
|
|
75
|
+
|
|
76
|
+
assert_eq!(response.status(), StatusCode::CREATED);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Test POST /api/users with invalid data returns 400
|
|
80
|
+
#[tokio::test]
|
|
81
|
+
async fn test_create_user_with_invalid_data_returns_400() {
|
|
82
|
+
let app = create_app().await;
|
|
83
|
+
|
|
84
|
+
let body = serde_json::json!({
|
|
85
|
+
"name": ""
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let response = app
|
|
89
|
+
.oneshot(
|
|
90
|
+
Request::builder()
|
|
91
|
+
.method(Method::POST)
|
|
92
|
+
.uri("/api/users")
|
|
93
|
+
.header("content-type", "application/json")
|
|
94
|
+
.body(Body::from(body.to_string()))
|
|
95
|
+
.unwrap(),
|
|
96
|
+
)
|
|
97
|
+
.await
|
|
98
|
+
.unwrap();
|
|
99
|
+
|
|
100
|
+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// TODO: Implement handler functions
|
|
104
|
+
async fn health_check() -> StatusCode {
|
|
105
|
+
StatusCode::OK
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async fn get_users() -> StatusCode {
|
|
109
|
+
StatusCode::OK
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async fn create_user() -> StatusCode {
|
|
113
|
+
StatusCode::CREATED
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
use axum::{routing::{get, post}, Router};
|
|
117
|
+
}
|