Compare commits

14 Commits

Author SHA1 Message Date
96ebc27001 updates
Some checks are pending
Deploy Prod / Run Python Tests (push) Waiting to run
Deploy Prod / Build and push image (reusable) (push) Blocked by required conditions
Deploy Prod / Generate Production URLs (push) Blocked by required conditions
Deploy Prod / Frontend - Build and Deploy to Cloudflare Pages (prod) (push) Blocked by required conditions
Deploy Prod / Helm upgrade/install (prod) (push) Blocked by required conditions
2025-11-12 17:34:50 +01:00
ribardej
922651fdbf fix(frontend): implemented CSAS button responsiveness 2025-11-12 15:37:53 +01:00
ribardej
e164b185e0 feat(frontend): implemented CSAS button responsiveness 2025-11-12 15:31:30 +01:00
ribardej
186b4fd09a fix(frontend): implemented multiple transaction selections in UI 2025-11-12 15:21:08 +01:00
ribardej
280d495335 feat(frontend): implemented multiple transaction selections in UI 2025-11-12 15:10:00 +01:00
ribardej
e73233c90a feat(docs): report.md update and refactored tests 2025-11-12 14:42:04 +01:00
ribardej
aade78bf3f feat(docs): report.md update and added options to test-with-ephemeral-mariadb.sh 2025-11-12 14:12:04 +01:00
ribardej
50e489a8e0 feat(tests): implemented local test DB container for isolation 2025-11-12 13:29:20 +01:00
ribardej
1679abb71f feat(tests): implemented local test DB container for isolation 2025-11-12 13:29:09 +01:00
573404dead feat(infrastructure): use correct url
Some checks are pending
Deploy Prod / Run Python Tests (push) Waiting to run
Deploy Prod / Build and push image (reusable) (push) Blocked by required conditions
Deploy Prod / Generate Production URLs (push) Blocked by required conditions
Deploy Prod / Frontend - Build and Deploy to Cloudflare Pages (prod) (push) Blocked by required conditions
Deploy Prod / Helm upgrade/install (prod) (push) Blocked by required conditions
2025-11-12 01:11:53 +01:00
d57dd82a64 feat(infrastructure): use correct url 2025-11-12 01:09:29 +01:00
50f37c1161 feat(infrastructure): use newer image 2025-11-12 00:58:54 +01:00
ae22d2ee5f feat(infrastructure): make tests mandatory 2025-11-12 00:46:36 +01:00
509608f8c9 Merge pull request #50 from dat515-2025/merge/update_workers
feat(workers): update workers
2025-11-12 00:42:16 +01:00
18 changed files with 332 additions and 121 deletions

View File

@@ -27,6 +27,7 @@ jobs:
build:
name: Build and push image (reusable)
needs: [test]
uses: ./.github/workflows/build-image.yaml
with:
mode: prod
@@ -36,6 +37,7 @@ jobs:
get_urls:
name: Generate Production URLs
needs: [test]
uses: ./.github/workflows/url_generator.yml
with:
mode: prod

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim
FROM python:3.11-trixie
WORKDIR /app
COPY requirements.txt .

View File

@@ -1,5 +1,6 @@
import json
import logging
import os
from os.path import dirname, join
from time import strptime
from uuid import UUID
@@ -55,7 +56,7 @@ def _load_mock_bank_transactions(user_id: UUID) -> None:
transactions = []
with httpx.Client() as client:
response = client.get("http://127.0.0.1:8000/mock-bank/scrape")
response = client.get(f"{os.getenv('APP_POD_URL')}/mock-bank/scrape")
if response.status_code != httpx.codes.OK:
return
for transaction in response.json():

View File

@@ -0,0 +1,20 @@
version: "3.9"
services:
mariadb:
image: mariadb:11.4
container_name: test-mariadb
environment:
MARIADB_ROOT_PASSWORD: rootpw
MARIADB_DATABASE: group_project
MARIADB_USER: appuser
MARIADB_PASSWORD: apppass
ports:
- "3307:3306" # host:container (use 3307 on host to avoid conflicts)
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-u", "root", "-prootpw", "--silent"]
interval: 5s
timeout: 2s
retries: 20
# Truly ephemeral, fast storage (removed when container stops)
tmpfs:
- /var/lib/mysql

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
# Run tests against a disposable local MariaDB on host port 3307 using Docker Compose.
# Requirements: Docker, docker compose plugin, Python, Alembic, pytest.
# Usage:
# chmod +x ./test-with-ephemeral-mariadb.sh
# # From 7project/backend directory
# ./test-with-ephemeral-mariadb.sh [--only-unit|--only-integration|--only-e2e] [pytest-args...]
# # Examples:
# ./test-with-ephemeral-mariadb.sh --only-unit -q
# ./test-with-ephemeral-mariadb.sh --only-integration -k "login"
# ./test-with-ephemeral-mariadb.sh --only-e2e -vv
#
# This script will:
# 1) Start a MariaDB 11.4 container (ephemeral storage, port 3307)
# 2) Wait until it's healthy
# 3) Export env vars expected by the app (DATABASE_URL etc.)
# 4) Run Alembic migrations
# 5) Run pytest
# 6) Tear everything down (containers and tmpfs data)
COMPOSE_FILE="docker-compose.test.yml"
SERVICE_NAME="mariadb"
CONTAINER_NAME="test-mariadb"
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required but not found in PATH" >&2
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose V2 plugin is required (docker compose)" >&2
exit 1
fi
# Bring up the DB
echo "Starting MariaDB (port 3307) with docker compose..."
docker compose -f "$COMPOSE_FILE" up -d
# Ensure we clean up on exit
cleanup() {
echo "\nTearing down docker compose stack..."
docker compose -f "$COMPOSE_FILE" down -v || true
}
trap cleanup EXIT
# Wait for healthy container
echo -n "Waiting for MariaDB to become healthy"
for i in {1..60}; do
status=$(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ "$status" = "healthy" ]; then
echo " -> healthy"
break
fi
echo -n "."
sleep 1
if [ $i -eq 60 ]; then
echo "\nMariaDB did not become healthy in time" >&2
exit 1
fi
done
# Export env vars for the app/tests (match app/core/db.py expectations)
export MARIADB_HOST=127.0.0.1
export MARIADB_PORT=3307
export MARIADB_DB=group_project
export MARIADB_USER=appuser
export MARIADB_PASSWORD=apppass
export DATABASE_URL="mysql+asyncmy://$MARIADB_USER:$MARIADB_PASSWORD@$MARIADB_HOST:$MARIADB_PORT/$MARIADB_DB"
export PYTEST_RUN_CONFIG="True"
# Determine which tests to run based on flags
UNIT_TESTS="tests/test_unit_user_service.py"
INTEGRATION_TESTS="tests/test_integration_app.py"
E2E_TESTS="tests/test_e2e.py"
FLAG_COUNT=0
TEST_TARGET=""
declare -a PYTEST_ARGS=()
for arg in "$@"; do
case "$arg" in
--only-unit)
TEST_TARGET="$UNIT_TESTS"; FLAG_COUNT=$((FLAG_COUNT+1));;
--only-integration)
TEST_TARGET="$INTEGRATION_TESTS"; FLAG_COUNT=$((FLAG_COUNT+1));;
--only-e2e)
TEST_TARGET="$E2E_TESTS"; FLAG_COUNT=$((FLAG_COUNT+1));;
*)
PYTEST_ARGS+=("$arg");;
esac
done
if [ "$FLAG_COUNT" -gt 1 ]; then
echo "Error: Use only one of --only-unit, --only-integration, or --only-e2e" >&2
exit 2
fi
# Run Alembic migrations then tests
pushd . >/dev/null
echo "Running Alembic migrations..."
alembic upgrade head
echo "Running pytest..."
if [ -n "$TEST_TARGET" ]; then
# Use "${PYTEST_ARGS[@]:-}" to safely expand empty array with 'set -u'
pytest "$TEST_TARGET" "${PYTEST_ARGS[@]:-}"
else
# Use "${PYTEST_ARGS[@]:-}" to safely expand empty array with 'set -u'
pytest "${PYTEST_ARGS[@]:-}"
fi
popd >/dev/null
# Cleanup handled by trap

View File

@@ -3,17 +3,6 @@ import pytest
from httpx import AsyncClient, ASGITransport
def test_root_ok(client):
resp = client.get("/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == {"status": "ok"}
def test_authenticated_route_requires_auth(client):
resp = client.get("/authenticated-route")
assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
@pytest.mark.asyncio
async def test_create_and_get_category(fastapi_app, test_user):
# Use AsyncClient for async tests
@@ -165,6 +154,6 @@ async def test_delete_transaction_not_found(fastapi_app, test_user):
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
token = (await ac.post("/auth/jwt/login", data=test_user)).json()["access_token"]
h = {"Authorization": f"Bearer {token}"}
r = await ac.delete("/transactions/999999/delete", headers=h)
r = await ac.delete("/transactions/9999999/delete", headers=h)
assert r.status_code == status.HTTP_404_NOT_FOUND

View File

@@ -1,7 +1,5 @@
import types
import asyncio
import pytest
from fastapi import status
from app.services import user_service
@@ -22,6 +20,15 @@ def test_get_jwt_strategy_lifetime():
# Basic smoke check: strategy has a lifetime set to 604800
assert getattr(strategy, "lifetime_seconds", None) in (604800,)
def test_root_ok(client):
resp = client.get("/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == {"status": "ok"}
def test_authenticated_route_requires_auth(client):
resp = client.get("/authenticated-route")
assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
@pytest.mark.asyncio
async def test_on_after_request_verify_enqueues_email(monkeypatch):

View File

@@ -120,3 +120,5 @@ spec:
secretKeyRef:
name: prod
key: SMTP_FROM
- name: APP_POD_URL
value: {{ printf "http://%s.%s.svc.cluster.local" .Values.app.name .Release.Namespace | quote }}

View File

@@ -133,6 +133,9 @@ export type User = {
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
// Optional JSON config object for user-level integrations and settings
// Example: { csas: "{\"expires_at\": 1761824615, ...}" } or { csas: { expires_at: 1761824615, ... } }
config?: Record<string, any> | null;
};
export async function getMe(): Promise<User> {

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { type Category, type Transaction, type BalancePoint, deleteTransaction, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
import { useEffect, useMemo, useState, useCallback } from 'react';
import { type Category, type Transaction, type BalancePoint, getMe, deleteTransaction, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
import AccountPage from './AccountPage';
import AppearancePage from './AppearancePage';
import BalanceChart from './BalanceChart';
@@ -118,6 +118,47 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
const [isMockModalOpen, setMockModalOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
// Current user and CSAS connection status
const [csasConnected, setCsasConnected] = useState(false);
useEffect(() => {
(async () => {
try {
const u = await getMe();
// Determine CSAS connection validity
const csas = (u as any)?.config?.csas;
let obj: any = null;
if (csas) {
if (typeof csas === 'string') {
try { obj = JSON.parse(csas); } catch {}
} else if (typeof csas === 'object') {
obj = csas;
}
}
let exp: number | null = null;
const raw = obj?.expires_at;
if (typeof raw === 'number') {
exp = raw;
} else if (typeof raw === 'string') {
const asNum = Number(raw);
if (!Number.isNaN(asNum)) {
exp = asNum;
} else {
const ms = Date.parse(raw);
if (!Number.isNaN(ms)) exp = Math.floor(ms / 1000);
}
}
if (exp && exp > Math.floor(Date.now() / 1000)) {
setCsasConnected(true);
} else {
setCsasConnected(false);
}
} catch (e) {
// ignore, user may not be loaded; keep button enabled
}
})();
}, []);
// Start CSAS (George) OAuth after login
async function startOauthCsas() {
const base = BACKEND_URL.replace(/\/$/, '');
@@ -168,7 +209,14 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
// Sidebar toggle for mobile
const [sidebarOpen, setSidebarOpen] = useState(false);
// Multi-select state for transactions and bulk category assignment
const [selectedTxIds, setSelectedTxIds] = useState<number[]>([]);
const [bulkCategoryIds, setBulkCategoryIds] = useState<number[]>([]);
const toggleSelectTx = useCallback((id: number) => {
setSelectedTxIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
}, []);
const clearSelection = useCallback(() => setSelectedTxIds([]), []);
const selectAllVisible = useCallback((ids: number[]) => setSelectedTxIds(ids), []);
async function loadAll() {
setLoading(true);
@@ -241,7 +289,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
}
}
useEffect(() => { loadAll(); }, [startDate, endDate]);
useEffect(() => { loadAll(); clearSelection(); }, [startDate, endDate]);
const filtered = useMemo(() => {
let arr = [...transactions];
@@ -267,6 +315,9 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
const pageEnd = pageStart + pageSize;
const visible = sortedDesc.slice(pageStart, pageEnd);
// Reset selection when page or filters impacting visible set change
useEffect(() => { clearSelection(); }, [page, minAmount, maxAmount, filterCategoryId, searchText]);
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
@@ -354,7 +405,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
<h3>Bank connections</h3>
<div className="connection-row">
<p className="muted" style={{ margin: 0 }}>Connect your CSAS (George) account.</p>
<button className="btn primary" onClick={startOauthCsas}>Connect CSAS (George)</button>
<button className="btn primary" onClick={startOauthCsas} disabled={csasConnected}>{csasConnected ? 'Successfully connected to CSAS' : 'Connect CSAS (George)'}</button>
</div>
<div className="connection-row">
<p className="muted" style={{ margin: 0 }}>Generate data from a mock bank.</p>
@@ -416,7 +467,55 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
<div className="muted">
Showing {visible.length} of {filtered.length} (page {Math.min(page + 1, Math.max(1, totalPages))}/{Math.max(1, totalPages)})
</div>
<div className="actions">
<div className="actions" style={{ gap: 8, alignItems: 'center' }}>
{selectedTxIds.length > 0 && (
<>
<span className="muted">Selected: {selectedTxIds.length}</span>
<select
className="input"
multiple
value={bulkCategoryIds.map(String)}
onChange={(e) => {
const ids = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
setBulkCategoryIds(ids);
}}
>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<button
className="btn primary"
onClick={async () => {
if (bulkCategoryIds.length === 0) {
alert('Pick at least one category to assign.');
return;
}
try {
// Apply selected categories to each selected transaction, replacing their categories
const updates = await Promise.allSettled(
selectedTxIds.map(id => updateTransaction(id, { category_ids: bulkCategoryIds }))
);
const fulfilled = updates.filter(u => u.status === 'fulfilled') as PromiseFulfilledResult<Transaction>[];
const updatedById = new Map<number, Transaction>(fulfilled.map(f => [f.value.id, f.value]));
setTransactions(prev => prev.map(t => updatedById.get(t.id) || t));
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
if (fulfilled.length !== selectedTxIds.length) {
alert(`Assigned categories to ${fulfilled.length} of ${selectedTxIds.length} selected transactions. Some updates failed.`);
}
} catch (e: any) {
alert(e?.message || 'Failed to assign categories');
} finally {
clearSelection();
setBulkCategoryIds([]);
}
}}
>
Apply categories to selected
</button>
<button className="btn" onClick={clearSelection}>Clear selection</button>
</>
)}
<button className="btn primary" disabled={page <= 0} onClick={() => setPage(p => Math.max(0, p - 1))}>Previous</button>
<button className="btn primary" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next</button>
</div>
@@ -424,6 +523,21 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
<table className="table responsive">
<thead>
<tr>
<th style={{ width: 36 }}>
<input
type="checkbox"
aria-label="Select all on page"
checked={visible.length > 0 && visible.every(v => selectedTxIds.includes(v.id))}
onChange={(e) => {
if (e.currentTarget.checked) {
selectAllVisible(visible.map(v => v.id));
} else {
// remove only currently visible from selection
setSelectedTxIds(prev => prev.filter(id => !visible.some(v => v.id === id)));
}
}}
/>
</th>
<th>Date</th>
<th style={{ textAlign: 'right' }}>Amount</th>
<th>Description</th>
@@ -433,7 +547,15 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
</thead>
<tbody>
{visible.map(t => (
<tr key={t.id}>
<tr key={t.id} style={{ backgroundColor: selectedTxIds.includes(t.id) ? 'rgba(88, 136, 255, 0.1)' : undefined }}>
<td>
<input
type="checkbox"
aria-label={`Select transaction ${t.id}`}
checked={selectedTxIds.includes(t.id)}
onChange={() => toggleSelectTx(t.id)}
/>
</td>
{/* Date cell */}
<td data-label="Date">
{editingTxId === t.id ? (

View File

@@ -1,9 +1,9 @@
# Personal finance tracker
> **Instructions**:
<!--- **Instructions**:
> This template provides the structure for your project report.
> Replace the placeholder text with your actual content.
> Remove instructions that are not relevant for your project, but leave the headings along with a (NA) label.
> Remove instructions that are not relevant for your project, but leave the headings along with a (NA) label. -->
## Project Overview
@@ -12,15 +12,21 @@
**Group Members**:
- 289229, Lukáš Trkan, lukastrkan
- 289258, Dejan Ribarovski, derib2613, ribardej
- 289258, Dejan Ribarovski, ribardej (derib2613)
**Brief Description**:
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
and later filter by them.
Our application allows users to easily track their cash flow
through multiple bank accounts. Users can label their transactions with custom categories that can be later used for
filtering and visualization. New transactions are automatically fetched in the background.
## Architecture Overview
Our system is a fullstack web application composed of a React frontend, a FastAPI backend, a PostgreSQL database, and asynchronous background workers powered by Celery with RabbitMQ. Redis is available for caching/kv and may be used by Celery as a result backend. The backend exposes REST endpoints for authentication (email/password and OAuth), users, categories, and transactions. A thin controller layer (FastAPI routers) lives under app/api. Infrastructure for Kubernetes is provided via OpenTofu (Terraformcompatible) modules and the application is packaged via a Helm chart.
Our system is a fullstack web application composed of a React frontend, a FastAPI backend,
a PostgreSQL database, and asynchronous background workers powered by Celery with RabbitMQ.
Redis is available for caching/kv and may be used by Celery as a result backend. The backend
exposes REST endpoints for authentication (email/password and OAuth), users, categories,
transactions, exchange rates and bank APIs. A thin controller layer (FastAPI routers) lives under app/api.
Infrastructure for Kubernetes is provided via OpenTofu (Terraformcompatible) modules and
the application is packaged via a Helm chart.
### High-Level Architecture
@@ -28,26 +34,33 @@ Our system is a fullstack web application composed of a React frontend, a Fas
flowchart LR
proc_queue[Message Queue] --> proc_queue_worker[Worker Service]
proc_queue_worker --> ext_mail[(Email Service)]
proc_cron[Task planner] --> proc_queue
proc_cron[Cron] --> svc
proc_queue_worker --> ext_bank[(Bank API)]
proc_queue_worker --> db
client[Client/Frontend] <--> svc[Backend API]
svc --> proc_queue
svc <--> db[(Database)]
svc <--> api[(UniRate API)]
```
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
the database via the backend API and currency rates from UniRate 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
- There is also a Task planner, that executes periodic tasks, like fetching new transactions automatically from the Bank APIs
### Features
- The stored transactions are encrypted in the DB for security reasons.
- For every pull request the full APP is deployed on a separate URL and the tests are run by github CI/CD
- On every push to main, the production app is automatically updated
- UI is responsive for mobile devices
### 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.
- Backend API (backend/app): FastAPI app with routers under app/api for auth, categories, and transactions. Uses FastAPI Users for auth (JWT + OAuth), SQLAlchemy ORM, and Pydantic v2 schemas.
- Backend API (backend/app): FastAPI app with routers under app/api for auth, users, categories, transactions, exchange rates and bankAPI. Uses FastAPI Users for auth (JWT + OAuth), SQLAlchemy ORM, and Pydantic v2 schemas.
- Worker service (backend/app/workers): Celery worker handling asynchronous tasks (e.g., sending verification emails, future background processing).
- Database (PostgreSQL): Persists users, categories, transactions; schema managed by Alembic migrations.
- Message Queue (RabbitMQ): Transports background jobs from the API to the worker.
@@ -59,7 +72,7 @@ to a background worker service via the Message queue.
- Backend: Python, FastAPI, FastAPI Users, SQLAlchemy, Pydantic, Alembic, Celery
- Frontend: React, TypeScript, Vite
- Database: MariaDB (Maxscale)
- Database: MariaDB with Maxscale
- Background jobs: RabbitMQ, Celery
- Containerization/Orchestration: Docker, Docker Compose (dev), Kubernetes, Helm
- IaC/Platform: Proxmox, Talos, Cloudflare pages, OpenTofu (Terraform), cert-manager, MetalLB, Cloudflare Tunnel, Prometheus, Loki
@@ -86,7 +99,7 @@ to a background worker service via the Message queue.
### Environment Variables (common)
# TODO: UPDATE
- Backend: SECRET, FRONTEND_URL, BACKEND_URL, DATABASE_URL, RABBITMQ_URL, REDIS_URL
- Backend: SECRET, FRONTEND_URL, BACKEND_URL, DATABASE_URL, RABBITMQ_URL, REDIS_URL, UNIRATE_API_KEY
- OAuth vars (Backend): MOJEID_CLIENT_ID/SECRET, BANKID_CLIENT_ID/SECRET (optional)
- Frontend: VITE_BACKEND_URL
@@ -256,28 +269,33 @@ open http://localhost:5173
```
## 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).
The tests are located in 7project/backend/tests directory. All tests are run by GitHub actions on every pull request and push to main.
See the workflow [here](../.github/workflows/run-tests.yml).
If you want to run the tests locally, the preferred is to use a [bash script](backend/test-with-ephemeral-mariadb.sh)
that will start a [test DB container](backend/docker-compose.test.yml) and remove it afterward.
```bash
cd backend
cd 7project/backend
bash test-with-ephemeral-mariadb.sh
```
### Unit Tests
There are only 3 basic unit tests, since our services logic is very simple
There are only 5 basic unit tests, since our services logic is very simple
```bash
pytest tests/test_unit_user_service.py
bash test-with-ephemeral-mariadb.sh --only-unit
```
### Integration Tests
There are 11 basic unit tests, testing the individual backend API logic
There are 9 basic unit tests, testing the individual backend API logic
```bash
pytest tests/test_integration_app.py
bash test-with-ephemeral-mariadb.sh --only-integration
```
### End-to-End Tests
There are 7 e2e tests testing more complex app logic
There are 7 e2e tests, testing more complex app logic
```bash
pytest tests/test_e2e.py
bash test-with-ephemeral-mariadb.sh --only-e2e
```
## Usage Examples

View File

@@ -105,14 +105,6 @@ module "database" {
s3_key_secret = var.s3_key_secret
}
#module "argocd" {
# source = "${path.module}/modules/argocd"
# depends_on = [module.storage, module.loadbalancer, module.cloudflare]
# argocd_admin_password = var.argocd_admin_password
# cloudflare_domain = var.cloudflare_domain
#}
#module "redis" {
# source = "${path.module}/modules/redis"
# depends_on = [module.storage]

View File

@@ -1,14 +0,0 @@
apiVersion: networking.cfargotunnel.com/v1alpha1
kind: TunnelBinding
metadata:
name: argocd-tunnel-binding
namespace: argocd
subjects:
- name: argocd-server
spec:
target: https://argocd-server.argocd.svc.cluster.local
fqdn: argocd.${base_domain}
noTlsVerify: true
tunnelRef:
kind: ClusterTunnel
name: cluster-tunnel

View File

@@ -1,39 +0,0 @@
terraform {
required_providers {
kubectl = {
source = "gavinbunney/kubectl"
version = "1.19.0"
}
helm = {
source = "hashicorp/helm"
version = "3.0.2"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.38.0"
}
}
}
resource "kubernetes_namespace" "argocd" {
metadata {
name = "argocd"
}
}
resource "helm_release" "argocd" {
name = "argocd"
namespace = "argocd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
depends_on = [kubernetes_namespace.argocd]
}
resource "kubectl_manifest" "argocd-tunnel-bind" {
depends_on = [helm_release.argocd]
yaml_body = templatefile("${path.module}/argocd-ui.yaml", {
base_domain = var.cloudflare_domain
})
}

View File

@@ -1,12 +0,0 @@
variable "argocd_admin_password" {
type = string
nullable = false
sensitive = true
description = "ArgoCD admin password"
}
variable "cloudflare_domain" {
type = string
default = "Base cloudflare domain, e.g. example.com"
nullable = false
}

View File

@@ -1,4 +1,4 @@
apiVersion: v2
name: maxscale-helm
version: 1.0.14
version: 1.0.15
description: Helm chart for MaxScale related Kubernetes manifests

View File

@@ -154,6 +154,13 @@ spec:
memory: 128Mi
limits:
memory: 1Gi
monitor:
interval: 2s
cooperativeMonitoring: majority_of_all
params:
auto_failover: "true"
auto_rejoin: "true"
switchover_on_low_disk_space: "true"
livenessProbe:
initialDelaySeconds: 20

View File

@@ -59,7 +59,7 @@ resource "helm_release" "mariadb-operator" {
resource "helm_release" "maxscale_helm" {
name = "maxscale-helm"
chart = "${path.module}/charts/maxscale-helm"
version = "1.0.14"
version = "1.0.15"
depends_on = [helm_release.mariadb-operator-crds, kubectl_manifest.secrets]
timeout = 3600