Skip to content

Commit 178f629

Browse files
committed
Add integration tests
1 parent f6f3189 commit 178f629

File tree

9 files changed

+491
-0
lines changed

9 files changed

+491
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from unittest.mock import AsyncMock
2+
3+
import pytest
4+
from fastapi.testclient import TestClient
5+
6+
from adapters.input import create_fastapi_app
7+
from domain.models import LLMResult, ResultCode, ResultStatus
8+
9+
10+
@pytest.fixture
11+
def mock_text_processor():
12+
class MockTextProcessor:
13+
summarize = AsyncMock(
14+
return_value=LLMResult(
15+
status=ResultStatus.SUCCESS,
16+
code=ResultCode.OK,
17+
rewritten_text="summary output",
18+
detected_language="it",
19+
)
20+
)
21+
improve = AsyncMock(
22+
return_value=LLMResult(
23+
status=ResultStatus.SUCCESS,
24+
code=ResultCode.OK,
25+
rewritten_text="improved output",
26+
)
27+
)
28+
translate = AsyncMock(
29+
return_value=LLMResult(
30+
status=ResultStatus.SUCCESS,
31+
code=ResultCode.OK,
32+
rewritten_text="translated output",
33+
detected_language="en",
34+
)
35+
)
36+
analyze_six_hats = AsyncMock(
37+
return_value=LLMResult(
38+
status=ResultStatus.SUCCESS,
39+
code=ResultCode.OK,
40+
rewritten_text="six hats output",
41+
)
42+
)
43+
generate = AsyncMock(
44+
return_value=LLMResult(
45+
status=ResultStatus.SUCCESS,
46+
code=ResultCode.OK,
47+
rewritten_text="generated output",
48+
)
49+
)
50+
51+
return MockTextProcessor()
52+
53+
54+
@pytest.fixture
55+
def client(mock_text_processor):
56+
app = create_fastapi_app(mock_text_processor)
57+
return TestClient(app)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
def test_generate_flow_returns_llm_result_and_calls_processor(client, mock_text_processor):
2+
response = client.post(
3+
"/llm/generate",
4+
json={
5+
"prompt": "Scrivi una introduzione",
6+
"context_text": "Contesto della nota",
7+
"word_count": 180,
8+
},
9+
)
10+
11+
assert response.status_code == 200
12+
assert response.json()["outcome"]["status"] == "success"
13+
assert response.json()["data"]["rewritten_text"] == "generated output"
14+
mock_text_processor.generate.assert_awaited_once_with(
15+
"Scrivi una introduzione",
16+
"Contesto della nota",
17+
180,
18+
)
19+
20+
21+
def test_generate_flow_validation_error_when_prompt_is_missing(client):
22+
response = client.post(
23+
"/llm/generate",
24+
json={"context_text": "Contesto della nota", "word_count": 180},
25+
)
26+
27+
assert response.status_code == 422
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def test_health_flow_returns_server_status(client):
2+
response = client.get("/health")
3+
4+
assert response.status_code == 200
5+
assert response.json() == {
6+
"status": "ok",
7+
"message": "Server is awake!",
8+
"architecture": "hexagonal",
9+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from domain.models import TextDocument
2+
3+
4+
def test_improve_flow_returns_llm_result_and_calls_processor(client, mock_text_processor):
5+
response = client.post(
6+
"/llm/improve",
7+
json={"text": "Testo base", "criterion": "piu formale"},
8+
)
9+
10+
assert response.status_code == 200
11+
assert response.json()["outcome"]["status"] == "success"
12+
assert response.json()["data"]["rewritten_text"] == "improved output"
13+
mock_text_processor.improve.assert_awaited_once()
14+
15+
document_arg, criterion_arg = mock_text_processor.improve.await_args.args
16+
assert isinstance(document_arg, TextDocument)
17+
assert document_arg.content == "Testo base"
18+
assert criterion_arg == "piu formale"
19+
20+
21+
def test_improve_flow_maps_value_error_to_400(client, mock_text_processor):
22+
mock_text_processor.improve.side_effect = ValueError("bad criterion")
23+
24+
response = client.post(
25+
"/llm/improve",
26+
json={"text": "Testo base", "criterion": "bad"},
27+
)
28+
29+
assert response.status_code == 400
30+
assert response.json()["detail"] == "bad criterion"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from domain.models import TextDocument
2+
3+
4+
def test_six_hats_flow_returns_llm_result_and_calls_processor(client, mock_text_processor):
5+
response = client.post(
6+
"/llm/six-hats",
7+
json={"text": "Analizza questo testo", "hat": "Nero"},
8+
)
9+
10+
assert response.status_code == 200
11+
assert response.json()["outcome"]["status"] == "success"
12+
assert response.json()["data"]["rewritten_text"] == "six hats output"
13+
mock_text_processor.analyze_six_hats.assert_awaited_once()
14+
15+
document_arg, hat_arg = mock_text_processor.analyze_six_hats.await_args.args
16+
assert isinstance(document_arg, TextDocument)
17+
assert document_arg.content == "Analizza questo testo"
18+
assert hat_arg == "Nero"
19+
20+
21+
def test_six_hats_flow_maps_runtime_error_to_500(client, mock_text_processor):
22+
mock_text_processor.analyze_six_hats.side_effect = RuntimeError("provider down")
23+
24+
response = client.post(
25+
"/llm/six-hats",
26+
json={"text": "Analizza questo testo", "hat": "Nero"},
27+
)
28+
29+
assert response.status_code == 500
30+
assert "Errore del servizio AI: provider down" in response.json()["detail"]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from domain.models import TextDocument
2+
3+
4+
def test_summarize_flow_returns_llm_result_and_calls_processor(client, mock_text_processor):
5+
response = client.post(
6+
"/llm/summarize",
7+
json={"text": "Contenuto da riassumere", "percentage": 50},
8+
)
9+
10+
assert response.status_code == 200
11+
assert response.json()["outcome"]["status"] == "success"
12+
assert response.json()["data"]["rewritten_text"] == "summary output"
13+
mock_text_processor.summarize.assert_awaited_once()
14+
15+
document_arg, percentage_arg = mock_text_processor.summarize.await_args.args
16+
assert isinstance(document_arg, TextDocument)
17+
assert document_arg.content == "Contenuto da riassumere"
18+
assert percentage_arg == 50
19+
20+
21+
def test_summarize_flow_validation_error_when_text_is_missing(client):
22+
response = client.post("/llm/summarize", json={"percentage": 40})
23+
24+
assert response.status_code == 422
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from domain.models import TextDocument
2+
3+
4+
def test_translate_flow_returns_llm_result_and_calls_processor(client, mock_text_processor):
5+
response = client.post(
6+
"/llm/translate",
7+
json={"text": "Ciao mondo", "targetLanguage": "english"},
8+
)
9+
10+
assert response.status_code == 200
11+
assert response.json()["outcome"]["status"] == "success"
12+
assert response.json()["data"]["rewritten_text"] == "translated output"
13+
mock_text_processor.translate.assert_awaited_once()
14+
15+
document_arg, language_arg = mock_text_processor.translate.await_args.args
16+
assert isinstance(document_arg, TextDocument)
17+
assert document_arg.content == "Ciao mondo"
18+
assert language_arg == "english"
19+
20+
21+
def test_translate_flow_validation_error_when_target_language_missing(client):
22+
response = client.post("/llm/translate", json={"text": "Ciao mondo"})
23+
24+
assert response.status_code == 422
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { useState } from 'react';
5+
6+
import FileSidebar from './FileSidebar';
7+
import { useNotesManager } from '../hooks/useNotesManager';
8+
9+
const { dbGetMock, dbSetMock, fileImportMock, fileExportMock } = vi.hoisted(() => ({
10+
dbGetMock: vi.fn(),
11+
dbSetMock: vi.fn(),
12+
fileImportMock: vi.fn(),
13+
fileExportMock: vi.fn(),
14+
}));
15+
16+
vi.mock('idb-keyval', () => ({
17+
get: dbGetMock,
18+
set: dbSetMock,
19+
}));
20+
21+
vi.mock('../services/fileService', () => ({
22+
fileService: {
23+
importFile: fileImportMock,
24+
exportFile: fileExportMock,
25+
},
26+
}));
27+
28+
function FileSidebarExportHarness() {
29+
const [snackbar, setSnackbar] = useState<{
30+
open: boolean;
31+
message: string;
32+
severity: 'success' | 'error';
33+
}>({
34+
open: false,
35+
message: '',
36+
severity: 'success',
37+
});
38+
39+
const {
40+
notes,
41+
activeNoteId,
42+
setActiveNoteId,
43+
isLoaded,
44+
handleCreateNote,
45+
handleDeleteNote,
46+
handleRenameNote,
47+
handleImportNote,
48+
handleExportNote,
49+
} = useNotesManager(setSnackbar);
50+
51+
if (!isLoaded) {
52+
return <div>Caricamento note...</div>;
53+
}
54+
55+
const activeNote = notes.find((note) => note.id === activeNoteId);
56+
57+
return (
58+
<>
59+
<FileSidebar
60+
notes={notes}
61+
activeId={activeNoteId}
62+
onSelect={setActiveNoteId}
63+
onCreate={handleCreateNote}
64+
onDelete={handleDeleteNote}
65+
onRename={handleRenameNote}
66+
onImport={handleImportNote}
67+
onExport={handleExportNote}
68+
/>
69+
<h2>{activeNote?.title}</h2>
70+
<p>{snackbar.open ? snackbar.message : ''}</p>
71+
</>
72+
);
73+
}
74+
75+
describe('FileSidebar integration (export flow)', () => {
76+
beforeEach(() => {
77+
vi.clearAllMocks();
78+
dbSetMock.mockResolvedValue(undefined);
79+
fileImportMock.mockResolvedValue(null);
80+
fileExportMock.mockResolvedValue(undefined);
81+
});
82+
83+
it('esporta la nota attiva quando clicco il bottone salva su disco', async () => {
84+
const user = userEvent.setup();
85+
dbGetMock.mockResolvedValue([
86+
{ id: 'note-1', title: 'Nota Uno', content: 'Contenuto uno', createdAt: 1 },
87+
]);
88+
89+
render(<FileSidebarExportHarness />);
90+
91+
await screen.findByRole('heading', { name: 'Nota Uno' });
92+
await user.click(screen.getByTitle(/salva nota su disco/i));
93+
94+
await waitFor(() =>
95+
expect(fileExportMock).toHaveBeenCalledWith('Nota Uno', 'Contenuto uno'),
96+
);
97+
});
98+
99+
it('se cambio nota attiva dalla lista, esporta la nuova nota selezionata', async () => {
100+
const user = userEvent.setup();
101+
dbGetMock.mockResolvedValue([
102+
{ id: 'note-1', title: 'Nota Uno', content: 'Contenuto uno', createdAt: 1 },
103+
{ id: 'note-2', title: 'Nota Due', content: 'Contenuto due', createdAt: 2 },
104+
]);
105+
106+
render(<FileSidebarExportHarness />);
107+
108+
await screen.findByRole('heading', { name: 'Nota Uno' });
109+
await user.click(screen.getByText('Nota Due'));
110+
await screen.findByRole('heading', { name: 'Nota Due' });
111+
112+
await user.click(screen.getByTitle(/salva nota su disco/i));
113+
await waitFor(() =>
114+
expect(fileExportMock).toHaveBeenCalledWith('Nota Due', 'Contenuto due'),
115+
);
116+
});
117+
});

0 commit comments

Comments
 (0)