licence-server/logger/packet_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

214 lines
6.4 KiB
Go

package logger
import (
"strings"
"testing"
)
func TestParseLogSaZaglavljem(t *testing.T) {
raw := "#TERMINIA TERM-001 1C:DB:D4:A6:A4:60 0.13.4\n" +
"[stampac] opet se javlja na 192.168.123.100 (ping)\n" +
"[izvestaj] dan 20659: 14 karata, 2100 RSD\n"
p := ParsePacket([]byte(raw), "192.168.0.20")
if p.Kind != KindLog {
t.Fatalf("vrsta = %v, ocekivano KindLog", p.Kind)
}
if !p.Identified {
t.Error("paket sa zaglavljem mora biti identifikovan")
}
if p.Name != "TERM-001" || p.MAC != "1C:DB:D4:A6:A4:60" || p.FW != "0.13.4" {
t.Errorf("identitet = %q / %q / %q", p.Name, p.MAC, p.FW)
}
if len(p.Lines) != 2 {
t.Fatalf("linija = %d, ocekivano 2: %q", len(p.Lines), p.Lines)
}
if !strings.HasPrefix(p.Lines[0], "[stampac]") {
t.Errorf("prva linija = %q", p.Lines[0])
}
if got := DeviceKey(p.MAC, p.SourceIP); got != "1CDBD4A6A460" {
t.Errorf("kljuc = %q", got)
}
}
func TestParseZaglavljeSaRazmakomUNazivu(t *testing.T) {
// Naziv uredjaja kuca korisnik i sme da ima razmake.
p := ParsePacket([]byte("#TERMINIA Bazen Ulaz A 1c-db-d4-a6-a4-60 0.13.4\nzdravo\n"), "10.0.0.5")
if p.Name != "Bazen Ulaz A" {
t.Errorf("naziv = %q", p.Name)
}
if p.MAC != "1C:DB:D4:A6:A4:60" {
t.Errorf("mac = %q", p.MAC)
}
if p.FW != "0.13.4" {
t.Errorf("fw = %q", p.FW)
}
}
func TestParseHeartbeat(t *testing.T) {
raw := `{"app":"TERMINIA","name":"TERM-001","mac":"1C:DB:D4:A6:A4:60",` +
`"ip":"192.168.0.20","fw":"0.13.4","serial":"1CDBD4A6A460","hb":1}`
p := ParsePacket([]byte(raw), "192.168.0.20")
if p.Kind != KindHeartbeat {
t.Fatalf("vrsta = %v, ocekivano KindHeartbeat", p.Kind)
}
if !p.Identified || p.MAC != "1C:DB:D4:A6:A4:60" || p.IP != "192.168.0.20" ||
p.FW != "0.13.4" || p.Serial != "1CDBD4A6A460" || p.Name != "TERM-001" {
t.Errorf("identitet = %+v", p)
}
if len(p.Lines) != 0 {
t.Errorf("heartbeat ne sme imati linije: %q", p.Lines)
}
}
func TestParseHeartbeatSaIdPoljem(t *testing.T) {
// Stariji/drugaciji klijent salje serijski pod imenom "id".
p := ParsePacket([]byte(`{"name":"T","mac":"AABBCCDDEEFF","id":"XYZ","hb":1}`), "10.0.0.1")
if p.Kind != KindHeartbeat || p.Serial != "XYZ" {
t.Errorf("rezultat = %+v", p)
}
}
func TestParseBezZaglavlja(t *testing.T) {
// FW stariji od 0.13.4: goli tekst, znamo samo izvornu adresu.
p := ParsePacket([]byte("nesto se desilo\ndrugi red\n"), "192.168.0.77")
if p.Kind != KindLog {
t.Fatalf("vrsta = %v", p.Kind)
}
if p.Identified {
t.Error("paket bez zaglavlja NE SME da vazi za identifikovan")
}
if p.MAC != "" {
t.Errorf("mac = %q, ocekivano prazno", p.MAC)
}
key := DeviceKey(p.MAC, p.SourceIP)
if key != "nepoznat-192_168_0_77" {
t.Errorf("kljuc = %q", key)
}
}
func TestParseSmeceNeObaraServis(t *testing.T) {
slucajevi := [][]byte{
nil,
{},
[]byte("\n\n\n"),
[]byte("{"), // krnj JSON
[]byte(`{"app":"TERMINIA"`), // presecen paket
[]byte(`{"nesto":"tudje"}`), // JSON bez identiteta
{0x00, 0x01, 0x02, 0xff, 0xfe}, // binarno smece
[]byte("#TERMINIA\n"), // zaglavlje bez ijednog polja
[]byte(strings.Repeat("A", MaxPacketBytes*3)), // predugacak paket
}
for i, c := range slucajevi {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("slucaj %d: panika: %v", i, r)
}
}()
p := ParsePacket(c, "1.2.3.4")
// Bilo koji ishod je prihvatljiv osim pada; ako je nesto proslo,
// mora biti u granicama.
if len(p.Lines) > MaxLinesPerPacket {
t.Errorf("slucaj %d: previse linija %d", i, len(p.Lines))
}
for _, ln := range p.Lines {
if len([]rune(ln)) > MaxLineLen {
t.Errorf("slucaj %d: linija duza od granice", i)
}
}
}()
}
}
func TestParseSeceIKontrolneZnake(t *testing.T) {
dugacko := strings.Repeat("x", 1000)
raw := "#TERMINIA " + strings.Repeat("N", 200) + " 1C:DB:D4:A6:A4:60 " + strings.Repeat("9", 100) + "\n" +
"linija\x1b[31m sa ESC \x07 i \x00 nulom\n" + dugacko + "\n"
p := ParsePacket([]byte(raw), "1.2.3.4")
if len([]rune(p.Name)) > MaxNameLen {
t.Errorf("naziv nije skracen: %d", len([]rune(p.Name)))
}
if len([]rune(p.FW)) > MaxFWLen {
t.Errorf("fw nije skracen: %d", len([]rune(p.FW)))
}
if strings.ContainsAny(p.Lines[0], "\x1b\x07\x00") {
t.Errorf("kontrolni znaci nisu uklonjeni: %q", p.Lines[0])
}
if len([]rune(p.Lines[1])) > MaxLineLen {
t.Errorf("linija nije skracena: %d", len([]rune(p.Lines[1])))
}
}
func TestParsePreviseLinija(t *testing.T) {
var b strings.Builder
b.WriteString("#TERMINIA T 1C:DB:D4:A6:A4:60 0.13.4\n")
for i := 0; i < 500; i++ {
b.WriteString("linija\n")
}
p := ParsePacket([]byte(b.String()), "1.2.3.4")
if len(p.Lines) != MaxLinesPerPacket {
t.Errorf("linija = %d, ocekivano %d", len(p.Lines), MaxLinesPerPacket)
}
}
func TestNormalizeMAC(t *testing.T) {
dobri := map[string]string{
"1C:DB:D4:A6:A4:60": "1C:DB:D4:A6:A4:60",
"1c:db:d4:a6:a4:60": "1C:DB:D4:A6:A4:60",
"1cdbd4a6a460": "1C:DB:D4:A6:A4:60",
"1C-DB-D4-A6-A4-60": "1C:DB:D4:A6:A4:60",
}
for in, want := range dobri {
got, ok := NormalizeMAC(in)
if !ok || got != want {
t.Errorf("NormalizeMAC(%q) = %q,%v", in, got, ok)
}
}
losi := []string{"", "1C:DB:D4:A6:A4", "1C:DB:D4:A6:A4:6G", "../../etc/passwd",
strings.Repeat("A", 200), "1C:DB:D4:A6:A4:60:70"}
for _, in := range losi {
if _, ok := NormalizeMAC(in); ok {
t.Errorf("NormalizeMAC(%q) je prosao, a ne bi smeo", in)
}
}
}
func TestSanitizeSegmentNemaIzlaskaIzFoldera(t *testing.T) {
slucajevi := map[string]string{
"../../etc/passwd": "etc_passwd",
"/apsolutno/putanja": "apsolutno_putanja",
"C:\\Windows\\win": "C__Windows_win",
"": "nepoznat",
"...": "nepoznat",
"1CDBD4A6A460": "1CDBD4A6A460",
}
for in, want := range slucajevi {
got := SanitizeSegment(in)
if got != want {
t.Errorf("SanitizeSegment(%q) = %q, ocekivano %q", in, got, want)
}
if strings.ContainsAny(got, `/\`) || got == ".." {
t.Errorf("SanitizeSegment(%q) = %q — pusta izlazak iz foldera", in, got)
}
}
if n := len(SanitizeSegment(strings.Repeat("a", 500))); n > MaxKeyLen {
t.Errorf("kljuc nije ogranicen: %d", n)
}
}
func TestDeviceKeyPoIPKadNemaMAC(t *testing.T) {
if got := DeviceKey("", "10.20.30.40"); got != "nepoznat-10_20_30_40" {
t.Errorf("kljuc = %q", got)
}
if got := DeviceKey("", ""); got != "nepoznat-bez-adrese" {
t.Errorf("kljuc bez adrese = %q", got)
}
// Lazna izvorna adresa ne sme da napravi putanju.
if got := DeviceKey("", "../../root"); strings.ContainsAny(got, `/\`) {
t.Errorf("kljuc = %q", got)
}
}