Compare commits

6 Commits

Author SHA1 Message Date
f58083870f Merge pull request #46 from dat515-2025/merge/prometheus_custom_metrics
Some checks failed
Deploy Prod / Run Python Tests (push) Has been cancelled
Deploy Prod / Build and push image (reusable) (push) Has been cancelled
Deploy Prod / Generate Production URLs (push) Has been cancelled
Deploy Prod / Frontend - Build and Deploy to Cloudflare Pages (prod) (push) Has been cancelled
Deploy Prod / Helm upgrade/install (prod) (push) Has been cancelled
feat(prometheus): add custom metrics
2025-11-09 12:54:52 +01:00
ca8287cd8b feat(prometheus): add custom metrics 2025-11-09 12:43:27 +01:00
ribardej
ed3e6329dd feat(docs): new metting.md
Some checks failed
Deploy Prod / Run Python Tests (push) Has been cancelled
Deploy Prod / Build and push image (reusable) (push) Has been cancelled
Deploy Prod / Generate Production URLs (push) Has been cancelled
Deploy Prod / Frontend - Build and Deploy to Cloudflare Pages (prod) (push) Has been cancelled
Deploy Prod / Helm upgrade/install (prod) (push) Has been cancelled
2025-11-06 13:13:16 +01:00
ribardej
a214e2cd8b fix(test): fixed tests for local usage and documentation in report.md 2025-11-06 12:28:42 +01:00
6c8d2202b5 update report 2025-11-06 12:03:15 +01:00
Dejan Ribarovski
b480734fee Merge pull request #45 from dat515-2025/add_more_tests
feat(test): added more tests
2025-11-06 11:31:07 +01:00
6 changed files with 230 additions and 57 deletions

View File

@@ -9,6 +9,8 @@ from fastapi.middleware.cors import CORSMiddleware
from prometheus_fastapi_instrumentator import Instrumentator, metrics from prometheus_fastapi_instrumentator import Instrumentator, metrics
from starlette.requests import Request from starlette.requests import Request
from app.services.prometheus import number_of_users, number_of_transactions
from app.services import bank_scraper from app.services import bank_scraper
from app.workers.celery_tasks import load_transactions, load_all_transactions from app.workers.celery_tasks import load_transactions, load_all_transactions
from app.models.user import User, OAuthAccount from app.models.user import User, OAuthAccount
@@ -50,6 +52,9 @@ fastApi.add_middleware(
prometheus = Instrumentator().instrument(fastApi) prometheus = Instrumentator().instrument(fastApi)
# Register custom metrics
prometheus.add(number_of_users()).add(number_of_transactions())
prometheus.expose( prometheus.expose(
fastApi, fastApi,
endpoint="/metrics", endpoint="/metrics",

View File

@@ -0,0 +1,48 @@
from typing import Callable
from prometheus_fastapi_instrumentator.metrics import Info
from prometheus_client import Gauge
from sqlalchemy import select, func
from app.core.db import async_session_maker
from app.models.transaction import Transaction
from app.models.user import User
def number_of_users() -> Callable[[Info], None]:
METRIC = Gauge(
"number_of_users_total",
"Number of registered users.",
labelnames=("users",)
)
async def instrumentation(info: Info) -> None:
try:
async with async_session_maker() as session:
result = await session.execute(select(func.count(User.id)))
user_count = result.scalar_one() or 0
except Exception:
# In case of DB errors, avoid crashing metrics endpoint
user_count = 0
METRIC.labels(users="total").set(user_count)
return instrumentation
def number_of_transactions() -> Callable[[Info], None]:
METRIC = Gauge(
"number_of_transactions_total",
"Number of transactions stored.",
labelnames=("transactions",)
)
async def instrumentation(info: Info) -> None:
try:
async with async_session_maker() as session:
result = await session.execute(select(func.count()).select_from(Transaction))
transaction_count = result.scalar_one() or 0
except Exception:
# In case of DB errors, avoid crashing metrics endpoint
transaction_count = 0
METRIC.labels(transactions="total").set(transaction_count)
return instrumentation

View File

@@ -101,17 +101,26 @@ async def test_e2e_transaction_workflow(fastapi_app, test_user):
async def test_register_then_login_and_fetch_me(fastapi_app): async def test_register_then_login_and_fetch_me(fastapi_app):
transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True) transport = ASGITransport(app=fastapi_app, raise_app_exceptions=True)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac: async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
email = "newuser@example.com" # Use unique email to avoid duplicates across runs
suffix = uuid.uuid4().hex[:8]
email = f"newuser_{suffix}@example.com"
password = "StrongPassw0rd!" password = "StrongPassw0rd!"
reg = await ac.post("/auth/register", json={"email": email, "password": password}) reg = await ac.post("/auth/register", json={"email": email, "password": password})
assert reg.status_code in (status.HTTP_201_CREATED, status.HTTP_200_OK) assert reg.status_code in (status.HTTP_201_CREATED, status.HTTP_200_OK)
login = await ac.post("/auth/jwt/login", data={"username": email, "password": password}) login = await ac.post("/auth/jwt/login", data={"username": email, "password": password})
assert login.status_code == status.HTTP_200_OK assert login.status_code == status.HTTP_200_OK
token = login.json()["access_token"] token = login.json()["access_token"]
me = await ac.get("/users/me", headers={"Authorization": f"Bearer {token}"}) headers = {"Authorization": f"Bearer {token}"}
assert me.status_code == status.HTTP_200_OK try:
assert me.json()["email"] == email me = await ac.get("/users/me", headers=headers)
assert me.status_code == status.HTTP_200_OK
assert me.json()["email"] == email
finally:
# Cleanup: delete the created user so future runs wont conflict
d = await ac.delete("/users/me", headers=headers)
assert d.status_code == status.HTTP_204_NO_CONTENT
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -158,22 +167,44 @@ async def test_update_category_conflict_and_404(fastapi_app, test_user):
async def test_category_cross_user_isolation(fastapi_app): async def test_category_cross_user_isolation(fastapi_app):
transport = ASGITransport(app=fastapi_app) transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac: async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
# Generate unique emails for both users
sfx = uuid.uuid4().hex[:8]
u1 = {"email": f"u1_{sfx}@example.com", "password": "Aaaaaa1!"}
u2 = {"email": f"u2_{sfx}@example.com", "password": "Aaaaaa1!"}
# user1 # user1
u1 = {"email": "u1@example.com", "password": "Aaaaaa1!"}
assert (await ac.post("/auth/register", json=u1)).status_code in (200, 201) assert (await ac.post("/auth/register", json=u1)).status_code in (200, 201)
t1 = (await ac.post("/auth/jwt/login", data={"username": u1["email"], "password": u1["password"]})).json()["access_token"] t1 = (await ac.post("/auth/jwt/login", data={"username": u1["email"], "password": u1["password"]})).json()["access_token"]
h1 = {"Authorization": f"Bearer {t1}"}
# user1 creates a category # user1 creates a category
c = (await ac.post("/categories/create", json={"name": "Private"}, headers={"Authorization": f"Bearer {t1}"})).json() c = (await ac.post("/categories/create", json={"name": "Private"}, headers=h1)).json()
cat_id = c["id"]
# user2 # user2
u2 = {"email": "u2@example.com", "password": "Aaaaaa1!"}
assert (await ac.post("/auth/register", json=u2)).status_code in (200, 201) assert (await ac.post("/auth/register", json=u2)).status_code in (200, 201)
t2 = (await ac.post("/auth/jwt/login", data={"username": u2["email"], "password": u2["password"]})).json()["access_token"] t2 = (await ac.post("/auth/jwt/login", data={"username": u2["email"], "password": u2["password"]})).json()["access_token"]
h2 = {"Authorization": f"Bearer {t2}"}
# user2 cannot read/delete user1's category try:
g = await ac.get(f"/categories/{c['id']}", headers={"Authorization": f"Bearer {t2}"}) # user2 cannot read/delete user1's category
assert g.status_code == status.HTTP_404_NOT_FOUND g = await ac.get(f"/categories/{cat_id}", headers=h2)
d = await ac.delete(f"/categories/{c['id']}", headers={"Authorization": f"Bearer {t2}"}) assert g.status_code == status.HTTP_404_NOT_FOUND
assert d.status_code == status.HTTP_404_NOT_FOUND d = await ac.delete(f"/categories/{cat_id}", headers=h2)
assert d.status_code == status.HTTP_404_NOT_FOUND
finally:
# Cleanup: remove the created category as its owner
try:
_ = await ac.delete(f"/categories/{cat_id}", headers=h1)
except Exception:
pass
# Cleanup: delete both users to avoid email conflicts later
try:
_ = await ac.delete("/users/me", headers=h1)
except Exception:
pass
try:
_ = await ac.delete("/users/me", headers=h2)
except Exception:
pass

View File

@@ -43,8 +43,8 @@ The tracker should not store the transactions in the database - security vulnera
Last 3 minutes of the meeting, summarize action items. Last 3 minutes of the meeting, summarize action items.
- [ ] Change the name on frontend from 7project - [x] Change the name on frontend from 7project
- [ ] Finalize the funcionality and everyting in the code part - [x] Finalize the funcionality and everyting in the code part
- [ ] Try to finalize report with focus on reproducibility - [ ] Try to finalize report with focus on reproducibility
- [ ] More high level explanation of the workflow in the report - [ ] More high level explanation of the workflow in the report

View File

@@ -0,0 +1,47 @@
# Weekly Meeting Notes
- Group 8 - Personal finance tracker
- Mentor: Jaychander
Keep all meeting notes in the `meetings.md` file in your project folder.
Just copy the template below for each weekly meeting and fill in the details.
## Administrative Info
- Date: 2025-10-30
- Attendees: Dejan, Lukas
- Notetaker: Dejan
## Progress Update (Before Meeting)
Last 3 minutes of the meeting, summarize action items.
- [x] Change the name on frontend from 7project
- [x] Finalize the funcionality and everyting in the code part
- [x] Try to finalize report with focus on reproducibility
- [x] More high level explanation of the workflow in the report
Summary of what has been accomplished since the last meeting in the following categories.
### Coding
### Documentation
## Questions and Topics for Discussion (Before Meeting)
## Discussion Notes (During Meeting)
The tracker should not store the transactions in the database - security vulnerability.
## Action Items for Next Week (During Meeting)
Last 3 minutes of the meeting, summarize action items.
- [ ] video
- [ ] highlight the optional stuff in the report
---

View File

@@ -14,7 +14,7 @@
- 289229, Lukáš Trkan, lukastrkan - 289229, Lukáš Trkan, lukastrkan
- 289258, Dejan Ribarovski, derib2613, ribardej - 289258, Dejan Ribarovski, derib2613, ribardej
**Brief Description**: (něco spíš jako abstract, introuction, story behind) **Brief Description**:
Our application is a finance tracker, so a person can easily track his cash flow Our application is a finance tracker, so a person can easily track his cash flow
through multiple bank accounts. Person can label transactions with custom categories through multiple bank accounts. Person can label transactions with custom categories
and later filter by them. and later filter by them.
@@ -34,9 +34,16 @@ flowchart LR
client[Client/Frontend] <--> svc[Backend API] client[Client/Frontend] <--> svc[Backend API]
svc --> proc_queue svc --> proc_queue
svc <--> db[(Database)] svc <--> db[(Database)]
svc <--> cache[(Cache)]
``` ```
The workflow works in the following way:
- Client connects to the frontend. After login, frontend automatically fetches the stored transactions from
the database via the backend API
- When the client opts for fetching new transactions via the Bank API, the backend delegates the task
to a background worker service via the Message queue.
- After successful load, these transactions are stored to the database and displayed to the client
- There is also a Task planner, that executes periodic tasks, like fetching new transactions automatically from the Bank API
### Components ### Components
- Frontend (frontend/): React + TypeScript app built with Vite. Talks to the backend via REST, handles login/registration, shows latest transactions, filtering, and allows adding transactions. - Frontend (frontend/): React + TypeScript app built with Vite. Talks to the backend via REST, handles login/registration, shows latest transactions, filtering, and allows adding transactions.
@@ -123,12 +130,13 @@ docker compose up --build
# Set environment variables (or create .env file) # Set environment variables (or create .env file)
# TODO: fix # TODO: fix
export SECRET=CHANGE_ME_SECRET export SECRET=CHANGE_ME_SECRET
export BACKEND_URL=http://127.0.0.1:8000 export FRONTEND_DOMAIN_SCHEME=http://localhost:5173
export FRONTEND_URL=http://localhost:5173 export BANKID_CLIENT_ID=CHANGE_ME
export DATABASE_URL=postgresql+asyncpg://user:password@127.0.0.1:5432/app export BANKID_CLIENT_SECRET=CHANGE_ME
export RABBITMQ_URL=amqp://guest:guest@127.0.0.1:5672/ export CSAS_CLIENT_ID=CHANGE_ME
export REDIS_URL=redis://127.0.0.1:6379/0 export CSAS_CLIENT_SECRET=CHANGE_ME
export MOJEID_CLIENT_ID=CHANGE_ME
export MOJEID_CLIENT_SECRET=CHANGE_ME
# Apply DB migrations (Alembic) # Apply DB migrations (Alembic)
# From 7project # From 7project
bash upgrade_database.sh bash upgrade_database.sh
@@ -164,7 +172,38 @@ npm run build
``` ```
## Deployment Instructions ## Deployment Instructions
### Setup Cluster
Deployment should work on any Kubernetes cluster. However, we are using 4 TalosOS virtual machines (1 control plane, 3 workers)
running on top of Proxmox VE.
1) Create 4 VMs with TalosOS
2) Install talosctl for your OS: https://docs.siderolabs.com/talos/v1.10/getting-started/talosctl
3) Generate Talos config
```bash
# TODO: add commands
```
4) Edit the generated worker.yaml
- add google container registry mirror
- add modules from config generator
- add extramounts for persistent storage
- add kernel modules
5) Apply the config to the VMs
```bash
#TODO: add config apply commands
```
6) Verify the cluster is up
```bash
```
7) Export kubeconfig
```bash
# TODO: add export command
```
### Install
1) Install base services to cluster 1) Install base services to cluster
```bash ```bash
cd tofu cd tofu
@@ -172,7 +211,7 @@ cd tofu
cp terraform.tfvars.example terraform.tfvars cp terraform.tfvars.example terraform.tfvars
# authenticate to your cluster/cloud as needed, then: # authenticate to your cluster/cloud as needed, then:
tofu init tofu init
tofu plan tofu apply -exclude modules.cloudflare
tofu apply tofu apply
``` ```
@@ -217,28 +256,28 @@ open http://localhost:5173
``` ```
## Testing Instructions ## Testing Instructions
The tests are located in 7project/backend/tests directory
If you want to test locally, you have to have the DB running locally as well (start the docker compose in /backend).
```bash
cd backend
```
### Unit Tests ### Unit Tests
There are only 3 basic unit tests, since our services logic is very simple
```bash ```bash
# Commands to run unit tests pytest tests/test_unit_user_service.py
# For example:
# go test ./...
# npm test
``` ```
### Integration Tests ### Integration Tests
There are 11 basic unit tests, testing the individual backend API logic
```bash ```bash
# Commands to run integration tests pytest tests/test_integration_app.py
# Any setup required for integration tests
``` ```
### End-to-End Tests ### End-to-End Tests
There are 7 e2e tests testing more complex app logic
```bash ```bash
# Commands to run e2e tests pytest tests/test_e2e.py
# How to set up test environment
``` ```
## Usage Examples ## Usage Examples
@@ -315,24 +354,24 @@ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/authenticated-route
--- ---
## Self-Assessment Table ## Progress Table
> Be honest and detailed in your assessments. > Be honest and detailed in your assessments.
> This information is used for individual grading. > This information is used for individual grading.
> Link to the specific commit on GitHub for each contribution. > Link to the specific commit on GitHub for each contribution.
| Task/Component | Assigned To | Status | Time Spent | Difficulty | Notes | | Task/Component | Assigned To | Status | Time Spent | Difficulty | Notes |
|-----------------------------------------------------------------------|-------------| ------------- |----------------|------------| ----------- | |-----------------------------------------------------------------------|-------------| ------------- |------------|------------| ----------- |
| [Project Setup & Repository](https://github.com/dat515-2025/Group-8#) | Lukas | ✅ Complete | [X hours] | Medium | [Any notes] | | [Project Setup & Repository](https://github.com/dat515-2025/Group-8#) | Lukas | ✅ Complete | [X hours] | Medium | [Any notes] |
| [Design Document](https://github.com/dat515-2025/Group-8/blob/main/6design/design.md) | Both | ✅ Complete | 2 Hours | Easy | [Any notes] | | [Design Document](https://github.com/dat515-2025/Group-8/blob/main/6design/design.md) | Both | ✅ Complete | 4 Hours | Easy | [Any notes] |
| [Backend API Development](https://github.com/dat515-2025/Group-8/tree/main/7project/backend/app/api) | Dejan | 🔄 In Progress | 10 hours | Medium | [Any notes] | | [Backend API Development](https://github.com/dat515-2025/Group-8/tree/main/7project/backend/app/api) | Dejan | ✅ Complete | 12 hours | Medium | [Any notes] |
| [Database Setup & Models](https://github.com/dat515-2025/Group-8/tree/main/7project/backend/app/models) | Lukas | 🔄 In Progress | [X hours] | Medium | [Any notes] | | [Database Setup & Models](https://github.com/dat515-2025/Group-8/tree/main/7project/backend/app/models) | Lukas | 🔄 In Progress | [X hours] | Medium | [Any notes] |
| [Frontend Development](https://github.com/dat515-2025/Group-8/tree/main/7project/frontend) | Dejan | 🔄 In Progress | 7 hours so far | Medium | [Any notes] | | [Frontend Development](https://github.com/dat515-2025/Group-8/tree/main/7project/frontend) | Dejan | ✅ Complete | 17 hours | Medium | [Any notes] |
| [Docker Configuration](https://github.com/dat515-2025/Group-8/blob/main/7project/compose.yml) | Lukas | ✅ Complete | [X hours] | Easy | [Any notes] | | [Docker Configuration](https://github.com/dat515-2025/Group-8/blob/main/7project/compose.yml) | Lukas | ✅ Complete | [X hours] | Easy | [Any notes] |
| [Cloud Deployment](https://github.com/dat515-2025/Group-8/blob/main/7project/deployment/app-demo-deployment.yaml) | Lukas | ✅ Complete | [X hours] | Hard | [Any notes] | | [Cloud Deployment](https://github.com/dat515-2025/Group-8/blob/main/7project/deployment/app-demo-deployment.yaml) | Lukas | ✅ Complete | [X hours] | Hard | [Any notes] |
| [Testing Implementation](https://github.com/dat515-2025/group-name) | Dejan | 🔄 In Progress | [X hours] | Medium | [Any notes] | | [Testing Implementation](https://github.com/dat515-2025/group-name) | Dejan | ✅ Complete | 16 hours | Medium | [Any notes] |
| [Documentation](https://github.com/dat515-2025/group-name) | Both | 🔄 In Progress | [X hours] | Easy | [Any notes] | | [Documentation](https://github.com/dat515-2025/group-name) | Both | 🔄 In Progress | [X hours] | Easy | [Any notes] |
| [Presentation Video](https://github.com/dat515-2025/group-name) | Both | ❌ Not Started | [X hours] | Medium | [Any notes] | | [Presentation Video](https://github.com/dat515-2025/group-name) | Both | ❌ Not Started | [X hours] | Medium | [Any notes] |
**Legend**: ✅ Complete | 🔄 In Progress | ⏳ Pending | ❌ Not Started **Legend**: ✅ Complete | 🔄 In Progress | ⏳ Pending | ❌ Not Started
@@ -353,15 +392,18 @@ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/authenticated-route
### Dejan ### Dejan
| Date | Activity | Hours | Description | | Date | Activity | Hours | Description |
|-----------------|----------------------|--------|----------------------------------------------------------------------------------| |-----------------|----------------------|--------|---------------------------------------------------------------|
| 25.9. | Design | 2 | 6design | | 25.9. | Design | 2 | 6design |
| 9.10 to 11.10. | Backend APIs | 10 | Implemented Backend APIs | | 9.10 to 11.10. | Backend APIs | 12 | Implemented Backend APIs |
| 13.10 to 15.10. | Frontend Development | 7 | Created user interface mockups | | 13.10 to 15.10. | Frontend Development | 8 | Created user interface mockups |
| Continually | Documantation | 5 | Documenting the dev process | | Continually | Documentation | 6 | Documenting the dev process |
| 21.10 to 23.10 | Tests, forntend | 10 | Test basics, balance charts, and frontend improvement | | 21.10 to 23.10 | Tests, frontend | 10 | Test basics, balance charts, and frontend improvement |
| 28.10 to 30.10 | Tests, forntend | 7 | Tests improvement with test database setup, UI fix and exchange rate integration | | 28.10 to 30.10 | CI | 6 | Integrated tests with test database setup on github workflows |
| **Total** | | **41** | | | 28.10 to 30.10 | Frontend | 7 | UI improvements and exchange rate API integration |
| 4.11 to 6.11 | Tests | 6 | Test fixes improvement, more integration and e2e |
| 4.11 to 6.11 | Frontend | 6 | Fixes, Improved UI, added support for mobile devices |
| **Total** | | **63** | |
### Group Total: [XXX.X] hours ### Group Total: [XXX.X] hours