Files
Dasko 2fb7c2d529 test: prva grupa testova (kripto, model, RBAC, helperi, TOTP repo)
Dodati testovi pored koda:
- internal/model: CenaBezPdv/PdvIznos, Klijent.PunoIme, PreostaloZaNaplatu
- internal/middleware: ImaDozvolu matrica po ulogama + SveDozvole
- internal/handler: izvuciIP, parseOpcionuCenu, validnoImeBackupa (anti path-traversal)
- internal/db/sqlite: integracioni nad privremenom SQLite bazom + prave migracije
  (TOTP šifrovan u mirovanju, brisanje, ZasifrujPostojeceTotp + idempotentnost)

19 test funkcija, prolaze i sa -race. Dopunjava kripto_test.go iz ranije.
2026-06-12 22:07:32 +02:00

140 lines
4.1 KiB
Go

package sqlite
import (
"context"
"crypto/rand"
"database/sql"
"os"
"path/filepath"
"testing"
)
// testDB otvara privremenu SQLite bazu i primenjuje sve migracije.
// Migracije se čitaju iz korena repozitorijuma (../../.. u odnosu na ovaj paket).
func testDB(t *testing.T) *sql.DB {
t.Helper()
putanja := filepath.Join(t.TempDir(), "test.db")
db, err := OtvoriDB(putanja)
if err != nil {
t.Fatalf("OtvoriDB: %v", err)
}
if err := PokreniMigracije(db, os.DirFS("../../..")); err != nil {
t.Fatalf("migracije: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func testKljuc(t *testing.T) []byte {
t.Helper()
k := make([]byte, 32)
if _, err := rand.Read(k); err != nil {
t.Fatalf("rand: %v", err)
}
return k
}
// sirovaTotpTajna čita kolonu totp_tajna direktno (kakva stoji u bazi)
func sirovaTotpTajna(t *testing.T, db *sql.DB, id int64) string {
t.Helper()
var s sql.NullString
if err := db.QueryRow(`SELECT totp_tajna FROM korisnici WHERE id = ?`, id).Scan(&s); err != nil {
t.Fatalf("čitanje sirove tajne: %v", err)
}
return s.String
}
// TOTP tajna se u bazi čuva šifrovana, a pri čitanju vraća kao čist tekst
func TestKorisniciTotpSifrovanUMirovanju(t *testing.T) {
ctx := context.Background()
db := testDB(t)
repo := NoviKorisniciRepo(db, testKljuc(t))
k, err := repo.Kreiraj(ctx, "pera", "hash", "radnik")
if err != nil {
t.Fatalf("Kreiraj: %v", err)
}
const tajna = "JBSWY3DPEHPK3PXP"
if err := repo.SacuvajTotpTajnu(ctx, k.ID, tajna); err != nil {
t.Fatalf("SacuvajTotpTajnu: %v", err)
}
// u bazi NIJE čist tekst
if sirova := sirovaTotpTajna(t, db, k.ID); sirova == tajna || sirova == "" {
t.Fatalf("tajna u bazi nije šifrovana (sirovo = %q)", sirova)
}
// čitanje kroz repo vraća dešifrovanu (originalnu) tajnu
svezi, err := repo.DohvatiPoID(ctx, k.ID)
if err != nil {
t.Fatalf("DohvatiPoID: %v", err)
}
if svezi.TotpTajna != tajna {
t.Fatalf("dešifrovana tajna = %q, očekivano %q", svezi.TotpTajna, tajna)
}
}
// brisanje TOTP-a (prazna tajna) upisuje NULL
func TestKorisniciTotpBrisanje(t *testing.T) {
ctx := context.Background()
db := testDB(t)
repo := NoviKorisniciRepo(db, testKljuc(t))
k, _ := repo.Kreiraj(ctx, "pera", "hash", "radnik")
_ = repo.SacuvajTotpTajnu(ctx, k.ID, "JBSWY3DPEHPK3PXP")
if err := repo.SacuvajTotpTajnu(ctx, k.ID, ""); err != nil {
t.Fatalf("brisanje: %v", err)
}
if sirova := sirovaTotpTajna(t, db, k.ID); sirova != "" {
t.Fatalf("posle brisanja tajna nije prazna: %q", sirova)
}
svezi, _ := repo.DohvatiPoID(ctx, k.ID)
if svezi.TotpTajna != "" {
t.Fatalf("posle brisanja TotpTajna = %q, očekivano prazno", svezi.TotpTajna)
}
}
// ZasifrujPostojeceTotp pretvara staru nešifrovanu tajnu u šifrovanu, a stvarnu
// vrednost ostavlja čitljivom kroz repo (fallback)
func TestZasifrujPostojeceTotp(t *testing.T) {
ctx := context.Background()
db := testDB(t)
kljuc := testKljuc(t)
repo := NoviKorisniciRepo(db, kljuc)
k, _ := repo.Kreiraj(ctx, "pera", "hash", "radnik")
// simuliraj staru bazu: upiši čist tekst direktno (mimo repo-a)
const tajna = "JBSWY3DPEHPK3PXP"
if _, err := db.Exec(`UPDATE korisnici SET totp_tajna = ? WHERE id = ?`, tajna, k.ID); err != nil {
t.Fatalf("upis plain-text tajne: %v", err)
}
// pre migracije repo i dalje vraća ispravnu tajnu (fallback na plain text)
if svezi, _ := repo.DohvatiPoID(ctx, k.ID); svezi.TotpTajna != tajna {
t.Fatalf("fallback nije vratio plain-text tajnu: %q", svezi.TotpTajna)
}
br, err := ZasifrujPostojeceTotp(ctx, db, kljuc)
if err != nil {
t.Fatalf("ZasifrujPostojeceTotp: %v", err)
}
if br != 1 {
t.Fatalf("očekivano 1 šifrovan red, dobijeno %d", br)
}
// sada je u bazi šifrovano, ali kroz repo i dalje čitljivo
if sirova := sirovaTotpTajna(t, db, k.ID); sirova == tajna {
t.Fatal("posle migracije tajna je i dalje plain text")
}
if svezi, _ := repo.DohvatiPoID(ctx, k.ID); svezi.TotpTajna != tajna {
t.Fatalf("posle migracije dešifrovana tajna = %q, očekivano %q", svezi.TotpTajna, tajna)
}
// idempotentno: drugo pokretanje ne menja ništa
if br2, _ := ZasifrujPostojeceTotp(ctx, db, kljuc); br2 != 0 {
t.Fatalf("ponovno pokretanje šifrovalo %d redova, očekivano 0", br2)
}
}