test: pytest (auth, RBAC, endpoints) + étape CI build->test->push
Build & Deploy / build (push) Successful in 11s
Build & Deploy / build (push) Successful in 11s
This commit is contained in:
@@ -15,11 +15,18 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build & push image
|
- name: Build image
|
||||||
|
run: docker build -t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
|
||||||
|
|
||||||
|
- name: Tests (pytest dans l'image, BDD simulée)
|
||||||
|
run: |
|
||||||
|
docker run --rm "${IMAGE}:${GITHUB_SHA::12}" \
|
||||||
|
sh -c 'pip install -q pytest httpx && pytest -q'
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.nfteam.ovh -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.nfteam.ovh -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
docker build -t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
|
|
||||||
docker push --all-tags "${IMAGE}"
|
docker push --all-tags "${IMAGE}"
|
||||||
echo "pushed ${IMAGE}"
|
echo "pushed ${IMAGE}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Fixtures de test : client FastAPI + curseur SQL simulé (aucune vraie BDD)."""
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
os.environ.setdefault("JWT_SECRET", "test-secret")
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
import main
|
||||||
|
from auth import create_access_token
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCursor:
|
||||||
|
"""Curseur pyodbc simulé : on règle description / rows / one par test."""
|
||||||
|
def __init__(self):
|
||||||
|
self.description = []
|
||||||
|
self._rows = []
|
||||||
|
self._one = None
|
||||||
|
self.executed = []
|
||||||
|
|
||||||
|
def execute(self, query, *args):
|
||||||
|
self.executed.append((query, args))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
return self._rows
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
return self._one
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cur(monkeypatch):
|
||||||
|
c = FakeCursor()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def fake_get_cursor():
|
||||||
|
yield c
|
||||||
|
|
||||||
|
monkeypatch.setattr(main, "get_cursor", fake_get_cursor)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return TestClient(main.app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_headers():
|
||||||
|
token = create_access_token({"sub": "admin", "role": "Admin", "uid": 1})
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests d'endpoints (curseur SQL simulé via la fixture `cur`)."""
|
||||||
|
from auth import hash_password, create_access_token
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_ok(client, cur):
|
||||||
|
cur._one = (1,)
|
||||||
|
r = client.get("/health")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["api"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_categories_requires_auth(client):
|
||||||
|
# sans token -> 401
|
||||||
|
assert client.get("/categories").status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_services_returns_list(client, cur, auth_headers):
|
||||||
|
cur.description = [("id_service",), ("nom_service",)]
|
||||||
|
cur._rows = [(1, "Contrat"), (2, "Fournisseur")]
|
||||||
|
r = client.get("/services", headers=auth_headers)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert {"id_service": 1, "nom_service": "Contrat"} in r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_success(client, cur):
|
||||||
|
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
|
||||||
|
r = client.post("/auth/login", data={"username": "admin", "password": "Admin2026!"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["token_type"] == "bearer"
|
||||||
|
assert body["user"]["role"] == "Admin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_wrong_password(client, cur):
|
||||||
|
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
|
||||||
|
r = client.post("/auth/login", data={"username": "admin", "password": "WRONG"})
|
||||||
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_me(client, auth_headers):
|
||||||
|
r = client.get("/auth/me", headers=auth_headers)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["username"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_monitoring_unknown_returns_404(client, cur, auth_headers):
|
||||||
|
r = client.get("/monitorings/999/details", headers=auth_headers)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_forbidden_for_consultant(client):
|
||||||
|
token = create_access_token({"sub": "bob", "role": "Consultant", "uid": 2})
|
||||||
|
r = client.get("/admin/users", headers={"Authorization": f"Bearer {token}"})
|
||||||
|
assert r.status_code == 403
|
||||||
Reference in New Issue
Block a user