566 lines
18 KiB
Go
566 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"git.g3e.fr/H6N/account/internal/server"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const testDBURL = "postgres://account:account@localhost:5432/account?sslmode=disable"
|
|
|
|
// ts wraps a live httptest.Server + helpers for the integration suite.
|
|
type ts struct {
|
|
*testing.T
|
|
srv *httptest.Server
|
|
}
|
|
|
|
func (s *ts) do(method, path string, body any) *http.Response {
|
|
s.Helper()
|
|
var r io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
s.Fatalf("marshal: %v", err)
|
|
}
|
|
r = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequest(method, s.srv.URL+path, r)
|
|
if err != nil {
|
|
s.Fatalf("new request: %v", err)
|
|
}
|
|
req.Header.Set("X-Owner-ID", "1")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
s.Fatalf("%s %s: %v", method, path, err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func (s *ts) decode(resp *http.Response, out any) {
|
|
s.Helper()
|
|
defer resp.Body.Close()
|
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
s.Fatalf("decode: %v", err)
|
|
}
|
|
}
|
|
|
|
func (s *ts) mustStatus(resp *http.Response, want int) {
|
|
s.Helper()
|
|
if resp.StatusCode != want {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
s.Fatalf("expected HTTP %d, got %d: %s", want, resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
func setupSuite(t *testing.T) *ts {
|
|
t.Helper()
|
|
dbURL := os.Getenv("TEST_DATABASE_URL")
|
|
if dbURL == "" {
|
|
dbURL = testDBURL
|
|
}
|
|
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, dbURL)
|
|
if err != nil {
|
|
t.Skipf("cannot connect to test DB: %v", err)
|
|
}
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
t.Skipf("test DB unreachable: %v", err)
|
|
}
|
|
|
|
// Nettoyer les données avant chaque run (ordre FK)
|
|
_, err = pool.Exec(ctx, `
|
|
TRUNCATE TABLE transaction, recurring_rule, account_snapshot,
|
|
position_snapshot, price_history, instrument_ticker_cache,
|
|
account, instrument RESTART IDENTITY CASCADE
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("truncate: %v", err)
|
|
}
|
|
// Réinsérer EUR
|
|
_, err = pool.Exec(ctx, `INSERT INTO instrument (type, code, name, devise_cotation) VALUES ('devise','EUR','Euro','EUR')`)
|
|
if err != nil {
|
|
t.Fatalf("seed EUR: %v", err)
|
|
}
|
|
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
srv := server.New(nil, pool, logger)
|
|
hs := httptest.NewServer(srv)
|
|
|
|
t.Cleanup(func() {
|
|
hs.Close()
|
|
pool.Close()
|
|
})
|
|
|
|
return &ts{T: t, srv: hs}
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
func TestAPI(t *testing.T) {
|
|
s := setupSuite(t)
|
|
|
|
// IDs partagés entre sous-tests
|
|
var (
|
|
eurID, etfID int32
|
|
accID, peaID int32
|
|
envID int32
|
|
tx1, tx2 int64
|
|
)
|
|
|
|
// ── Instruments ──────────────────────────────────────────────────────────
|
|
|
|
t.Run("GET /instruments retourne EUR", func(t *testing.T) {
|
|
resp := s.do("GET", "/instruments", nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 1 || list[0]["code"] != "EUR" {
|
|
t.Fatalf("expected [EUR], got %v", list)
|
|
}
|
|
eurID = int32(list[0]["id"].(float64))
|
|
})
|
|
|
|
t.Run("POST /instruments crée un ETF", func(t *testing.T) {
|
|
resp := s.do("POST", "/instruments", map[string]any{
|
|
"type": "etf", "code": "LU1681043599",
|
|
"name": "Amundi MSCI World", "devise_cotation": "EUR",
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var inst map[string]any
|
|
s.decode(resp, &inst)
|
|
if inst["code"] != "LU1681043599" {
|
|
t.Fatalf("unexpected instrument: %v", inst)
|
|
}
|
|
etfID = int32(inst["id"].(float64))
|
|
})
|
|
|
|
t.Run("GET /instruments/{id}", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/instruments/%d", etfID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var inst map[string]any
|
|
s.decode(resp, &inst)
|
|
if inst["name"] != "Amundi MSCI World" {
|
|
t.Fatalf("unexpected name: %v", inst["name"])
|
|
}
|
|
})
|
|
|
|
t.Run("PUT /instruments/{id}", func(t *testing.T) {
|
|
resp := s.do("PUT", fmt.Sprintf("/instruments/%d", etfID), map[string]any{
|
|
"type": "etf", "code": "LU1681043599",
|
|
"name": "Amundi MSCI World ETF", "devise_cotation": "EUR",
|
|
})
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var inst map[string]any
|
|
s.decode(resp, &inst)
|
|
if inst["name"] != "Amundi MSCI World ETF" {
|
|
t.Fatalf("PUT did not update name: %v", inst)
|
|
}
|
|
})
|
|
|
|
t.Run("GET /instruments/{id} 404", func(t *testing.T) {
|
|
resp := s.do("GET", "/instruments/9999", nil)
|
|
s.mustStatus(resp, http.StatusNotFound)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
|
|
t.Run("POST /accounts crée Livret A", func(t *testing.T) {
|
|
resp := s.do("POST", "/accounts", map[string]any{
|
|
"nom": "Livret A", "type": "livret", "plafond": 22950,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var acc map[string]any
|
|
s.decode(resp, &acc)
|
|
if acc["nom"] != "Livret A" {
|
|
t.Fatalf("unexpected account: %v", acc)
|
|
}
|
|
if acc["devise_reference"] != "EUR" {
|
|
t.Fatalf("devise_reference should default to EUR, got %v", acc["devise_reference"])
|
|
}
|
|
accID = int32(acc["id"].(float64))
|
|
})
|
|
|
|
t.Run("POST /accounts crée PEA", func(t *testing.T) {
|
|
resp := s.do("POST", "/accounts", map[string]any{
|
|
"nom": "PEA", "type": "pea",
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var acc map[string]any
|
|
s.decode(resp, &acc)
|
|
peaID = int32(acc["id"].(float64))
|
|
})
|
|
|
|
t.Run("GET /accounts liste les comptes maîtres", func(t *testing.T) {
|
|
resp := s.do("GET", "/accounts", nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected 2 accounts, got %d", len(list))
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id} inclut les enveloppes vides", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var acc map[string]any
|
|
s.decode(resp, &acc)
|
|
if acc["envelopes"] == nil {
|
|
t.Fatal("envelopes field missing")
|
|
}
|
|
})
|
|
|
|
t.Run("PUT /accounts/{id} préserve devise_reference", func(t *testing.T) {
|
|
resp := s.do("PUT", fmt.Sprintf("/accounts/%d", accID), map[string]any{
|
|
"nom": "Livret A CIC", "type": "livret", "plafond": 22950,
|
|
})
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var acc map[string]any
|
|
s.decode(resp, &acc)
|
|
if acc["devise_reference"] != "EUR" {
|
|
t.Fatalf("devise_reference lost after PUT: %v", acc["devise_reference"])
|
|
}
|
|
if acc["nom"] != "Livret A CIC" {
|
|
t.Fatalf("nom not updated: %v", acc["nom"])
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id} 404", func(t *testing.T) {
|
|
resp := s.do("GET", "/accounts/9999", nil)
|
|
s.mustStatus(resp, http.StatusNotFound)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
// ── Envelopes ─────────────────────────────────────────────────────────────
|
|
|
|
t.Run("POST /accounts/{id}/envelopes crée une enveloppe", func(t *testing.T) {
|
|
resp := s.do("POST", fmt.Sprintf("/accounts/%d/envelopes", accID), map[string]any{
|
|
"nom": "Vacances", "objectif": "Road trip USA",
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var env map[string]any
|
|
s.decode(resp, &env)
|
|
if env["type"] != "livret" {
|
|
t.Fatalf("envelope should inherit type from master, got %v", env["type"])
|
|
}
|
|
if env["devise_reference"] != "EUR" {
|
|
t.Fatalf("envelope should inherit devise, got %v", env["devise_reference"])
|
|
}
|
|
if int32(env["master_account_id"].(float64)) != accID {
|
|
t.Fatalf("master_account_id mismatch")
|
|
}
|
|
envID = int32(env["id"].(float64))
|
|
})
|
|
|
|
t.Run("GET /accounts/{id}/envelopes", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d/envelopes", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected 1 envelope, got %d", len(list))
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id} inclut l'enveloppe", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var acc map[string]any
|
|
s.decode(resp, &acc)
|
|
envs := acc["envelopes"].([]any)
|
|
if len(envs) != 1 {
|
|
t.Fatalf("expected 1 envelope in account, got %d", len(envs))
|
|
}
|
|
})
|
|
|
|
t.Run("PUT /envelopes/{id}", func(t *testing.T) {
|
|
resp := s.do("PUT", fmt.Sprintf("/envelopes/%d", envID), map[string]any{
|
|
"nom": "Vacances 2027", "objectif": "Japon",
|
|
})
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var env map[string]any
|
|
s.decode(resp, &env)
|
|
if env["objectif"] != "Japon" {
|
|
t.Fatalf("objectif not updated: %v", env["objectif"])
|
|
}
|
|
})
|
|
|
|
t.Run("POST chaînage d'enveloppe interdit", func(t *testing.T) {
|
|
resp := s.do("POST", fmt.Sprintf("/accounts/%d/envelopes", envID), map[string]any{
|
|
"nom": "sous-enveloppe",
|
|
})
|
|
s.mustStatus(resp, http.StatusBadRequest)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
// ── Transactions ──────────────────────────────────────────────────────────
|
|
|
|
t.Run("POST flux entrant (salaire)", func(t *testing.T) {
|
|
resp := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-06-01", "label": "Salaire juin",
|
|
"tiers": "Employeur", "categorie": "salaire",
|
|
"account_dest_id": accID, "instrument_dest_id": eurID,
|
|
"quantite_dest": 2800, "validated": true,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var tx map[string]any
|
|
s.decode(resp, &tx)
|
|
tx1 = int64(tx["id"].(float64))
|
|
})
|
|
|
|
t.Run("POST flux sortant (dépense)", func(t *testing.T) {
|
|
resp := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-06-05", "label": "Courses",
|
|
"categorie": "alimentation",
|
|
"account_source_id": accID, "instrument_source_id": eurID,
|
|
"quantite_source": 85.50, "validated": true,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var tx map[string]any
|
|
s.decode(resp, &tx)
|
|
tx2 = int64(tx["id"].(float64))
|
|
})
|
|
|
|
t.Run("POST virement (Livret A → PEA)", func(t *testing.T) {
|
|
resp := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-06-10", "label": "Versement PEA",
|
|
"account_source_id": accID, "instrument_source_id": eurID, "quantite_source": 500,
|
|
"account_dest_id": peaID, "instrument_dest_id": etfID, "quantite_dest": 1.35,
|
|
"validated": true,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
t.Run("POST transaction prévisionnelle", func(t *testing.T) {
|
|
resp := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-05-15", "label": "Loyer mai",
|
|
"categorie": "logement",
|
|
"account_source_id": accID, "instrument_source_id": eurID,
|
|
"quantite_source": 950, "validated": false,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
t.Run("GET /accounts/{id}/transactions vue signée", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d/transactions", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 4 {
|
|
t.Fatalf("expected 4 transactions, got %d", len(list))
|
|
}
|
|
// Premier résultat = date la plus récente (virement)
|
|
if list[0]["montant"].(float64) != -500 {
|
|
t.Fatalf("virement should be -500, got %v", list[0]["montant"])
|
|
}
|
|
if list[0]["contrepartie_account_id"] == nil {
|
|
t.Fatalf("contrepartie_account_id should be set for virement")
|
|
}
|
|
// Salaire doit être positif
|
|
var salaire map[string]any
|
|
for _, tx := range list {
|
|
if tx["label"] == "Salaire juin" {
|
|
salaire = tx
|
|
break
|
|
}
|
|
}
|
|
if salaire == nil || salaire["montant"].(float64) != 2800 {
|
|
t.Fatalf("salaire montant should be +2800, got %v", salaire)
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id}/transactions?pending=true", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d/transactions?pending=true", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 1 || list[0]["label"] != "Loyer mai" {
|
|
t.Fatalf("expected 1 pending transaction (Loyer mai), got %v", list)
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id}/transactions?validated=true", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d/transactions?validated=true", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 3 {
|
|
t.Fatalf("expected 3 validated transactions, got %d", len(list))
|
|
}
|
|
})
|
|
|
|
t.Run("GET /accounts/{id}/transactions?from&to", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/accounts/%d/transactions?from=2026-06-05&to=2026-06-10", accID), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var list []map[string]any
|
|
s.decode(resp, &list)
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected 2 transactions in range, got %d", len(list))
|
|
}
|
|
})
|
|
|
|
t.Run("GET /transactions/{id} vue brute", func(t *testing.T) {
|
|
resp := s.do("GET", fmt.Sprintf("/transactions/%d", tx1), nil)
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var tx map[string]any
|
|
s.decode(resp, &tx)
|
|
if tx["quantite_dest"].(float64) != 2800 {
|
|
t.Fatalf("unexpected quantite_dest: %v", tx["quantite_dest"])
|
|
}
|
|
})
|
|
|
|
t.Run("PUT /transactions/{id}", func(t *testing.T) {
|
|
resp := s.do("PUT", fmt.Sprintf("/transactions/%d", tx2), map[string]any{
|
|
"date": "2026-06-05", "label": "Courses Monoprix",
|
|
"categorie": "alimentation",
|
|
"account_source_id": accID, "instrument_source_id": eurID,
|
|
"quantite_source": 92.30, "validated": true,
|
|
})
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var tx map[string]any
|
|
s.decode(resp, &tx)
|
|
if tx["quantite_source"].(float64) != 92.30 {
|
|
t.Fatalf("PUT did not update quantite_source: %v", tx["quantite_source"])
|
|
}
|
|
})
|
|
|
|
t.Run("PATCH /transactions/{id}/validate — valider", func(t *testing.T) {
|
|
// Créer une transaction non validée à valider
|
|
resp := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-06-12", "label": "À valider",
|
|
"account_dest_id": accID, "instrument_dest_id": eurID,
|
|
"quantite_dest": 100, "validated": false,
|
|
})
|
|
s.mustStatus(resp, http.StatusCreated)
|
|
var created map[string]any
|
|
s.decode(resp, &created)
|
|
id := int64(created["id"].(float64))
|
|
|
|
resp2 := s.do("PATCH", fmt.Sprintf("/transactions/%d/validate", id), map[string]any{"validated": true})
|
|
s.mustStatus(resp2, http.StatusOK)
|
|
var tx map[string]any
|
|
s.decode(resp2, &tx)
|
|
if tx["validated"] != true {
|
|
t.Fatalf("expected validated=true, got %v", tx["validated"])
|
|
}
|
|
})
|
|
|
|
t.Run("PATCH /transactions/{id}/validate — dévalider", func(t *testing.T) {
|
|
resp := s.do("PATCH", fmt.Sprintf("/transactions/%d/validate", tx1), map[string]any{"validated": false})
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var tx map[string]any
|
|
s.decode(resp, &tx)
|
|
if tx["validated"] != false {
|
|
t.Fatalf("expected validated=false, got %v", tx["validated"])
|
|
}
|
|
})
|
|
|
|
t.Run("DELETE /transactions/{id}", func(t *testing.T) {
|
|
resp := s.do("DELETE", fmt.Sprintf("/transactions/%d", tx2), nil)
|
|
s.mustStatus(resp, http.StatusNoContent)
|
|
resp.Body.Close()
|
|
|
|
resp2 := s.do("GET", fmt.Sprintf("/transactions/%d", tx2), nil)
|
|
s.mustStatus(resp2, http.StatusNotFound)
|
|
resp2.Body.Close()
|
|
})
|
|
|
|
t.Run("DELETE /accounts/{id} supprime les enveloppes en cascade", func(t *testing.T) {
|
|
// Créer un compte temporaire avec une enveloppe
|
|
r1 := s.do("POST", "/accounts", map[string]any{"nom": "Temp", "type": "courant"})
|
|
s.mustStatus(r1, http.StatusCreated)
|
|
var acc map[string]any
|
|
s.decode(r1, &acc)
|
|
tmpID := int32(acc["id"].(float64))
|
|
|
|
r2 := s.do("POST", fmt.Sprintf("/accounts/%d/envelopes", tmpID), map[string]any{"nom": "Env Temp"})
|
|
s.mustStatus(r2, http.StatusCreated)
|
|
r2.Body.Close()
|
|
|
|
r3 := s.do("DELETE", fmt.Sprintf("/accounts/%d", tmpID), nil)
|
|
s.mustStatus(r3, http.StatusNoContent)
|
|
r3.Body.Close()
|
|
|
|
r4 := s.do("GET", fmt.Sprintf("/accounts/%d", tmpID), nil)
|
|
s.mustStatus(r4, http.StatusNotFound)
|
|
r4.Body.Close()
|
|
})
|
|
|
|
// ── Snapshots ─────────────────────────────────────────────────────────────
|
|
|
|
t.Run("POST transaction invalide le snapshot du compte", func(t *testing.T) {
|
|
// La transaction salaire (tx1) a déjà été créée pour accID.
|
|
// On vérifie qu'une invalidation existe pour ce compte.
|
|
resp := s.do("POST", fmt.Sprintf("/accounts/%d/snapshots/recompute", accID), nil)
|
|
// tx1 a une date passée → invalidation présente → 200 avec from/to
|
|
s.mustStatus(resp, http.StatusOK)
|
|
var result map[string]any
|
|
s.decode(resp, &result)
|
|
if result["status"] != "ok" {
|
|
t.Fatalf("expected status ok, got %v", result)
|
|
}
|
|
if result["account_id"].(float64) != float64(accID) {
|
|
t.Fatalf("unexpected account_id: %v", result["account_id"])
|
|
}
|
|
})
|
|
|
|
t.Run("POST /accounts/{id}/snapshots/recompute — aucune invalidation → 204", func(t *testing.T) {
|
|
// Le recompute précédent a effacé l'invalidation.
|
|
resp := s.do("POST", fmt.Sprintf("/accounts/%d/snapshots/recompute", accID), nil)
|
|
s.mustStatus(resp, http.StatusNoContent)
|
|
resp.Body.Close()
|
|
})
|
|
|
|
t.Run("Modifier une transaction re-invalide le compte", func(t *testing.T) {
|
|
// Créer une nouvelle transaction pour déclencher une invalidation.
|
|
r1 := s.do("POST", "/transactions", map[string]any{
|
|
"date": "2026-05-01", "label": "Test invalidation",
|
|
"account_dest_id": accID, "instrument_dest_id": eurID,
|
|
"quantite_dest": 50, "validated": true,
|
|
})
|
|
s.mustStatus(r1, http.StatusCreated)
|
|
r1.Body.Close()
|
|
|
|
// L'invalidation est présente → recompute retourne 200.
|
|
r2 := s.do("POST", fmt.Sprintf("/accounts/%d/snapshots/recompute", accID), nil)
|
|
s.mustStatus(r2, http.StatusOK)
|
|
var result map[string]any
|
|
s.decode(r2, &result)
|
|
if result["status"] != "ok" {
|
|
t.Fatalf("expected status ok, got %v", result)
|
|
}
|
|
})
|
|
|
|
t.Run("POST /accounts/{id}/snapshots/recompute — compte inexistant → 404", func(t *testing.T) {
|
|
resp := s.do("POST", "/accounts/9999/snapshots/recompute", nil)
|
|
// Compte inexistant : GetInvalidation retourne pgx.ErrNoRows → 404
|
|
// (en pratique le compte n'existe pas, l'invalidation non plus → 204)
|
|
// On accepte 204 ou 404
|
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 204 or 404 for unknown account, got %d", resp.StatusCode)
|
|
}
|
|
resp.Body.Close()
|
|
})
|
|
|
|
// Utiliser les variables pour éviter "declared but not used"
|
|
_ = eurID
|
|
}
|