api: flesh tests for mood and user's crud

Pedro Lucas Porcellis porcellis@eletrotupi.com 6 months ago 56ae9d8272c35d408c95fb441f727bf93b0507d3
Parents: ba9ebee
8 file(s) changed
  • api/tests/mood.test.js +13 -5
  • api/tests/mood/creation.test.js +498 -0
  • api/tests/mood/queries.test.js +298 -0
  • api/tests/mood/updates.test.js +349 -0
  • api/tests/test-helper.js +21 -12
  • api/tests/users/authentication.test.js +147 -0
  • api/tests/users/creation.test.js +257 -0
  • api/tests/users/profile.test.js +246 -0
api/tests/mood.test.js
@@ -33,8 +33,10 @@ const res = await request(app).post("/mood").send({
33 33 user_id: userId,
34 34 stress_level: 3,
35 35 anxiety_level: 4,
36 + energy_level: 5,
36 37 title: "Bad day",
37 38 description: "Stressado com a faculdade",
39 + recorded_at: new Date().toISOString()
38 40 });
39 41
40 42 t.is(res.status, 201);
@@ -42,16 +44,18 @@ t.truthy(res.body.id);
42 44 });
43 45
44 46 test.serial("Listar humores do usuário", async (t) => {
45 - const res = await request(app).get(`/mood/${userId}`);
47 + const res = await request(app).get(`/mood/user/${userId}`);
46 48 t.is(res.status, 200);
47 - t.true(Array.isArray(res.body));
49 + t.true(Array.isArray(res.body.moods));
48 50 });
49 51
50 52 test.serial("Falha ao registrar humor sem stress_level", async (t) => {
51 53 const res = await request(app).post("/mood").send({
52 54 user_id: userId,
53 55 anxiety_level: 4,
56 + energy_level: 5,
54 57 title: "Test",
58 + recorded_at: new Date().toISOString()
55 59 });
56 60
57 61 t.is(res.status, 400);
@@ -67,8 +71,10 @@ const res1 = await request(app).post("/mood").send({
67 71 user_id: userId,
68 72 stress_level: 3,
69 73 anxiety_level: 4,
74 + energy_level: 6,
70 75 title: "Morning",
71 76 description: "Morning mood",
77 + recorded_at: new Date().toISOString()
72 78 });
73 79 t.is(res1.status, 201);
74 80
@@ -77,14 +83,16 @@ const res2 = await request(app).post("/mood").send({
77 83 user_id: userId,
78 84 stress_level: 2,
79 85 anxiety_level: 2,
86 + energy_level: 8,
80 87 title: "Evening",
81 88 description: "Evening mood",
89 + recorded_at: new Date().toISOString()
82 90 });
83 91 t.is(res2.status, 201);
84 92
85 - const res = await request(app).get(`/mood/${userId}`);
93 + const res = await request(app).get(`/mood/user/${userId}`);
86 94 t.is(res.status, 200);
87 - t.true(Array.isArray(res.body));
88 - t.is(res.body.length, 2);
95 + t.true(Array.isArray(res.body.moods));
96 + t.is(res.body.moods.length, 2);
89 97 });
90 98
api/tests/mood/creation.test.js
@@ -0,0 +1,498 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import { createApp } from "../../src/create-app.js";
4 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
5 +
6 + let app;
7 + let db;
8 + let userId;
9 +
10 + test.before(() => {
11 + db = setupTestDb();
12 + app = createApp(db);
13 + });
14 +
15 + test.beforeEach(async () => {
16 + cleanupTestDb();
17 +
18 + // Create a test user with unique email
19 + const uniqueEmail = `mood_${Date.now()}_${Math.random()}@example.com`;
20 + const res = await request(app)
21 + .post("/users")
22 + .send({
23 + email: uniqueEmail,
24 + password: "senha123",
25 + first_name: "Mood",
26 + last_name: "Tester"
27 + });
28 +
29 + if (res.status !== 201) {
30 + throw new Error(`Failed to create test user: ${JSON.stringify(res.body)}`);
31 + }
32 +
33 + userId = res.body.id;
34 + });
35 +
36 + test("Criar registro de humor com todos os campos obrigatórios", async (t) => {
37 + const now = new Date().toISOString();
38 +
39 + const res = await request(app)
40 + .post("/mood")
41 + .send({
42 + user_id: userId,
43 + stress_level: 3,
44 + anxiety_level: 4,
45 + energy_level: 7,
46 + recorded_at: now
47 + });
48 +
49 + t.is(res.status, 201);
50 + t.truthy(res.body.id);
51 + });
52 +
53 + test("Criar registro de humor com avaliação e descrição", async (t) => {
54 + const now = new Date().toISOString();
55 +
56 + const res = await request(app)
57 + .post("/mood")
58 + .send({
59 + user_id: userId,
60 + rating: 8,
61 + stress_level: 2,
62 + anxiety_level: 3,
63 + energy_level: 9,
64 + title: "Dia produtivo",
65 + description: "Consegui terminar todas as tarefas",
66 + recorded_at: now
67 + });
68 +
69 + t.is(res.status, 201);
70 +
71 + // Verify data was saved correctly
72 + const mood = db.prepare("SELECT * FROM mood WHERE id = ?").get(res.body.id);
73 + t.is(mood.rating, 8);
74 + t.is(mood.stress_level, 2);
75 + t.is(mood.title, "Dia produtivo");
76 + });
77 +
78 + test("Criar registro de humor com componentes emocionais", async (t) => {
79 + const now = new Date().toISOString();
80 +
81 + const res = await request(app)
82 + .post("/mood")
83 + .send({
84 + user_id: userId,
85 + stress_level: 5,
86 + anxiety_level: 6,
87 + energy_level: 4,
88 + recorded_at: now,
89 + mood_components: [
90 + { emotion: "joy", intensity: 7 },
91 + { emotion: "anxiety", intensity: 6 },
92 + { emotion: "trust", intensity: 8 }
93 + ]
94 + });
95 +
96 + t.is(res.status, 201);
97 +
98 + // Verify components were saved
99 + const components = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").all(res.body.id);
100 + t.is(components.length, 3);
101 +
102 + const emotions = components.map(c => c.emotion);
103 + t.true(emotions.includes('joy'));
104 + t.true(emotions.includes('anxiety'));
105 + t.true(emotions.includes('trust'));
106 + });
107 +
108 + test("Componentes emocionais devem aceitar maiúsculas e converter para minúsculas", async (t) => {
109 + const now = new Date().toISOString();
110 +
111 + const res = await request(app)
112 + .post("/mood")
113 + .send({
114 + user_id: userId,
115 + stress_level: 5,
116 + anxiety_level: 6,
117 + energy_level: 4,
118 + recorded_at: now,
119 + mood_components: [
120 + { emotion: "JOY", intensity: 7 },
121 + { emotion: "AnXiEtY", intensity: 6 }
122 + ]
123 + });
124 +
125 + t.is(res.status, 201);
126 +
127 + // Verify emotions were lowercased
128 + const components = db.prepare("SELECT emotion FROM mood_components WHERE mood_id = ? ORDER BY emotion ASC").all(res.body.id);
129 + t.is(components[0].emotion, 'anxiety');
130 + t.is(components[1].emotion, 'joy');
131 + });
132 +
133 + test("Falha ao criar registro de humor sem ID de usuário", async (t) => {
134 + const now = new Date().toISOString();
135 +
136 + const res = await request(app)
137 + .post("/mood")
138 + .send({
139 + stress_level: 3,
140 + anxiety_level: 4,
141 + energy_level: 7,
142 + recorded_at: now
143 + });
144 +
145 + t.is(res.status, 400);
146 + t.is(res.body.error, "Campos faltantes");
147 + t.true(res.body.missing.includes('user_id'));
148 + });
149 +
150 + test("Falha ao criar registro de humor sem nível de stress", async (t) => {
151 + const now = new Date().toISOString();
152 +
153 + const res = await request(app)
154 + .post("/mood")
155 + .send({
156 + user_id: userId,
157 + anxiety_level: 4,
158 + energy_level: 7,
159 + recorded_at: now
160 + });
161 +
162 + t.is(res.status, 400);
163 + t.is(res.body.error, "Campos faltantes");
164 + t.true(res.body.missing.includes('stress_level'));
165 + });
166 +
167 + test("Falha ao criar registro de humor sem nível de ansiedade", async (t) => {
168 + const now = new Date().toISOString();
169 +
170 + const res = await request(app)
171 + .post("/mood")
172 + .send({
173 + user_id: userId,
174 + stress_level: 3,
175 + energy_level: 7,
176 + recorded_at: now
177 + });
178 +
179 + t.is(res.status, 400);
180 + t.is(res.body.error, "Campos faltantes");
181 + t.true(res.body.missing.includes('anxiety_level'));
182 + });
183 +
184 + test("Falha ao criar registro de humor sem nível de energia", async (t) => {
185 + const now = new Date().toISOString();
186 +
187 + const res = await request(app)
188 + .post("/mood")
189 + .send({
190 + user_id: userId,
191 + stress_level: 3,
192 + anxiety_level: 4,
193 + recorded_at: now
194 + });
195 +
196 + t.is(res.status, 400);
197 + t.is(res.body.error, "Campos faltantes");
198 + t.true(res.body.missing.includes('energy_level'));
199 + });
200 +
201 + test("Falha ao criar registro de humor sem data de registro", async (t) => {
202 + const res = await request(app)
203 + .post("/mood")
204 + .send({
205 + user_id: userId,
206 + stress_level: 3,
207 + anxiety_level: 4,
208 + energy_level: 7
209 + });
210 +
211 + t.is(res.status, 400);
212 + t.is(res.body.error, "Campos faltantes");
213 + t.true(res.body.missing.includes('recorded_at'));
214 + });
215 +
216 + test("Falha ao criar registro de humor com nível de estresse inválido (menor que 1)", async (t) => {
217 + const now = new Date().toISOString();
218 +
219 + const res = await request(app)
220 + .post("/mood")
221 + .send({
222 + user_id: userId,
223 + stress_level: 0,
224 + anxiety_level: 4,
225 + energy_level: 7,
226 + recorded_at: now
227 + });
228 +
229 + t.is(res.status, 400);
230 + t.is(res.body.error, "stress_level deve ser entre 1 e 10");
231 + });
232 +
233 + test("Falha ao criar registro de humor com nível de estresse inválido (maior que 10)", async (t) => {
234 + const now = new Date().toISOString();
235 +
236 + const res = await request(app)
237 + .post("/mood")
238 + .send({
239 + user_id: userId,
240 + stress_level: 11,
241 + anxiety_level: 4,
242 + energy_level: 7,
243 + recorded_at: now
244 + });
245 +
246 + t.is(res.status, 400);
247 + t.is(res.body.error, "stress_level deve ser entre 1 e 10");
248 + });
249 +
250 + test("Falha ao criar registro de humor com nível de ansiedade inválido", async (t) => {
251 + const now = new Date().toISOString();
252 +
253 + const res = await request(app)
254 + .post("/mood")
255 + .send({
256 + user_id: userId,
257 + stress_level: 3,
258 + anxiety_level: 15,
259 + energy_level: 7,
260 + recorded_at: now
261 + });
262 +
263 + t.is(res.status, 400);
264 + t.is(res.body.error, "anxiety_level deve ser entre 1 e 10");
265 + });
266 +
267 + test("Falha ao criar registro de humor com nível de energia inválido", async (t) => {
268 + const now = new Date().toISOString();
269 +
270 + const res = await request(app)
271 + .post("/mood")
272 + .send({
273 + user_id: userId,
274 + stress_level: 3,
275 + anxiety_level: 4,
276 + energy_level: -5,
277 + recorded_at: now
278 + });
279 +
280 + t.is(res.status, 400);
281 + t.is(res.body.error, "energy_level deve ser entre 1 e 10");
282 + });
283 +
284 + test("Falha ao criar registro de humor com avaliação inválido", async (t) => {
285 + const now = new Date().toISOString();
286 +
287 + const res = await request(app)
288 + .post("/mood")
289 + .send({
290 + user_id: userId,
291 + rating: 20,
292 + stress_level: 3,
293 + anxiety_level: 4,
294 + energy_level: 7,
295 + recorded_at: now
296 + });
297 +
298 + t.is(res.status, 400);
299 + t.is(res.body.error, "rating deve ser entre 1 e 10");
300 + });
301 +
302 + test("Falha ao criar registro de humor com data inválida", async (t) => {
303 + const res = await request(app)
304 + .post("/mood")
305 + .send({
306 + user_id: userId,
307 + stress_level: 3,
308 + anxiety_level: 4,
309 + energy_level: 7,
310 + recorded_at: "data-invalida"
311 + });
312 +
313 + t.is(res.status, 400);
314 + t.is(res.body.error, "recorded_at deve ser uma data válida");
315 + });
316 +
317 + test("Falha ao criar registro de humor com data no futuro", async (t) => {
318 + const futureDate = new Date();
319 + futureDate.setDate(futureDate.getDate() + 1);
320 +
321 + const res = await request(app)
322 + .post("/mood")
323 + .send({
324 + user_id: userId,
325 + stress_level: 3,
326 + anxiety_level: 4,
327 + energy_level: 7,
328 + recorded_at: futureDate.toISOString()
329 + });
330 +
331 + t.is(res.status, 400);
332 + t.is(res.body.error, "recorded_at não pode ser no futuro");
333 + });
334 +
335 + test("Falha ao criar registro de humor com emoção inválida", async (t) => {
336 + const now = new Date().toISOString();
337 +
338 + const res = await request(app)
339 + .post("/mood")
340 + .send({
341 + user_id: userId,
342 + stress_level: 3,
343 + anxiety_level: 4,
344 + energy_level: 7,
345 + recorded_at: now,
346 + mood_components: [
347 + { emotion: "invalid_emotion", intensity: 5 }
348 + ]
349 + });
350 +
351 + t.is(res.status, 400);
352 + t.regex(res.body.error, /Invalid emotion/);
353 + });
354 +
355 + test("Falha ao criar registro de humor com intensidade inválida (menor que 1)", async (t) => {
356 + const now = new Date().toISOString();
357 +
358 + const res = await request(app)
359 + .post("/mood")
360 + .send({
361 + user_id: userId,
362 + stress_level: 3,
363 + anxiety_level: 4,
364 + energy_level: 7,
365 + recorded_at: now,
366 + mood_components: [
367 + { emotion: "joy", intensity: 0 }
368 + ]
369 + });
370 +
371 + t.is(res.status, 400);
372 + t.regex(res.body.error, /Invalid intensity/);
373 + });
374 +
375 + test("Falha ao criar registro de humor com intensidade inválida (maior que 10)", async (t) => {
376 + const now = new Date().toISOString();
377 +
378 + const res = await request(app)
379 + .post("/mood")
380 + .send({
381 + user_id: userId,
382 + stress_level: 3,
383 + anxiety_level: 4,
384 + energy_level: 7,
385 + recorded_at: now,
386 + mood_components: [
387 + { emotion: "sad", intensity: 11 }
388 + ]
389 + });
390 +
391 + t.is(res.status, 400);
392 + t.regex(res.body.error, /Invalid intensity/);
393 + });
394 +
395 + test("Falha ao criar registro de humor com emoções duplicadas", async (t) => {
396 + const now = new Date().toISOString();
397 +
398 + const res = await request(app)
399 + .post("/mood")
400 + .send({
401 + user_id: userId,
402 + stress_level: 3,
403 + anxiety_level: 4,
404 + energy_level: 7,
405 + recorded_at: now,
406 + mood_components: [
407 + { emotion: "joy", intensity: 7 },
408 + { emotion: "joy", intensity: 5 }
409 + ]
410 + });
411 +
412 + t.is(res.status, 400);
413 + t.regex(res.body.error, /Duplicate emotion/);
414 + });
415 +
416 + test("Falha ao criar registro de humor com componente sem emoção de referencia", async (t) => {
417 + const now = new Date().toISOString();
418 +
419 + const res = await request(app)
420 + .post("/mood")
421 + .send({
422 + user_id: userId,
423 + stress_level: 3,
424 + anxiety_level: 4,
425 + energy_level: 7,
426 + recorded_at: now,
427 + mood_components: [
428 + { intensity: 5 }
429 + ]
430 + });
431 +
432 + t.is(res.status, 400);
433 + t.regex(res.body.error, /must have emotion and intensity/);
434 + });
435 +
436 + test("Falha ao criar registro de humor com componente sem intensidade", async (t) => {
437 + const now = new Date().toISOString();
438 +
439 + const res = await request(app)
440 + .post("/mood")
441 + .send({
442 + user_id: userId,
443 + stress_level: 3,
444 + anxiety_level: 4,
445 + energy_level: 7,
446 + recorded_at: now,
447 + mood_components: [
448 + { emotion: "joy" }
449 + ]
450 + });
451 +
452 + t.is(res.status, 400);
453 + t.regex(res.body.error, /must have emotion and intensity/);
454 + });
455 +
456 + test("Criar registro de humor com lista vazia de componentes deve funcionar", async (t) => {
457 + const now = new Date().toISOString();
458 +
459 + const res = await request(app)
460 + .post("/mood")
461 + .send({
462 + user_id: userId,
463 + stress_level: 3,
464 + anxiety_level: 4,
465 + energy_level: 7,
466 + recorded_at: now,
467 + mood_components: []
468 + });
469 +
470 + t.is(res.status, 201);
471 +
472 + // Verify no components were created
473 + const components = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").all(res.body.id);
474 + t.is(components.length, 0);
475 + });
476 +
477 + test("Descrição e título devem ser sanitizados (truncados em 500 caracteres)", async (t) => {
478 + const now = new Date().toISOString();
479 + const longText = "A".repeat(600);
480 +
481 + const res = await request(app)
482 + .post("/mood")
483 + .send({
484 + user_id: userId,
485 + stress_level: 3,
486 + anxiety_level: 4,
487 + energy_level: 7,
488 + recorded_at: now,
489 + title: longText,
490 + description: longText
491 + });
492 +
493 + t.is(res.status, 201);
494 +
495 + const mood = db.prepare("SELECT title, description FROM mood WHERE id = ?").get(res.body.id);
496 + t.is(mood.title.length, 500);
497 + t.is(mood.description.length, 500);
498 + });
api/tests/mood/queries.test.js
@@ -0,0 +1,298 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import { createApp } from "../../src/create-app.js";
4 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
5 +
6 + let app;
7 + let db;
8 + let userId;
9 + let moodIds = [];
10 +
11 + test.before(() => {
12 + db = setupTestDb();
13 + app = createApp(db);
14 + });
15 +
16 + test.beforeEach(async () => {
17 + cleanupTestDb();
18 + moodIds = [];
19 +
20 + // Create a test user with unique email
21 + const uniqueEmail = `mood_${Date.now()}_${Math.random()}@example.com`;
22 + const userRes = await request(app)
23 + .post("/users")
24 + .send({
25 + email: uniqueEmail,
26 + password: "senha123",
27 + first_name: "Mood",
28 + last_name: "Tester"
29 + });
30 +
31 + if (userRes.status !== 201) {
32 + throw new Error(`Failed to create test user: ${JSON.stringify(userRes.body)}`);
33 + }
34 +
35 + userId = userRes.body.id;
36 +
37 + // Create multiple moods for testing
38 + const dates = [
39 + new Date('2024-01-15T10:00:00Z'),
40 + new Date('2024-01-20T14:30:00Z'),
41 + new Date('2024-02-01T08:00:00Z'),
42 + new Date('2024-02-15T16:45:00Z'),
43 + new Date('2024-03-01T12:00:00Z')
44 + ];
45 +
46 + for (let i = 0; i < dates.length; i++) {
47 + const moodRes = await request(app)
48 + .post("/mood")
49 + .send({
50 + user_id: userId,
51 + rating: 5 + i,
52 + stress_level: 3 + i,
53 + anxiety_level: 2 + i,
54 + energy_level: 6 - i,
55 + title: `Mood ${i + 1}`,
56 + recorded_at: dates[i].toISOString(),
57 + mood_components: i % 2 === 0 ? [
58 + { emotion: "joy", intensity: 5 + i },
59 + { emotion: "trust", intensity: 4 + i }
60 + ] : []
61 + });
62 + moodIds.push(moodRes.body.id);
63 + }
64 + });
65 +
66 + test.serial("Obter mood específico por ID", async (t) => {
67 + const res = await request(app).get(`/mood/${moodIds[0]}`);
68 +
69 + t.is(res.status, 200);
70 + t.is(Number(res.body.id), Number(moodIds[0]));
71 + t.is(Number(res.body.user_id), Number(userId));
72 + t.is(res.body.title, "Mood 1");
73 + t.truthy(res.body.first_name);
74 + t.truthy(res.body.last_name);
75 + t.true(Array.isArray(res.body.mood_components));
76 + });
77 +
78 + test.serial("Mood deve incluir componentes emocionais", async (t) => {
79 + const res = await request(app).get(`/mood/${moodIds[0]}`);
80 +
81 + t.is(res.status, 200);
82 + t.is(res.body.mood_components.length, 2);
83 +
84 + const emotions = res.body.mood_components.map(c => c.emotion);
85 + t.true(emotions.includes('joy'));
86 + t.true(emotions.includes('trust'));
87 + });
88 +
89 + test.serial("Mood deve incluir estatísticas emocionais quando houver componentes", async (t) => {
90 + const res = await request(app).get(`/mood/${moodIds[0]}`);
91 +
92 + t.is(res.status, 200);
93 + t.truthy(res.body.emotion_stats);
94 + t.truthy(res.body.emotion_stats.dominant);
95 + t.truthy(res.body.emotion_stats.average);
96 + t.truthy(res.body.emotion_stats.breakdown);
97 + });
98 +
99 + test.serial("Falha ao obter mood inexistente", async (t) => {
100 + const res = await request(app).get("/mood/99999");
101 +
102 + t.is(res.status, 404);
103 + t.is(res.body.error, "Humor não encontrado");
104 + });
105 +
106 + test.serial("Obter todos os moods de um usuário", async (t) => {
107 + const res = await request(app).get(`/mood/user/${userId}`);
108 +
109 + t.is(res.status, 200);
110 + t.truthy(res.body.moods);
111 + t.is(res.body.moods.length, 5);
112 + t.truthy(res.body.pagination);
113 + t.is(res.body.pagination.total, 5);
114 + });
115 +
116 + test.serial("Moods devem estar ordenados por data (mais recente primeiro)", async (t) => {
117 + const res = await request(app).get(`/mood/user/${userId}`);
118 +
119 + t.is(res.status, 200);
120 +
121 + const dates = res.body.moods.map(m => new Date(m.recorded_at));
122 + for (let i = 1; i < dates.length; i++) {
123 + t.true(dates[i - 1] >= dates[i]);
124 + }
125 + });
126 +
127 + test.serial("Filtrar registros de humor por intervalo de datas", async (t) => {
128 + const res = await request(app).get(`/mood/user/${userId}`)
129 + .query({
130 + start_date: '2024-01-01',
131 + end_date: '2024-01-31'
132 + });
133 +
134 + t.is(res.status, 200);
135 + t.is(res.body.moods.length, 2); // Only January moods
136 + });
137 +
138 + test.serial("Filtrar registros de humor apenas por data de início", async (t) => {
139 + const res = await request(app).get(`/mood/user/${userId}`)
140 + .query({
141 + start_date: '2024-02-01'
142 + });
143 +
144 + t.is(res.status, 200);
145 + t.is(res.body.moods.length, 3); // Feb and March moods
146 + });
147 +
148 + test.serial("Filtrar registros de humor apenas por data de fim", async (t) => {
149 + const res = await request(app).get(`/mood/user/${userId}`)
150 + .query({
151 + end_date: '2024-01-31'
152 + });
153 +
154 + t.is(res.status, 200);
155 + t.is(res.body.moods.length, 2); // Only January moods
156 + });
157 +
158 + test.serial("Paginação de moods", async (t) => {
159 + const res = await request(app).get(`/mood/user/${userId}`)
160 + .query({
161 + page: 2,
162 + limit: 2
163 + });
164 +
165 + t.is(res.status, 200);
166 + t.is(res.body.moods.length, 2);
167 + t.is(res.body.pagination.page, 2);
168 + t.is(res.body.pagination.limit, 2);
169 + t.is(res.body.pagination.total_pages, 3);
170 + });
171 +
172 + test.serial("Paginação com limite máximo de 100", async (t) => {
173 + const res = await request(app).get(`/mood/user/${userId}`)
174 + .query({
175 + limit: 200
176 + });
177 +
178 + t.is(res.status, 200);
179 + t.is(res.body.pagination.limit, 100); // Should be capped at 100
180 + });
181 +
182 + test.serial("Falha ao filtrar com intervalo de datas inválido", async (t) => {
183 + const res = await request(app).get(`/mood/user/${userId}`)
184 + .query({
185 + start_date: '2024-02-01',
186 + end_date: '2024-01-01' // End before start
187 + });
188 +
189 + t.is(res.status, 400);
190 + t.is(res.body.error, "Intervalo de datas inválido");
191 + });
192 +
193 + test.serial("Falha ao filtrar com data de início inválida", async (t) => {
194 + const res = await request(app).get(`/mood/user/${userId}`)
195 + .query({
196 + start_date: 'data-invalida'
197 + });
198 +
199 + t.is(res.status, 400);
200 + t.is(res.body.error, "Data de início inválida");
201 + });
202 +
203 + test.serial("Falha ao filtrar com data de fim inválida", async (t) => {
204 + const res = await request(app).get(`/mood/user/${userId}`)
205 + .query({
206 + end_date: 'data-invalida'
207 + });
208 +
209 + t.is(res.status, 400);
210 + t.is(res.body.error, "Data de fim inválida");
211 + });
212 +
213 + test.serial("Obter estatísticas de mood do usuário", async (t) => {
214 + const res = await request(app).get(`/mood/user/${userId}/stats`);
215 +
216 + t.is(res.status, 200);
217 + t.truthy(res.body.overall);
218 + t.is(res.body.overall.total_entries, 5);
219 + t.truthy(res.body.overall.avg_rating);
220 + t.truthy(res.body.overall.avg_stress);
221 + t.truthy(res.body.overall.avg_anxiety);
222 + t.truthy(res.body.overall.avg_energy);
223 + t.truthy(res.body.emotions);
224 + t.truthy(res.body.trends);
225 + });
226 +
227 + test.serial("Estatísticas devem incluir frequência de emoções", async (t) => {
228 + const res = await request(app).get(`/mood/user/${userId}/stats`);
229 +
230 + t.is(res.status, 200);
231 + t.true(Array.isArray(res.body.emotions));
232 +
233 + // We created 3 moods with components
234 + const joyStats = res.body.emotions.find(e => e.emotion === 'joy');
235 + t.truthy(joyStats);
236 + t.is(joyStats.frequency, 3);
237 + });
238 +
239 + test.serial("Estatísticas com filtro de data", async (t) => {
240 + const res = await request(app).get(`/mood/user/${userId}/stats`)
241 + .query({
242 + start_date: '2024-01-01',
243 + end_date: '2024-01-31'
244 + });
245 +
246 + t.is(res.status, 200);
247 + t.is(res.body.overall.total_entries, 2); // Only January
248 + t.is(res.body.date_range.start, '2024-01-01');
249 + t.is(res.body.date_range.end, '2024-01-31');
250 + });
251 +
252 + test.serial("Estatísticas devem incluir tendências por dia da semana", async (t) => {
253 + const res = await request(app).get(`/mood/user/${userId}/stats`);
254 +
255 + t.is(res.status, 200);
256 + t.true(Array.isArray(res.body.trends.by_day_of_week));
257 +
258 + const hasDay = res.body.trends.by_day_of_week.some(d =>
259 + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].includes(d.day_of_week)
260 + );
261 + t.true(hasDay);
262 + });
263 +
264 + test.serial("Usuário sem moods deve retornar lista vazia", async (t) => {
265 + // Create another user without moods
266 + const newUserRes = await request(app)
267 + .post("/users")
268 + .send({
269 + email: "nomood@example.com",
270 + password: "senha123",
271 + first_name: "No",
272 + last_name: "Mood"
273 + });
274 +
275 + const res = await request(app).get(`/mood/user/${newUserRes.body.id}`);
276 +
277 + t.is(res.status, 200);
278 + t.is(res.body.moods.length, 0);
279 + t.is(res.body.pagination.total, 0);
280 + });
281 +
282 + test.serial("Estatísticas de usuário sem moods", async (t) => {
283 + // Create another user without moods
284 + const newUserRes = await request(app)
285 + .post("/users")
286 + .send({
287 + email: "nostats@example.com",
288 + password: "senha123",
289 + first_name: "No",
290 + last_name: "Stats"
291 + });
292 +
293 + const res = await request(app).get(`/mood/user/${newUserRes.body.id}/stats`);
294 +
295 + t.is(res.status, 200);
296 + t.is(res.body.overall.total_entries, 0);
297 + t.is(res.body.emotions.length, 0);
298 + });
api/tests/mood/updates.test.js
@@ -0,0 +1,349 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import { createApp } from "../../src/create-app.js";
4 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
5 +
6 + let app;
7 + let db;
8 + let userId;
9 + let moodId;
10 +
11 + test.before(() => {
12 + db = setupTestDb();
13 + app = createApp(db);
14 + });
15 +
16 + test.beforeEach(async () => {
17 + cleanupTestDb();
18 +
19 + // Create a test user with unique email
20 + const uniqueEmail = `mood_${Date.now()}_${Math.random()}@example.com`;
21 + const userRes = await request(app)
22 + .post("/users")
23 + .send({
24 + email: uniqueEmail,
25 + password: "senha123",
26 + first_name: "Mood",
27 + last_name: "Tester"
28 + });
29 +
30 + if (userRes.status !== 201) {
31 + throw new Error(`Failed to create test user: ${JSON.stringify(userRes.body)}`);
32 + }
33 +
34 + userId = userRes.body.id;
35 +
36 + // Create a mood to update
37 + const moodRes = await request(app)
38 + .post("/mood")
39 + .send({
40 + user_id: userId,
41 + rating: 5,
42 + stress_level: 4,
43 + anxiety_level: 3,
44 + energy_level: 7,
45 + title: "Original mood",
46 + description: "Original description",
47 + recorded_at: new Date().toISOString(),
48 + mood_components: [
49 + { emotion: "joy", intensity: 6 },
50 + { emotion: "trust", intensity: 7 }
51 + ]
52 + });
53 +
54 + moodId = moodRes.body.id;
55 + });
56 +
57 + test.serial("Atualizar rating do mood", async (t) => {
58 + const res = await request(app)
59 + .put(`/mood/${moodId}`)
60 + .send({
61 + rating: 8
62 + });
63 +
64 + t.is(res.status, 200);
65 + t.true(res.body.success);
66 +
67 + // Verify update
68 + const mood = db.prepare("SELECT rating FROM mood WHERE id = ?").get(moodId);
69 + t.is(mood.rating, 8);
70 + });
71 +
72 + test.serial("Atualizar múltiplos níveis do mood", async (t) => {
73 + const res = await request(app)
74 + .put(`/mood/${moodId}`)
75 + .send({
76 + stress_level: 2,
77 + anxiety_level: 1,
78 + energy_level: 9
79 + });
80 +
81 + t.is(res.status, 200);
82 + t.true(res.body.success);
83 +
84 + // Verify updates
85 + const mood = db.prepare("SELECT stress_level, anxiety_level, energy_level FROM mood WHERE id = ?").get(moodId);
86 + t.is(mood.stress_level, 2);
87 + t.is(mood.anxiety_level, 1);
88 + t.is(mood.energy_level, 9);
89 + });
90 +
91 + test.serial("Atualizar título e descrição do mood", async (t) => {
92 + const res = await request(app)
93 + .put(`/mood/${moodId}`)
94 + .send({
95 + title: "Novo título",
96 + description: "Nova descrição detalhada"
97 + });
98 +
99 + t.is(res.status, 200);
100 + t.true(res.body.success);
101 +
102 + // Verify updates
103 + const mood = db.prepare("SELECT title, description FROM mood WHERE id = ?").get(moodId);
104 + t.is(mood.title, "Novo título");
105 + t.is(mood.description, "Nova descrição detalhada");
106 + });
107 +
108 + test.serial("Atualizar data do mood", async (t) => {
109 + const newDate = new Date('2024-01-15T10:00:00Z');
110 +
111 + const res = await request(app)
112 + .put(`/mood/${moodId}`)
113 + .send({
114 + recorded_at: newDate.toISOString()
115 + });
116 +
117 + t.is(res.status, 200);
118 + t.true(res.body.success);
119 +
120 + // Verify update
121 + const mood = db.prepare("SELECT recorded_at FROM mood WHERE id = ?").get(moodId);
122 + t.truthy(mood.recorded_at.includes('2024-01-15'));
123 + });
124 +
125 + test.serial("Atualizar componentes emocionais do mood", async (t) => {
126 + const res = await request(app)
127 + .put(`/mood/${moodId}`)
128 + .send({
129 + mood_components: [
130 + { emotion: "sad", intensity: 4 },
131 + { emotion: "fear", intensity: 3 },
132 + { emotion: "angry", intensity: 2 }
133 + ]
134 + });
135 +
136 + t.is(res.status, 200);
137 + t.true(res.body.success);
138 +
139 + // Verify old components were replaced
140 + const components = db.prepare("SELECT emotion, intensity FROM mood_components WHERE mood_id = ? ORDER BY emotion").all(moodId);
141 + t.is(components.length, 3);
142 +
143 + const emotions = components.map(c => c.emotion);
144 + t.true(emotions.includes('sad'));
145 + t.true(emotions.includes('fear'));
146 + t.true(emotions.includes('angry'));
147 + t.false(emotions.includes('joy')); // Old emotion should be gone
148 + t.false(emotions.includes('trust')); // Old emotion should be gone
149 + });
150 +
151 + test.serial("Remover todos os componentes emocionais enviando array vazio", async (t) => {
152 + const res = await request(app)
153 + .put(`/mood/${moodId}`)
154 + .send({
155 + mood_components: []
156 + });
157 +
158 + t.is(res.status, 200);
159 + t.true(res.body.success);
160 +
161 + // Verify components were deleted
162 + const components = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").all(moodId);
163 + t.is(components.length, 0);
164 + });
165 +
166 + test.serial("Atualização deve modificar a coluna de atualização", async (t) => {
167 + // Get original updated_at
168 + const originalMood = db.prepare("SELECT updated_at FROM mood WHERE id = ?")
169 + .get(moodId);
170 +
171 + // Wait a bit to ensure timestamp difference
172 + await new Promise(resolve => setTimeout(resolve, 1100));
173 +
174 + const res = await request(app)
175 + .put(`/mood/${moodId}`)
176 + .send({
177 + rating: 9
178 + });
179 +
180 + t.is(res.status, 200);
181 +
182 + // Verify updated_at changed
183 + const updatedMood = db.prepare("SELECT updated_at FROM mood WHERE id = ?")
184 + .get(moodId);
185 +
186 + t.not(updatedMood.updated_at, originalMood.updated_at);
187 + });
188 +
189 + test.serial("Falha ao atualizar humor inexistente", async (t) => {
190 + const res = await request(app)
191 + .put("/mood/99999")
192 + .send({
193 + rating: 8
194 + });
195 +
196 + t.is(res.status, 404);
197 + t.is(res.body.error, "Humor não encontrado");
198 + });
199 +
200 + test.serial("Falha ao atualizar com avaliação inválida", async (t) => {
201 + const res = await request(app)
202 + .put(`/mood/${moodId}`)
203 + .send({
204 + rating: 15
205 + });
206 +
207 + t.is(res.status, 400);
208 + t.is(res.body.error, "rating deve ser entre 1 e 10");
209 + });
210 +
211 + test.serial("Falha ao atualizar com nível de estresse inválido", async (t) => {
212 + const res = await request(app)
213 + .put(`/mood/${moodId}`)
214 + .send({
215 + stress_level: 0
216 + });
217 +
218 + t.is(res.status, 400);
219 + t.is(res.body.error, "stress_level deve ser entre 1 e 10");
220 + });
221 +
222 + test.serial("Falha ao atualizar com nível de ansiedade inválido", async (t) => {
223 + const res = await request(app)
224 + .put(`/mood/${moodId}`)
225 + .send({
226 + anxiety_level: 11
227 + });
228 +
229 + t.is(res.status, 400);
230 + t.is(res.body.error, "anxiety_level deve ser entre 1 e 10");
231 + });
232 +
233 + test.serial("Falha ao atualizar com nível de energia inválida", async (t) => {
234 + const res = await request(app)
235 + .put(`/mood/${moodId}`)
236 + .send({
237 + energy_level: -1
238 + });
239 +
240 + t.is(res.status, 400);
241 + t.is(res.body.error, "energy_level deve ser entre 1 e 10");
242 + });
243 +
244 + test.serial("Falha ao atualizar com data inválida", async (t) => {
245 + const res = await request(app)
246 + .put(`/mood/${moodId}`)
247 + .send({
248 + recorded_at: "data-invalida"
249 + });
250 +
251 + t.is(res.status, 400);
252 + t.is(res.body.error, "recorded_at deve ser uma data válida");
253 + });
254 +
255 + test.serial("Falha ao atualizar com data no futuro", async (t) => {
256 + const futureDate = new Date();
257 + futureDate.setDate(futureDate.getDate() + 1);
258 +
259 + const res = await request(app)
260 + .put(`/mood/${moodId}`)
261 + .send({
262 + recorded_at: futureDate.toISOString()
263 + });
264 +
265 + t.is(res.status, 400);
266 + t.is(res.body.error, "recorded_at não pode ser no futuro");
267 + });
268 +
269 + test.serial("Falha ao atualizar com emoção inválida nos componentes", async (t) => {
270 + const res = await request(app)
271 + .put(`/mood/${moodId}`)
272 + .send({
273 + mood_components: [
274 + { emotion: "invalid", intensity: 5 }
275 + ]
276 + });
277 +
278 + t.is(res.status, 400);
279 + t.regex(res.body.error, /Invalid emotion/);
280 + });
281 +
282 + test.serial("Falha ao atualizar com emoções duplicadas", async (t) => {
283 + const res = await request(app)
284 + .put(`/mood/${moodId}`)
285 + .send({
286 + mood_components: [
287 + { emotion: "joy", intensity: 5 },
288 + { emotion: "joy", intensity: 7 }
289 + ]
290 + });
291 +
292 + t.is(res.status, 400);
293 + t.regex(res.body.error, /Duplicate emotion/);
294 + });
295 +
296 + test.serial("Deletar humor existente", async (t) => {
297 + const res = await request(app).delete(`/mood/${moodId}`);
298 +
299 + t.is(res.status, 200);
300 + t.true(res.body.success);
301 + t.is(Number(res.body.deleted), Number(moodId));
302 +
303 + // Verify mood is deleted
304 + const mood = db.prepare("SELECT * FROM mood WHERE id = ?").get(moodId);
305 + t.falsy(mood);
306 + });
307 +
308 + test.serial("Deletar humor deve deletar seus componentes (em cascata)", async (t) => {
309 + // Verify components exist
310 + const componentsBefore = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").all(moodId);
311 + t.true(componentsBefore.length > 0);
312 +
313 + const res = await request(app).delete(`/mood/${moodId}`);
314 + t.is(res.status, 200);
315 +
316 + // Verify components are deleted
317 + const componentsAfter = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").all(moodId);
318 + t.is(componentsAfter.length, 0);
319 + });
320 +
321 + test.serial("Falha ao deletar mood inexistente", async (t) => {
322 + const res = await request(app).delete("/mood/99999");
323 +
324 + t.is(res.status, 404);
325 + t.is(res.body.error, "Humor não encontrado");
326 + });
327 +
328 + test.serial("Atualização parcial mantém valores não alterados", async (t) => {
329 + // Get original values
330 + const originalMood = db.prepare("SELECT * FROM mood WHERE id = ?").get(moodId);
331 +
332 + // Update only rating
333 + const res = await request(app)
334 + .put(`/mood/${moodId}`)
335 + .send({
336 + rating: 10
337 + });
338 +
339 + t.is(res.status, 200);
340 +
341 + // Verify only rating changed, others remained
342 + const updatedMood = db.prepare("SELECT * FROM mood WHERE id = ?").get(moodId);
343 + t.is(updatedMood.rating, 10);
344 + t.is(updatedMood.stress_level, originalMood.stress_level);
345 + t.is(updatedMood.anxiety_level, originalMood.anxiety_level);
346 + t.is(updatedMood.energy_level, originalMood.energy_level);
347 + t.is(updatedMood.title, originalMood.title);
348 + t.is(updatedMood.description, originalMood.description);
349 + });
api/tests/test-helper.js
@@ -1,25 +1,33 @@
1 1 import { setupTestDatabase } from '../src/setup-db.js';
2 2
3 - // Store the test database globally
4 - let testDb;
3 + // Store a single test database instance
4 + let globalTestDb = null;
5 5
6 - // Replace the default db connection with test database
6 + // Setup the test database
7 7 export function setupTestDb() {
8 - testDb = setupTestDatabase();
9 - return testDb;
8 + if (!globalTestDb) {
9 + globalTestDb = setupTestDatabase();
10 + }
11 + return globalTestDb;
10 12 }
11 13
12 14 // Get the test database
13 15 export function getTestDb() {
14 - return testDb;
16 + return globalTestDb;
15 17 }
16 18
19 + // Cleanup test database
17 20 export function cleanupTestDb() {
18 - // Clear all tables
19 - if (testDb) {
20 - testDb.exec(`
21 - DELETE FROM mood;
22 - DELETE FROM users;
23 - `);
21 + if (globalTestDb) {
22 + try {
23 + // Delete in correct order due to foreign keys
24 + globalTestDb.exec(`
25 + DELETE FROM mood_components;
26 + DELETE FROM mood;
27 + DELETE FROM users;
28 + `);
29 + } catch (err) {
30 + console.error('Error cleaning test DB:', err);
31 + }
24 32 }
25 - }
33 + }
api/tests/users/authentication.test.js
@@ -0,0 +1,148 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import bcrypt from "bcryptjs";
4 + import { createApp } from "../../src/create-app.js";
5 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
6 +
7 + let app;
8 + let db;
9 +
10 + test.before(() => {
11 + db = setupTestDb();
12 + app = createApp(db);
13 + });
14 +
15 + test.beforeEach(() => {
16 + cleanupTestDb();
17 +
18 + // Create a test user for login tests
19 + const hashedPassword = bcrypt.hashSync("senha123", 10);
20 + db.prepare(`
21 + INSERT INTO users (email, password, first_name, last_name, timezone)
22 + VALUES (?, ?, ?, ?, ?)
23 + `).run("teste@example.com", hashedPassword, "Teste", "User", "UTC");
24 + });
25 +
26 + test("Login bem-sucedido com credenciais corretas", async (t) => {
27 + const res = await request(app)
28 + .post("/users/login")
29 + .send({
30 + email: "teste@example.com",
31 + password: "senha123"
32 + });
33 +
34 + t.is(res.status, 200);
35 + t.true(res.body.success);
36 + t.truthy(res.body.user);
37 + t.is(res.body.user.email, "teste@example.com");
38 + t.is(res.body.user.first_name, "Teste");
39 + t.is(res.body.user.last_name, "User");
40 + t.falsy(res.body.user.password); // Password should not be in response
41 + });
42 +
43 + test("Login com email em maiúsculas deve funcionar", async (t) => {
44 + const res = await request(app)
45 + .post("/users/login")
46 + .send({
47 + email: "TESTE@EXAMPLE.COM",
48 + password: "senha123"
49 + });
50 +
51 + t.is(res.status, 200);
52 + t.true(res.body.success);
53 + });
54 +
55 + test("Falha no login com senha incorreta", async (t) => {
56 + const res = await request(app)
57 + .post("/users/login")
58 + .send({
59 + email: "teste@example.com",
60 + password: "senhaerrada"
61 + });
62 +
63 + t.is(res.status, 401);
64 + t.is(res.body.error, "Credenciais inválidas");
65 + });
66 +
67 + test("Falha no login com email não cadastrado", async (t) => {
68 + const res = await request(app)
69 + .post("/users/login")
70 + .send({
71 + email: "naocadastrado@example.com",
72 + password: "senha123"
73 + });
74 +
75 + t.is(res.status, 401);
76 + t.is(res.body.error, "Credenciais inválidas");
77 + });
78 +
79 + test("Falha no login sem email", async (t) => {
80 + const res = await request(app)
81 + .post("/users/login")
82 + .send({
83 + password: "senha123"
84 + });
85 +
86 + t.is(res.status, 400);
87 + t.is(res.body.error, "Email e senha são obrigatórios");
88 + });
89 +
90 + test("Falha no login sem senha", async (t) => {
91 + const res = await request(app)
92 + .post("/users/login")
93 + .send({
94 + email: "teste@example.com"
95 + });
96 +
97 + t.is(res.status, 400);
98 + t.is(res.body.error, "Email e senha são obrigatórios");
99 + });
100 +
101 + test("Falha no login sem credenciais", async (t) => {
102 + const res = await request(app)
103 + .post("/users/login")
104 + .send({});
105 +
106 + t.is(res.status, 400);
107 + t.is(res.body.error, "Email e senha são obrigatórios");
108 + });
109 +
110 + test("Login não deve retornar a senha do usuário", async (t) => {
111 + const res = await request(app)
112 + .post("/users/login")
113 + .send({
114 + email: "teste@example.com",
115 + password: "senha123"
116 + });
117 +
118 + t.is(res.status, 200);
119 + t.falsy(res.body.user.password);
120 + t.is(typeof res.body.user.password, 'undefined');
121 + });
122 +
123 + test("Login deve retornar informações completas do usuário", async (t) => {
124 + const res = await request(app)
125 + .post("/users/login")
126 + .send({
127 + email: "teste@example.com",
128 + password: "senha123"
129 + });
130 +
131 + t.is(res.status, 200);
132 + t.truthy(res.body.user.id);
133 + t.is(res.body.user.email, "teste@example.com");
134 + t.is(res.body.user.first_name, "Teste");
135 + t.is(res.body.user.last_name, "User");
136 + t.is(res.body.user.timezone, "UTC");
137 + });
138 +
139 + test("Login com espaços em branco deve ser tratado corretamente", async (t) => {
140 + const res = await request(app)
141 + .post("/users/login")
142 + .send({
143 + email: " teste@example.com ",
144 + password: "senha123"
145 + });
146 +
147 + t.is(res.status, 401); // Should fail as email with spaces doesn't match
148 + });
api/tests/users/creation.test.js
@@ -0,0 +1,258 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import { createApp } from "../../src/create-app.js";
4 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
5 +
6 + let app;
7 + let db;
8 +
9 + test.before(() => {
10 + db = setupTestDb();
11 + app = createApp(db);
12 + });
13 +
14 + test.beforeEach(() => {
15 + cleanupTestDb();
16 + });
17 +
18 + test("Criar usuário com todos os campos válidos", async (t) => {
19 + const res = await request(app)
20 + .post("/users")
21 + .send({
22 + email: "usuario@example.com",
23 + password: "senha123",
24 + first_name: "João",
25 + last_name: "Silva",
26 + timezone: "America/Sao_Paulo"
27 + });
28 +
29 + t.is(res.status, 201);
30 + t.truthy(res.body.id);
31 + });
32 +
33 + test("Criar usuário com timezone padrão (UTC)", async (t) => {
34 + const res = await request(app)
35 + .post("/users")
36 + .send({
37 + email: "utc@example.com",
38 + password: "senha123",
39 + first_name: "Maria",
40 + last_name: "Santos"
41 + });
42 +
43 + t.is(res.status, 201);
44 +
45 + // Verify timezone was set to UTC
46 + const user = db.prepare("SELECT timezone FROM users WHERE id = ?").get(res.body.id);
47 + t.is(user.timezone, 'UTC');
48 + });
49 +
50 + test("Falha ao criar usuário sem email", async (t) => {
51 + const res = await request(app)
52 + .post("/users")
53 + .send({
54 + password: "senha123",
55 + first_name: "João",
56 + last_name: "Silva"
57 + });
58 +
59 + t.is(res.status, 400);
60 + t.is(res.body.error, "Campos faltantes");
61 + t.true(res.body.missing.includes('email'));
62 + });
63 +
64 + test("Falha ao criar usuário sem senha", async (t) => {
65 + const res = await request(app)
66 + .post("/users")
67 + .send({
68 + email: "usuario@example.com",
69 + first_name: "João",
70 + last_name: "Silva"
71 + });
72 +
73 + t.is(res.status, 400);
74 + t.is(res.body.error, "Campos faltantes");
75 + t.true(res.body.missing.includes('password'));
76 + });
77 +
78 + test("Falha ao criar usuário sem primeiro nome", async (t) => {
79 + const res = await request(app)
80 + .post("/users")
81 + .send({
82 + email: "usuario@example.com",
83 + password: "senha123",
84 + last_name: "Silva"
85 + });
86 +
87 + t.is(res.status, 400);
88 + t.is(res.body.error, "Campos faltantes");
89 + t.true(res.body.missing.includes('first_name'));
90 + });
91 +
92 + test("Falha ao criar usuário sem último nome", async (t) => {
93 + const res = await request(app)
94 + .post("/users")
95 + .send({
96 + email: "usuario@example.com",
97 + password: "senha123",
98 + first_name: "João"
99 + });
100 +
101 + t.is(res.status, 400);
102 + t.is(res.body.error, "Campos faltantes");
103 + t.true(res.body.missing.includes('last_name'));
104 + });
105 +
106 + test("Falha ao criar usuário com múltiplos campos faltantes", async (t) => {
107 + const res = await request(app)
108 + .post("/users")
109 + .send({
110 + first_name: "João"
111 + });
112 +
113 + t.is(res.status, 400);
114 + t.is(res.body.error, "Campos faltantes");
115 + t.true(res.body.missing.includes('email'));
116 + t.true(res.body.missing.includes('password'));
117 + t.true(res.body.missing.includes('last_name'));
118 + });
119 +
120 + test("Falha ao criar usuário com email inválido (sem @)", async (t) => {
121 + const res = await request(app)
122 + .post("/users")
123 + .send({
124 + email: "emailinvalido",
125 + password: "senha123",
126 + first_name: "João",
127 + last_name: "Silva"
128 + });
129 +
130 + t.is(res.status, 400);
131 + t.is(res.body.error, "Email inválido");
132 + });
133 +
134 + test("Falha ao criar usuário com email inválido (sem domínio)", async (t) => {
135 + const res = await request(app)
136 + .post("/users")
137 + .send({
138 + email: "usuario@",
139 + password: "senha123",
140 + first_name: "João",
141 + last_name: "Silva"
142 + });
143 +
144 + t.is(res.status, 400);
145 + t.is(res.body.error, "Email inválido");
146 + });
147 +
148 + test("Falha ao criar usuário com email inválido (formato incorreto)", async (t) => {
149 + const res = await request(app)
150 + .post("/users")
151 + .send({
152 + email: "@example.com",
153 + password: "senha123",
154 + first_name: "João",
155 + last_name: "Silva"
156 + });
157 +
158 + t.is(res.status, 400);
159 + t.is(res.body.error, "Email inválido");
160 + });
161 +
162 + test("Falha ao criar usuário com senha muito curta (menos de 6 caracteres)", async (t) => {
163 + const res = await request(app)
164 + .post("/users")
165 + .send({
166 + email: "usuario@example.com",
167 + password: "123",
168 + first_name: "João",
169 + last_name: "Silva"
170 + });
171 +
172 + t.is(res.status, 400);
173 + t.is(res.body.error, "Senha deve ter pelo menos 6 caracteres");
174 + });
175 +
176 + test("Falha ao criar usuário com senha vazia", async (t) => {
177 + const res = await request(app)
178 + .post("/users")
179 + .send({
180 + email: "usuario@example.com",
181 + password: "",
182 + first_name: "João",
183 + last_name: "Silva"
184 + });
185 +
186 + t.is(res.status, 400);
187 + t.is(res.body.error, "Campos faltantes");
188 + });
189 +
190 + test("Falha ao criar usuário com email duplicado", async (t) => {
191 + // Create first user
192 + await request(app)
193 + .post("/users")
194 + .send({
195 + email: "duplicado@example.com",
196 + password: "senha123",
197 + first_name: "João",
198 + last_name: "Silva"
199 + });
200 +
201 + // Try to create with same email
202 + const res = await request(app)
203 + .post("/users")
204 + .send({
205 + email: "duplicado@example.com",
206 + password: "outrasenha",
207 + first_name: "Maria",
208 + last_name: "Santos"
209 + });
210 +
211 + t.is(res.status, 400);
212 + t.is(res.body.error, "Email já cadastrado");
213 + });
214 +
215 + test("Email deve ser case-insensitive para duplicação", async (t) => {
216 + // Create with lowercase
217 + await request(app)
218 + .post("/users")
219 + .send({
220 + email: "usuario@example.com",
221 + password: "senha123",
222 + first_name: "João",
223 + last_name: "Silva"
224 + });
225 +
226 + // Try with uppercase
227 + const res = await request(app)
228 + .post("/users")
229 + .send({
230 + email: "USUARIO@EXAMPLE.COM",
231 + password: "senha123",
232 + first_name: "Maria",
233 + last_name: "Santos"
234 + });
235 +
236 + t.is(res.status, 400);
237 + t.is(res.body.error, "Email já cadastrado");
238 + });
239 +
240 + test("Sanitização de strings longas no nome", async (t) => {
241 + const longName = "A".repeat(600); // 600 characters
242 +
243 + const res = await request(app)
244 + .post("/users")
245 + .send({
246 + email: "longname@example.com",
247 + password: "senha123",
248 + first_name: longName,
249 + last_name: longName
250 + });
251 +
252 + t.is(res.status, 201);
253 +
254 + // Check that name was truncated to 500 chars
255 + const user = db.prepare("SELECT first_name, last_name FROM users WHERE id = ?").get(res.body.id);
256 + t.is(user.first_name.length, 500);
257 + t.is(user.last_name.length, 500);
258 + });
api/tests/users/profile.test.js
@@ -0,0 +1,246 @@
1 + import test from "ava";
2 + import request from "supertest";
3 + import { createApp } from "../../src/create-app.js";
4 + import { setupTestDb, cleanupTestDb } from "../test-helper.js";
5 +
6 + let app;
7 + let db;
8 + let userId;
9 +
10 + test.before(() => {
11 + db = setupTestDb();
12 + app = createApp(db);
13 + });
14 +
15 + let testEmail;
16 +
17 + test.serial.beforeEach(async () => {
18 + cleanupTestDb();
19 +
20 + // Create a test user with unique email
21 + testEmail = `perfil_${Date.now()}_${Math.random()}@example.com`;
22 + const res = await request(app)
23 + .post("/users")
24 + .send({
25 + email: testEmail,
26 + password: "senha123",
27 + first_name: "Perfil",
28 + last_name: "Teste",
29 + timezone: "America/Sao_Paulo"
30 + });
31 +
32 + if (res.status !== 201) {
33 + throw new Error(`Failed to create test user: ${JSON.stringify(res.body)}`);
34 + }
35 +
36 + userId = res.body.id;
37 + });
38 +
39 + test.serial("Obter perfil de usuário existente", async (t) => {
40 + const res = await request(app).get(`/users/${userId}`);
41 +
42 + t.is(res.status, 200);
43 + t.is(Number(res.body.id), Number(userId));
44 + t.is(res.body.email, testEmail);
45 + t.is(res.body.first_name, "Perfil");
46 + t.is(res.body.last_name, "Teste");
47 + t.is(res.body.timezone, "America/Sao_Paulo");
48 + t.truthy(res.body.created_at);
49 + t.truthy(res.body.updated_at);
50 + t.is(typeof res.body.total_moods, 'number');
51 + t.falsy(res.body.password); // Should not include password
52 + });
53 +
54 + test.serial("Perfil deve incluir contagem de registros de humor", async (t) => {
55 + // Add some moods
56 + const now = new Date().toISOString();
57 +
58 + await db.prepare(`
59 + INSERT INTO mood (user_id, rating, stress_level, anxiety_level, energy_level, recorded_at)
60 + VALUES (?, ?, ?, ?, ?, ?)
61 + `).run(userId, 7, 3, 4, 8, now);
62 +
63 + await db.prepare(`
64 + INSERT INTO mood (user_id, rating, stress_level, anxiety_level, energy_level, recorded_at)
65 + VALUES (?, ?, ?, ?, ?, ?)
66 + `).run(userId, 5, 6, 7, 4, now);
67 +
68 + const res = await request(app).get(`/users/${userId}`);
69 +
70 + t.is(res.status, 200);
71 + t.is(res.body.total_moods, 2);
72 + });
73 +
74 + test.serial("Falha ao obter perfil de usuário inexistente", async (t) => {
75 + const res = await request(app).get("/users/99999");
76 +
77 + t.is(res.status, 404);
78 + t.is(res.body.error, "Usuário não encontrado");
79 + });
80 +
81 + test.serial("Atualizar email do usuário", async (t) => {
82 + const res = await request(app)
83 + .put(`/users/${userId}`)
84 + .send({
85 + email: "novoemail@example.com"
86 + });
87 +
88 + t.is(res.status, 200);
89 + t.true(res.body.success);
90 +
91 + // Verify update
92 + const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
93 + t.is(user.email, "novoemail@example.com");
94 + });
95 +
96 + test.serial("Atualizar senha do usuário", async (t) => {
97 + const res = await request(app)
98 + .put(`/users/${userId}`)
99 + .send({
100 + password: "novasenha123"
101 + });
102 +
103 + t.is(res.status, 200);
104 + t.true(res.body.success);
105 +
106 + // Verify can login with new password
107 + const loginRes = await request(app)
108 + .post("/users/login")
109 + .send({
110 + email: testEmail,
111 + password: "novasenha123"
112 + });
113 +
114 + t.is(loginRes.status, 200);
115 + });
116 +
117 + test.serial("Atualizar múltiplos campos do usuário", async (t) => {
118 + const res = await request(app)
119 + .put(`/users/${userId}`)
120 + .send({
121 + first_name: "NovoNome",
122 + last_name: "NovoSobrenome",
123 + timezone: "Europe/London"
124 + });
125 +
126 + t.is(res.status, 200);
127 + t.true(res.body.success);
128 +
129 + // Verify updates
130 + const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
131 + t.is(user.first_name, "NovoNome");
132 + t.is(user.last_name, "NovoSobrenome");
133 + t.is(user.timezone, "Europe/London");
134 + });
135 +
136 + test.serial("Falha ao atualizar com email inválido", async (t) => {
137 + const res = await request(app)
138 + .put(`/users/${userId}`)
139 + .send({
140 + email: "emailinvalido"
141 + });
142 +
143 + t.is(res.status, 400);
144 + t.is(res.body.error, "Email inválido");
145 + });
146 +
147 + test.serial("Falha ao atualizar com senha muito curta", async (t) => {
148 + const res = await request(app)
149 + .put(`/users/${userId}`)
150 + .send({
151 + password: "123"
152 + });
153 +
154 + t.is(res.status, 400);
155 + t.is(res.body.error, "Senha deve ter pelo menos 6 caracteres");
156 + });
157 +
158 + test.serial("Falha ao atualizar email para um já existente", async (t) => {
159 + // Create another user
160 + await request(app)
161 + .post("/users")
162 + .send({
163 + email: "outro@example.com",
164 + password: "senha123",
165 + first_name: "Outro",
166 + last_name: "Usuario"
167 + });
168 +
169 + // Try to update to existing email
170 + const res = await request(app)
171 + .put(`/users/${userId}`)
172 + .send({
173 + email: "outro@example.com"
174 + });
175 +
176 + t.is(res.status, 400);
177 + t.is(res.body.error, "Email já está em uso");
178 + });
179 +
180 + test.serial("Falha ao atualizar usuário inexistente", async (t) => {
181 + const res = await request(app)
182 + .put("/users/99999")
183 + .send({
184 + first_name: "Novo"
185 + });
186 +
187 + t.is(res.status, 404);
188 + t.is(res.body.error, "Usuário não encontrado");
189 + });
190 +
191 + test.serial("Falha ao atualizar sem campos", async (t) => {
192 + const res = await request(app)
193 + .put(`/users/${userId}`)
194 + .send({});
195 +
196 + t.is(res.status, 400);
197 + t.is(res.body.error, "Nenhum campo para atualizar");
198 + });
199 +
200 + test.serial("Deletar usuário existente", async (t) => {
201 + const res = await request(app).delete(`/users/${userId}`);
202 +
203 + t.is(res.status, 200);
204 + t.true(res.body.success);
205 + t.is(Number(res.body.deleted), Number(userId));
206 +
207 + // Verify user is deleted
208 + const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
209 + t.falsy(user);
210 + });
211 +
212 + test.serial("Deletar usuário deve deletar seus registros de humores (em cascata)", async (t) => {
213 + // Add a mood
214 + const now = new Date().toISOString();
215 + const moodResult = db.prepare(`
216 + INSERT INTO mood (user_id, rating, stress_level, anxiety_level, energy_level, recorded_at)
217 + VALUES (?, ?, ?, ?, ?, ?)
218 + `).run(userId, 7, 3, 4, 8, now);
219 +
220 + const moodId = moodResult.lastInsertRowid;
221 +
222 + // Add mood components
223 + db.prepare(`
224 + INSERT INTO mood_components (mood_id, emotion, intensity)
225 + VALUES (?, ?, ?)
226 + `).run(moodId, 'joy', 8);
227 +
228 + // Delete user
229 + const res = await request(app).delete(`/users/${userId}`);
230 + t.is(res.status, 200);
231 +
232 + // Verify moods are deleted
233 + const mood = db.prepare("SELECT * FROM mood WHERE user_id = ?").get(userId);
234 + t.falsy(mood);
235 +
236 + // Verify mood components are deleted
237 + const component = db.prepare("SELECT * FROM mood_components WHERE mood_id = ?").get(moodId);
238 + t.falsy(component);
239 + });
240 +
241 + test.serial("Falha ao deletar usuário inexistente", async (t) => {
242 + const res = await request(app).delete("/users/99999");
243 +
244 + t.is(res.status, 404);
245 + t.is(res.body.error, "Usuário não encontrado");
246 + });