Ograničenje konekcija BE emulatora, sopstveni kontekst fiskalizacije nezavisan od prekida zahteva, validacija porta na setup stranici, serverska optimizacija kratke pretrage bez eval-a, testovi
This commit is contained in:
@@ -262,6 +262,11 @@ func (k *Kartica) cmdVerifyPin(pin string) map[string]any {
|
||||
return map[string]any{"status": "ok"}
|
||||
}
|
||||
|
||||
// cmdResetAudit namerno NE zahteva pinUnesen: koristi ga isključivo admin panel
|
||||
// (BeResetAudit, već iza "podesavanja.izmeni" RBAC provere) preko sopstvene TCP
|
||||
// konekcije koja nikad ne zove verify_pin — zahtevanje PIN-a ovde bi trajno
|
||||
// pokvarilo dugme "Resetuj audit" u Podešavanjima. cmdSign ostaje PIN-zaštićen
|
||||
// jer njega poziva eksterni fiskalni klijent ("Fisk") koji prethodno zove verify_pin.
|
||||
func (k *Kartica) cmdResetAudit() map[string]any {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
@@ -274,6 +279,10 @@ func (k *Kartica) cmdSign(invoiceType, transactionType string, totalAmount float
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
if !k.pinUnesen {
|
||||
return map[string]any{"status": "error", "code": "2101", "message": "PIN nije verifikovan"}
|
||||
}
|
||||
|
||||
// blokada limite
|
||||
if k.unreadAmount >= k.Limit {
|
||||
return map[string]any{
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package be
|
||||
|
||||
import "testing"
|
||||
|
||||
func novaTestnaKartica() *Kartica {
|
||||
return &Kartica{
|
||||
PIN: "1234",
|
||||
Limit: 500000,
|
||||
counters: map[string]int{
|
||||
"pp": 0, "pr": 0, "ap": 0, "ar": 0,
|
||||
"kp": 0, "kr": 0, "op": 0, "or": 0,
|
||||
"rp": 0, "rr": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignBezPinaOdbija(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
resp := k.cmdSign("Normal", "Sale", 100)
|
||||
if resp["status"] != "error" {
|
||||
t.Fatalf("cmdSign bez verifikovanog PIN-a mora vratiti status=error, dobijeno: %v", resp)
|
||||
}
|
||||
if resp["code"] != "2101" {
|
||||
t.Errorf("code = %v, očekivano 2101", resp["code"])
|
||||
}
|
||||
if k.totalCounter != 0 {
|
||||
t.Errorf("totalCounter se povećao iako PIN nije verifikovan: %d", k.totalCounter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignPogresanPinOdbija(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
if resp := k.cmdVerifyPin("0000"); resp["status"] != "error" {
|
||||
t.Fatalf("cmdVerifyPin sa pogrešnim PIN-om mora vratiti error, dobijeno: %v", resp)
|
||||
}
|
||||
if k.pinUnesen {
|
||||
t.Fatal("pinUnesen ne sme biti true posle pogrešnog PIN-a")
|
||||
}
|
||||
if resp := k.cmdSign("Normal", "Sale", 100); resp["status"] != "error" {
|
||||
t.Fatalf("cmdSign posle pogrešnog PIN-a mora ostati blokiran, dobijeno: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignPoslePravogPinaUspeva(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
if resp := k.cmdVerifyPin("1234"); resp["status"] != "ok" {
|
||||
t.Fatalf("cmdVerifyPin sa ispravnim PIN-om mora vratiti ok, dobijeno: %v", resp)
|
||||
}
|
||||
resp := k.cmdSign("Normal", "Sale", 100)
|
||||
if resp["status"] != "ok" {
|
||||
t.Fatalf("cmdSign posle ispravnog PIN-a mora uspeti, dobijeno: %v", resp)
|
||||
}
|
||||
if k.totalCounter != 1 {
|
||||
t.Errorf("totalCounter = %d, očekivano 1", k.totalCounter)
|
||||
}
|
||||
if k.unreadAmount != 100 {
|
||||
t.Errorf("unreadAmount = %v, očekivano 100", k.unreadAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignBlokiraNaLimitu(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
k.pinUnesen = true
|
||||
k.unreadAmount = k.Limit
|
||||
resp := k.cmdSign("Normal", "Sale", 1)
|
||||
if resp["status"] != "blocked" {
|
||||
t.Fatalf("cmdSign na limitu mora vratiti status=blocked, dobijeno: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignRefundSmanjujeUnreadAmount(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
k.pinUnesen = true
|
||||
k.unreadAmount = 500
|
||||
k.cmdSign("Normal", "Refund", 200)
|
||||
if k.unreadAmount != 300 {
|
||||
t.Errorf("unreadAmount posle refunda = %v, očekivano 300", k.unreadAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdSignRefundNeIdeUNegativu(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
k.pinUnesen = true
|
||||
k.unreadAmount = 50
|
||||
k.cmdSign("Normal", "Refund", 200)
|
||||
if k.unreadAmount != 0 {
|
||||
t.Errorf("unreadAmount ne sme biti negativan, dobijeno %v", k.unreadAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdResetAuditRadiBezPina(t *testing.T) {
|
||||
// namerno: admin panel (BeResetAudit) nikad ne zove verify_pin, v. komentar
|
||||
// uz cmdResetAudit u kartica.go — reset ne sme zahtevati PIN.
|
||||
k := novaTestnaKartica()
|
||||
k.unreadAmount = 12345
|
||||
resp := k.cmdResetAudit()
|
||||
if resp["status"] != "ok" {
|
||||
t.Fatalf("cmdResetAudit mora uspeti i bez PIN-a, dobijeno: %v", resp)
|
||||
}
|
||||
if k.unreadAmount != 0 {
|
||||
t.Errorf("unreadAmount posle reset_audit = %v, očekivano 0", k.unreadAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdVerifyPinKonstantnoVreme(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
if resp := k.cmdVerifyPin("1234"); resp["status"] != "ok" {
|
||||
t.Fatalf("očekivan uspeh sa ispravnim PIN-om, dobijeno: %v", resp)
|
||||
}
|
||||
if !k.pinUnesen {
|
||||
t.Fatal("pinUnesen mora biti true posle ispravnog PIN-a")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStatusPrijavljujePinRequired(t *testing.T) {
|
||||
k := novaTestnaKartica()
|
||||
resp := k.cmdStatus()
|
||||
if resp["pin_required"] != true {
|
||||
t.Fatalf("pin_required mora biti true pre verifikacije, dobijeno: %v", resp)
|
||||
}
|
||||
k.cmdVerifyPin("1234")
|
||||
resp = k.cmdStatus()
|
||||
if resp["pin_required"] != false {
|
||||
t.Fatalf("pin_required mora biti false posle verifikacije, dobijeno: %v", resp)
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -8,8 +8,19 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxKonekcija ograničava broj istovremenih TCP konekcija — bez ovoga bi klijent
|
||||
// koji samo otvori konekciju i ništa ne pošalje mogao da drži neograničen broj
|
||||
// gorutina/file descriptor-a (v. BUG.md #36).
|
||||
const maxKonekcija = 32
|
||||
|
||||
// citanjeTimeout je rok po redu (komandi) koji se osvežava pre svakog čitanja —
|
||||
// klijent koji ćuti duže od ovoga se prekida umesto da drži konekciju zauvek.
|
||||
// var (ne const) da bi testovi mogli da ga privremeno skrate.
|
||||
var citanjeTimeout = 30 * time.Second
|
||||
|
||||
// JeUkljucen vraća true ako emulator treba da se pokrene.
|
||||
// Prioritet: env var BE_ENABLED > DB podesavanje be_enabled > podrazumevano true.
|
||||
func JeUkljucen(db *sql.DB) bool {
|
||||
@@ -35,6 +46,7 @@ func Pokreni(db *sql.DB) {
|
||||
slog.Info("kartica emulator sluša", "addr", addr)
|
||||
|
||||
k := novaKartica(db)
|
||||
sem := make(chan struct{}, maxKonekcija)
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
@@ -42,7 +54,16 @@ func Pokreni(db *sql.DB) {
|
||||
slog.Warn("be: greška pri Accept()", "error", err)
|
||||
continue
|
||||
}
|
||||
go obradiKonekciju(conn, k)
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
go func() {
|
||||
defer func() { <-sem }()
|
||||
obradiKonekciju(conn, k)
|
||||
}()
|
||||
default:
|
||||
slog.Warn("be: dostignut limit konekcija, odbijam klijenta", "limit", maxKonekcija)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +71,14 @@ func obradiKonekciju(conn net.Conn, k *Kartica) {
|
||||
defer conn.Close()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
for {
|
||||
// SetDeadline (ne samo SetReadDeadline) da rok pokrije i odgovori() Write
|
||||
// koji sledi posle čitanja — klijent koji šalje komande a nikad ne čita
|
||||
// odgovore bi inače mogao da blokira Write zauvek i iscrpi semafor konekcija.
|
||||
conn.SetDeadline(time.Now().Add(citanjeTimeout))
|
||||
if !scanner.Scan() {
|
||||
return
|
||||
}
|
||||
line := scanner.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package be
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestObradiKonekcijaZatvaraNakonTisine proverava ispravku BUG.md #36 — konekcija
|
||||
// koja ništa ne pošalje mora biti zatvorena posle citanjeTimeout-a, ne držana
|
||||
// zauvek.
|
||||
func TestObradiKonekcijaZatvaraNakonTisine(t *testing.T) {
|
||||
stariTimeout := citanjeTimeout
|
||||
citanjeTimeout = 100 * time.Millisecond
|
||||
defer func() { citanjeTimeout = stariTimeout }()
|
||||
|
||||
klijentska, serverska := net.Pipe()
|
||||
defer klijentska.Close()
|
||||
|
||||
gotovo := make(chan struct{})
|
||||
go func() {
|
||||
obradiKonekciju(serverska, novaTestnaKartica())
|
||||
close(gotovo)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gotovo:
|
||||
// očekivano: server je zatvorio konekciju posle isteka citanjeTimeout-a
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("obradiKonekciju nije prekinuta ni posle 2s tišine klijenta — deadline ne radi")
|
||||
}
|
||||
}
|
||||
|
||||
// TestObradiKonekcijaOdgovaraNaKomandu proverava da server i dalje ispravno
|
||||
// odgovara na komandu pre nego što istekne deadline (da fix #36 ne pokvari
|
||||
// normalan tok status → verify_pin → sign preko realnog TCP-a).
|
||||
func TestObradiKonekcijaOdgovaraNaKomandu(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("ne mogu da otvorim listener: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
k := novaTestnaKartica()
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
obradiKonekciju(conn, k)
|
||||
}()
|
||||
|
||||
conn, err := net.DialTimeout("tcp", ln.Addr().String(), 2*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("ne mogu da se povežem: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write([]byte(`{"command":"status"}` + "\n")); err != nil {
|
||||
t.Fatalf("greška pri slanju komande: %v", err)
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
line, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("greška pri čitanju odgovora: %v", err)
|
||||
}
|
||||
|
||||
var resp map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &resp); err != nil {
|
||||
t.Fatalf("odgovor nije validan JSON: %v (%q)", err, line)
|
||||
}
|
||||
if resp["status"] != "ok" {
|
||||
t.Fatalf("status = %v, očekivano ok", resp["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPokreniOdbijaKadJeLimitDostignut(t *testing.T) {
|
||||
// Konceptualna provera semafora u Pokreni je pokrivena kroz kod (kanal
|
||||
// kapaciteta maxKonekcija); ovde samo proveravamo da konstanta ima razuman,
|
||||
// pozitivan limit umesto neograničenog broja gorutina.
|
||||
if maxKonekcija <= 0 {
|
||||
t.Fatalf("maxKonekcija mora biti pozitivan broj, dobijeno: %d", maxKonekcija)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user