licence-server/logger/service_test.go
Nenad Djukic 3480766c3e Prijemnik dijagnostike sa TERMINIA uredjaja (logger/)
- UDP prijemnik (podrazumevano 46465, NE 46464 gde uredjaji slusaju discovery):
  dnevnik rada sa zaglavljem #TERMINIA, heartbeat JSON, sve ostalo se odbacuje
- paket bez zaglavlja (FW < 0.13.4) se vodi kao "nepoznat uredjaj" po izvornoj
  IP adresi i tako je i oznacen u prikazu
- dnevnik u fajlove po uredjaju i danu, rotacija po velicini + tri kvote
  (starost, po uredjaju, ukupno); otvoren fajl se nikad ne brise
- heartbeat u memoriji; interval se meri iz razmaka javljanja, "tih" = 3x interval
- web stranica (podrazumevano 127.0.0.1:8081): spisak uredjaja, crveno za tihe,
  poslednjih N linija sa pretragom, /dnevnik.txt i /api/uredjaji
- bez baze i bez spoljnih zavisnosti; ne dira licencni servis, njegovu bazu,
  rute ni nginx
- sanitizacija svega iz paketa (duzine, kontrolni znaci, ime foldera) — bez
  izlaska iz foldera dnevnika
- systemd unit + primer env fajla + README sa uputstvom za pustanje na server
- testovi: parsiranje (zaglavlje/JSON/bez zaglavlja/smece), registar, rotacija
  i kvote, web rute

Nije pusteno na server — puštanje radi vlasnik po logger/README.md.
2026-07-25 17:40:27 +02:00

201 lines
6.4 KiB
Go

package logger
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func testService(t *testing.T, prilagodi func(*Config)) *Service {
t.Helper()
cfg := DefaultConfig()
cfg.Dir = t.TempDir()
if prilagodi != nil {
prilagodi(&cfg)
}
s, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
t.Cleanup(s.store.Close)
return s
}
func TestServiceHandleUpisujeDnevnikIPamtiHeartbeat(t *testing.T) {
s := testService(t, nil)
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
s.now = func() time.Time { return t0 }
s.Handle([]byte(`{"app":"TERMINIA","name":"TERM-001","mac":"1C:DB:D4:A6:A4:60","ip":"192.168.0.20","fw":"0.13.4","hb":1}`), "192.168.0.20")
s.Handle([]byte("#TERMINIA TERM-001 1C:DB:D4:A6:A4:60 0.13.4\n[stampac] javio se\n"), "192.168.0.20")
s.Handle([]byte{0x00, 0x01, 0x02, 0xff, 0xfe}, "9.9.9.9") // binarno smece
if got := s.stats.Heartbeat.Load(); got != 1 {
t.Errorf("heartbeata = %d", got)
}
if got := s.stats.LogPkts.Load(); got != 1 {
t.Errorf("paketa dnevnika = %d", got)
}
if got := s.stats.WriteErrs.Load(); got != 0 {
t.Errorf("gresaka upisa = %d", got)
}
lines, _ := s.store.Tail("1CDBD4A6A460", 10, "")
if len(lines) != 1 || !strings.Contains(lines[0], "[stampac] javio se") {
t.Errorf("dnevnik = %q", lines)
}
}
func TestServiceStranicaUredjaja(t *testing.T) {
s := testService(t, nil)
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
s.now = func() time.Time { return t0 }
s.Handle([]byte("#TERMINIA TERM-001 1C:DB:D4:A6:A4:60 0.13.4\n[stampac] javio se\n[kapija] prolaz\n"), "192.168.0.20")
h := s.Handler()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
if rec.Code != http.StatusOK {
t.Fatalf("/ = %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "TERM-001") || !strings.Contains(body, "1C:DB:D4:A6:A4:60") {
t.Errorf("spisak ne prikazuje uredjaj: %s", body)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/uredjaj?k=1CDBD4A6A460&q=stampac", nil))
if rec.Code != http.StatusOK {
t.Fatalf("/uredjaj = %d", rec.Code)
}
body = rec.Body.String()
if !strings.Contains(body, "[stampac] javio se") {
t.Error("pretraga ne vraca ocekivanu liniju")
}
if strings.Contains(body, "[kapija] prolaz") {
t.Error("pretraga vraca i linije koje ne odgovaraju upitu")
}
}
func TestServiceNepoznatUredjajJeOznacen(t *testing.T) {
s := testService(t, nil)
s.Handle([]byte("goli tekst bez zaglavlja\n"), "192.168.0.77")
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
body := rec.Body.String()
if !strings.Contains(body, "nepoznat uređaj") {
t.Errorf("uredjaj bez zaglavlja nije oznacen kao nepoznat:\n%s", body)
}
if !strings.Contains(body, "192.168.0.77") {
t.Error("nije prikazana izvorna adresa nepoznatog uredjaja")
}
}
func TestServiceTihUredjajJeCrven(t *testing.T) {
s := testService(t, nil)
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
s.now = func() time.Time { return t0 }
s.Handle([]byte(`{"name":"TERM-001","mac":"1C:DB:D4:A6:A4:60","hb":1}`), "192.168.0.20")
s.now = func() time.Time { return t0.Add(10 * time.Minute) } // 3x60 s je davno proslo
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
if !strings.Contains(rec.Body.String(), `class="tih"`) {
t.Error("tih uredjaj nije oznacen crvenim redom")
}
}
func TestServiceHTMLEscapeIzPaketa(t *testing.T) {
// Sadrzaj paketa je tudji tekst - ne sme da postane HTML.
s := testService(t, nil)
s.Handle([]byte("#TERMINIA <script>alert(1)</script> 1C:DB:D4:A6:A4:60 0.13.4\n<img src=x onerror=alert(2)>\n"), "1.2.3.4")
for _, url := range []string{"/", "/uredjaj?k=1CDBD4A6A460"} {
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest("GET", url, nil))
if strings.Contains(rec.Body.String(), "<script>alert(1)</script>") ||
strings.Contains(rec.Body.String(), "<img src=x") {
t.Errorf("%s: HTML iz paketa nije ekranovan", url)
}
}
}
func TestServiceToken(t *testing.T) {
s := testService(t, func(c *Config) { c.Token = "tajna" })
h := s.Handler()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
if rec.Code != http.StatusForbidden {
t.Errorf("bez tokena = %d, ocekivano 403", rec.Code)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/?t=tajna", nil))
if rec.Code != http.StatusOK {
t.Errorf("sa tokenom = %d", rec.Code)
}
}
func TestServiceAPIJson(t *testing.T) {
s := testService(t, nil)
s.Handle([]byte(`{"name":"TERM-001","mac":"1C:DB:D4:A6:A4:60","ip":"192.168.0.20","fw":"0.13.4","hb":1}`), "192.168.0.20")
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest("GET", "/api/uredjaji", nil))
if rec.Code != http.StatusOK {
t.Fatalf("/api/uredjaji = %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), `"mac": "1C:DB:D4:A6:A4:60"`) {
t.Errorf("JSON = %s", rec.Body.String())
}
}
func TestServiceRawTekst(t *testing.T) {
s := testService(t, nil)
s.Handle([]byte("#TERMINIA T 1C:DB:D4:A6:A4:60 0.13.4\nprva\ndruga\n"), "1.2.3.4")
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest("GET", "/dnevnik.txt?k=1CDBD4A6A460", nil))
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Errorf("Content-Type = %q", ct)
}
if !strings.Contains(rec.Body.String(), "prva") {
t.Errorf("telo = %q", rec.Body.String())
}
}
func TestConfigFromEnv(t *testing.T) {
t.Setenv("LOGGER_UDP_PORT", "46465")
t.Setenv("LOGGER_HTTP_PORT", "8081")
t.Setenv("LOGGER_DIR", "/var/log/terminia")
cfg, err := ConfigFromEnv()
if err != nil {
t.Fatalf("ConfigFromEnv: %v", err)
}
if cfg.UDPPort != 46465 || cfg.HTTPPort != 8081 || cfg.Dir != "/var/log/terminia" {
t.Errorf("cfg = %+v", cfg)
}
t.Setenv("LOGGER_UDP_PORT", "nije-broj")
if _, err := ConfigFromEnv(); err == nil {
t.Error("neispravan port mora dati gresku, ne tihu podrazumevanu vrednost")
}
t.Setenv("LOGGER_UDP_PORT", "46465")
t.Setenv("LOGGER_MAX_DEVICE_MB", "5000")
t.Setenv("LOGGER_MAX_TOTAL_MB", "100")
if _, err := ConfigFromEnv(); err == nil {
t.Error("kvota po uredjaju veca od ukupne mora dati gresku")
}
}
func TestDefaultConfigNijeNaPortuUredjaja(t *testing.T) {
// 46464 je port na kom UREDJAJI slusaju discovery - prijemnik tamo ne sme.
if DefaultConfig().UDPPort == 46464 {
t.Error("podrazumevani UDP port se sudara sa discovery portom uredjaja")
}
}