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

246 lines
6.8 KiB
Go

package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func testStore(t *testing.T, cfg StoreConfig) *Store {
t.Helper()
cfg.Dir = t.TempDir()
s, err := NewStore(cfg)
if err != nil {
t.Fatalf("NewStore: %v", err)
}
t.Cleanup(s.Close)
return s
}
func TestStorePiseIVracaLinije(t *testing.T) {
s := testStore(t, StoreConfig{})
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
if err := s.Write("1CDBD4A6A460", t0, []string{"prva", "druga"}); err != nil {
t.Fatalf("Write: %v", err)
}
if err := s.Write("1CDBD4A6A460", t0.Add(time.Second), []string{"treca"}); err != nil {
t.Fatalf("Write: %v", err)
}
lines, err := s.Tail("1CDBD4A6A460", 10, "")
if err != nil {
t.Fatalf("Tail: %v", err)
}
if len(lines) != 3 {
t.Fatalf("linija = %d: %q", len(lines), lines)
}
if !strings.HasSuffix(lines[0], "prva") || !strings.HasSuffix(lines[2], "treca") {
t.Errorf("redosled nije hronoloski: %q", lines)
}
if !strings.HasPrefix(lines[0], "2026-07-25 12:00:00") {
t.Errorf("nedostaje vremenska oznaka: %q", lines[0])
}
}
func TestStorePretraga(t *testing.T) {
s := testStore(t, StoreConfig{})
t0 := time.Now()
s.Write("dev", t0, []string{"[stampac] greska", "[kapija] prolaz", "[stampac] ok"})
lines, _ := s.Tail("dev", 10, "STAMPAC")
if len(lines) != 2 {
t.Fatalf("pretraga = %d linija: %q", len(lines), lines)
}
lines, _ = s.Tail("dev", 10, "nepostojece")
if len(lines) != 0 {
t.Errorf("ocekivano 0 linija, dobijeno %q", lines)
}
}
func TestStoreOgranicavaBrojLinija(t *testing.T) {
s := testStore(t, StoreConfig{})
t0 := time.Now()
for i := 0; i < 100; i++ {
s.Write("dev", t0, []string{fmt.Sprintf("linija %d", i)})
}
lines, _ := s.Tail("dev", 10, "")
if len(lines) != 10 {
t.Fatalf("linija = %d", len(lines))
}
if !strings.HasSuffix(lines[9], "linija 99") {
t.Errorf("poslednja linija = %q", lines[9])
}
}
func TestStoreRotacijaPoVelicini(t *testing.T) {
s := testStore(t, StoreConfig{MaxFileBytes: 200})
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
for i := 0; i < 30; i++ {
if err := s.Write("dev", t0, []string{fmt.Sprintf("popunjavanje %02d", i)}); err != nil {
t.Fatalf("Write: %v", err)
}
}
files, err := os.ReadDir(filepath.Join(s.cfg.Dir, "dev"))
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
if len(files) < 2 {
t.Fatalf("ocekivana rotacija, fajlova = %d", len(files))
}
var imena []string
for _, f := range files {
imena = append(imena, f.Name())
if info, _ := f.Info(); info.Size() > 400 {
t.Errorf("fajl %s je %d B — granica se ne postuje", f.Name(), info.Size())
}
}
if imena[0] != "2026-07-25.log" || imena[1] != "2026-07-25.log.001" {
t.Errorf("imena fajlova = %q", imena)
}
// Sve linije se i dalje citaju, hronoloski.
lines, _ := s.Tail("dev", 100, "")
if len(lines) != 30 {
t.Errorf("procitano %d linija, ocekivano 30", len(lines))
}
if !strings.HasSuffix(lines[0], "popunjavanje 00") || !strings.HasSuffix(lines[29], "popunjavanje 29") {
t.Errorf("redosled kroz rotirane fajlove nije dobar: %q ... %q", lines[0], lines[29])
}
}
func TestStoreNoviDanNoviFajl(t *testing.T) {
s := testStore(t, StoreConfig{})
t0 := time.Date(2026, 7, 25, 23, 59, 0, 0, time.UTC)
s.Write("dev", t0, []string{"juce"})
s.Write("dev", t0.Add(2*time.Minute), []string{"danas"})
dani := s.Days("dev")
if len(dani) != 2 || dani[0] != "2026-07-26" || dani[1] != "2026-07-25" {
t.Errorf("dani = %q", dani)
}
}
func TestCleanupBriseStareDnevnike(t *testing.T) {
s := testStore(t, StoreConfig{KeepDays: 3})
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
for i := 0; i < 10; i++ {
s.Write("dev", now.AddDate(0, 0, -i), []string{"nesto"})
}
s.Close() // da ciscenje sme da dira i tekuci fajl
n, err := s.Cleanup(now)
if err != nil {
t.Fatalf("Cleanup: %v", err)
}
if n == 0 {
t.Fatal("nista nije obrisano")
}
dani := s.Days("dev")
for _, d := range dani {
dd, _ := time.Parse(dayLayout, d)
if dd.Before(now.AddDate(0, 0, -3)) {
t.Errorf("ostao stari dnevnik %s", d)
}
}
}
func TestCleanupPostujeKvotuPoUredjaju(t *testing.T) {
s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 600, KeepDays: 3650})
base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
for i := 0; i < 20; i++ {
s.Write("dev", base.AddDate(0, 0, i), []string{strings.Repeat("x", 200)})
}
s.Close()
if _, err := s.Cleanup(base.AddDate(0, 0, 20)); err != nil {
t.Fatalf("Cleanup: %v", err)
}
total, _ := s.Usage()
if total > 600 {
t.Errorf("posle ciscenja %d B, kvota je 600 B", total)
}
if total == 0 {
t.Error("ciscenje je obrisalo sve — najnoviji dnevnik mora ostati")
}
}
func TestCleanupPostujeUkupnuKvotu(t *testing.T) {
s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 1 << 20, MaxTotalBytes: 800, KeepDays: 3650})
base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
for i := 0; i < 10; i++ {
s.Write(fmt.Sprintf("dev%d", i), base.AddDate(0, 0, i), []string{strings.Repeat("y", 200)})
}
s.Close()
if _, err := s.Cleanup(base.AddDate(0, 0, 10)); err != nil {
t.Fatalf("Cleanup: %v", err)
}
total, _ := s.Usage()
if total > 800 {
t.Errorf("posle ciscenja %d B, ukupna kvota je 800 B", total)
}
}
func TestCleanupNeBriseOtvoreniFajl(t *testing.T) {
// Fajl u koji se trenutno pise ne sme da nestane: na Linuxu bi upisi
// posle brisanja tiho odlazili u nepostojeci fajl.
s := testStore(t, StoreConfig{MaxTotalBytes: 1, MaxDeviceBytes: 1, KeepDays: 1})
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
s.Write("dev", now, []string{"vazna linija"})
if _, err := s.Cleanup(now); err != nil {
t.Fatalf("Cleanup: %v", err)
}
if _, err := os.Stat(filepath.Join(s.cfg.Dir, "dev", "2026-07-25.log")); err != nil {
t.Errorf("otvoreni fajl je obrisan: %v", err)
}
}
func TestStoreOdbijaIzlazakIzFoldera(t *testing.T) {
s := testStore(t, StoreConfig{})
now := time.Now()
if err := s.Write("../../../pobegao", now, []string{"linija"}); err != nil {
t.Fatalf("Write: %v", err)
}
// Sve mora ostati unutar Dir.
var vanFoldera []string
filepath.Walk(filepath.Dir(s.cfg.Dir), func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
if !strings.HasPrefix(p, s.cfg.Dir) {
vanFoldera = append(vanFoldera, p)
}
return nil
})
if len(vanFoldera) > 0 {
t.Errorf("fajlovi napravljeni van foldera dnevnika: %q", vanFoldera)
}
}
func TestTailPreseceneLinijeNaGranici(t *testing.T) {
s := testStore(t, StoreConfig{})
now := time.Now()
s.Write("dev", now, []string{"cela linija jedan", "cela linija dva"})
lines, _ := s.Tail("dev", 5, "")
for _, ln := range lines {
if !strings.Contains(ln, "cela linija") {
t.Errorf("krnja linija u ispisu: %q", ln)
}
}
}
func TestTailNepostojeciUredjaj(t *testing.T) {
s := testStore(t, StoreConfig{})
lines, err := s.Tail("nema-ga", 10, "")
if err != nil {
t.Fatalf("Tail: %v", err)
}
if len(lines) != 0 {
t.Errorf("linije = %q", lines)
}
}