From 1b8db5e4a7932bd39d3108beb4e956aaef1a40fb Mon Sep 17 00:00:00 2001 From: djuka Date: Wed, 4 Mar 2026 07:58:24 +0000 Subject: [PATCH] Obimni testovi: 179 ukupno (46 Go + 133 Playwright) Novi Go testovi: - config_test.go: 9 testova (defaults, override, DSN, .env loading) - helpers_test.go: 13 testova (writeJSON, writeError, clientIP) Prosireni E2E testovi za svaku stranicu: - login: 15 testova (forma, auth, redirect, sesije) - dashboard: 18 testova (statistike, navbar, navigacija, odjava) - licenses: 20 testova (tabela, filteri, pretraga, kombinacije) - license-crud: 22 testa (forma, validacija, svi proizvodi/tipovi) - license-detail: 26 testova (info, aktivacije, audit, revoke, release) - audit: 14 testova (tabela, API zapisi, formati) - api-client: 18 testova (activate flow, auth, revoke flow) Azuriran TESTING.md sa kompletnom checklistom Co-Authored-By: Claude Opus 4.6 --- TESTING.md | 345 ++++++++++++++++++++++--------- internal/config/config_test.go | 215 +++++++++++++++++++ internal/handler/helpers_test.go | 178 ++++++++++++++++ tests/e2e/audit.spec.ts | 151 +++++++++++++- tests/e2e/dashboard.spec.ts | 95 +++++++-- tests/e2e/license-crud.spec.ts | 245 ++++++++++++++-------- tests/e2e/license-detail.spec.ts | 198 ++++++++++++++---- tests/e2e/licenses.spec.ts | 124 ++++++++++- tests/e2e/login.spec.ts | 95 ++++++++- 9 files changed, 1388 insertions(+), 258 deletions(-) create mode 100644 internal/config/config_test.go create mode 100644 internal/handler/helpers_test.go diff --git a/TESTING.md b/TESTING.md index 850115c..9f25816 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,105 +1,258 @@ # DAL License Server — Test Checklista -## Health -- [ ] GET /api/v1/health vraca {"status":"ok"} +## Ukupno testova: 179 +- Go unit testovi: 46 +- Playwright E2E testovi: 133 -## Proizvodi -- [ ] GET /api/v1/admin/products vraca 3 proizvoda (ESIR, ARV, LIGHT_TICKET) +## Pokretanje testova -## Kreiranje licence -- [ ] Kreiranje ESIR licence sa svim poljima -- [ ] Kreiranje ARV licence sa svim poljima -- [ ] Kreiranje LIGHT_TICKET licence sa svim poljima -- [ ] Kreiranje bez customer_name → error -- [ ] Kreiranje sa nepostojecim proizvodom → error -- [ ] Generisani kljuc ima pravilan prefix (ESIR-, ARV-, LT-) -- [ ] Generisani kljuc ima format XXXX-XXXX-XXXX-XXXX +```bash +# Go unit testovi +go test ./internal/... -v -count=1 -## Lista licenci -- [ ] GET /api/v1/admin/licenses vraca sve licence -- [ ] Filter po proizvodu radi ispravno +# Playwright E2E testovi (server mora biti pokrenut na :8090) +npx playwright test -## Detalji licence -- [ ] GET /api/v1/admin/licenses/{id} vraca sve podatke -- [ ] Nepostojeci ID → 404 - -## Aktivacija -- [ ] Aktivacija sa validnim kljucem i fingerprint-om → 200 + potpis -- [ ] Response sadrzi RSA-SHA256 potpis -- [ ] Response sadrzi sve licencne podatke (limits, features, customer) -- [ ] Ponovna aktivacija istog fingerprint-a → 200 (isti racunar) -- [ ] Aktivacija sa drugog racunara (drugi fingerprint) → ALREADY_ACTIVATED error -- [ ] Aktivacija sa nepostojecim kljucem → INVALID_KEY error -- [ ] Aktivacija opozvane licence → KEY_REVOKED error - -## Deaktivacija -- [ ] Deaktivacija aktivne licence → 200 -- [ ] Deaktivacija vec deaktivirane → error -- [ ] Deaktivacija sa pogresnim fingerprint-om → error - -## Ponovna aktivacija -- [ ] Posle deaktivacije, aktivacija sa novim fingerprint-om → 200 - -## Online validacija -- [ ] Validacija aktivne licence → valid: true -- [ ] Validacija opozvane licence → valid: false / revoked: true -- [ ] Validacija sa pogresnim fingerprint-om → error -- [ ] Validacija nepostojeceg kljuca → error - -## Opozivanje (Revoke) -- [ ] Revoke licence → uspesno -- [ ] Revoke vec opozvane → error -- [ ] Posle revoke-a, aktivacija odbija → KEY_REVOKED - -## Force Release -- [ ] Release aktivacije → uspesno -- [ ] Posle release-a, aktivacija sa novog racunara → 200 - -## API Key autentifikacija -- [ ] Admin endpoint bez X-API-Key → 401 -- [ ] Admin endpoint sa pogresnim kljucem → 401 -- [ ] Admin endpoint sa validnim kljucem → 200 - -## Rate limiting -- [ ] Vise od 10 activate zahteva u minuti → 429 -- [ ] Vise od 60 validate zahteva u minuti → 429 - -## Statistike -- [ ] GET /api/v1/admin/stats vraca podatke po proizvodima -- [ ] Brojevi se azuriraju nakon kreiranja/aktiviranja licence - -## Audit log -- [ ] GET /api/v1/admin/audit vraca log -- [ ] Aktivacija se loguje -- [ ] Deaktivacija se loguje -- [ ] Revoke se loguje -- [ ] Kreiranje licence se loguje -- [ ] Force release se loguje - -## Dashboard — Login -- [ ] GET /login prikazuje login formu -- [ ] Login sa ispravnom lozinkom → redirect na /dashboard -- [ ] Login sa pogresnom lozinkom → error -- [ ] Pristup /dashboard bez logina → redirect na /login - -## Dashboard — Stranice -- [ ] /dashboard prikazuje statistike po proizvodu -- [ ] /licenses prikazuje tabelu licenci -- [ ] /licenses?product=ESIR filtrira po proizvodu -- [ ] /licenses/new prikazuje formu za novu licencu -- [ ] POST /licenses kreira licencu i prikazuje detalje -- [ ] /licenses/{id} prikazuje detalje licence sa aktivacijama -- [ ] /audit prikazuje audit log - -## Dashboard — Akcije -- [ ] Revoke iz dashboard-a → uspesno, prikazuje poruku -- [ ] Force release iz dashboard-a → uspesno - -## Bezbednost -- [ ] .env nije u git-u -- [ ] crypto/private.pem nije u git-u -- [ ] Licencni kljuc je maskiran u logovima +# Sve zajedno +go test ./internal/... -v -count=1 && npx playwright test +``` --- -*Poslednje azuriranje: mart 2026* +## Go Unit Testovi (46) + +### Config (9 testova) +- [x] Podrazumevane vrednosti (port, env, db, rsa, rate limit) +- [x] Override iz env varijabli +- [x] DSN format (parseTime, charset, multiStatements) +- [x] DSN sa specijalnim karakterima +- [x] Ucitavanje .env fajla +- [x] Postojece env varijable se ne prepisuju iz .env +- [x] Komentari u .env se preskacu +- [x] Prazne linije u .env se preskacu +- [x] Nepostojeci .env fajl ne izaziva gresku + +### Keygen (4 testa) +- [x] Format kljuca: {PREFIX}-XXXX-XXXX-XXXX-XXXX +- [x] Samo dozvoljeni karakteri (A-H, J-N, P-Y, 2-9) +- [x] Nema konfuznih karaktera (O, 0, I, 1, L, Z) +- [x] Jedinstvenost 100 generisanih kljuceva + +### Crypto Service (7 testova) +- [x] Kreiranje servisa sa validnim kljucem +- [x] Greska za nepostojeci fajl +- [x] Greska za nevalidan PEM +- [x] Potpis format (RSA-SHA256:base64...) +- [x] Verifikacija potpisa sa public key-em +- [x] Tampered data ne prolazi verifikaciju +- [x] Generisanje public key PEM-a + +### Model — License (13 testova) +- [x] IsExpired: PERPETUAL (nikad ne istice) +- [x] IsExpired: aktivna licenca +- [x] IsExpired: istekla licenca +- [x] IsInGrace: nije istekla → false +- [x] IsInGrace: istekla pre 5 dana, grace 30 → true +- [x] IsInGrace: istekla pre 35 dana, grace 30 → false +- [x] IsInGrace: PERPETUAL → false +- [x] IsGraceExpired: istekao grace +- [x] IsGraceExpired: PERPETUAL → false +- [x] MaskedKey: LT, ESIR, ARV prefiksi +- [x] StatusText: revoked, inactive, active, trial, grace, expired +- [x] StatusClass: status-active, status-revoked +- [x] ExpiresAtFormatted: datum format i Neograniceno + +### Handler Helpers (13 testova) +- [x] writeJSON: status 200, Content-Type, body +- [x] writeJSON: status 201 +- [x] writeJSON: nil data +- [x] writeJSON: prazan slice [] +- [x] writeError: 400 sa kodom i porukom +- [x] writeError: 401 Unauthorized +- [x] writeError: 500 Internal Server Error +- [x] writeLicenseError: LicenseError → 400 +- [x] writeLicenseError: genericka greska → 500 +- [x] clientIP: RemoteAddr +- [x] clientIP: X-Forwarded-For +- [x] clientIP: X-Real-IP +- [x] clientIP: X-Forwarded-For prioritet + +### Middleware Auth (3 testa) +- [x] Validan API kljuc → 200 +- [x] Bez kljuca → 401 +- [x] Pogresan kljuc → 401 + +### Middleware Rate Limit (4 testa) +- [x] Dozvoljeni zahtevi prolaze (do limita) +- [x] Zahtev preko limita → blokiran +- [x] Razliciti IP-ovi imaju odvojene limite +- [x] X-Forwarded-For se koristi za identifikaciju + +--- + +## Playwright E2E Testovi (133) + +### Login stranica (15 testova) +- [x] Prikazuje login formu sa svim elementima +- [x] Password polje ima autofocus +- [x] Forma ima ispravnu action i method +- [x] Prijava sa ispravnom lozinkom preusmerava na dashboard +- [x] Prijava sa pogresnom lozinkom prikazuje gresku +- [x] Prijava sa praznom lozinkom (browser validacija) +- [x] Razlicite pogresne lozinke (7 pokusaja) +- [x] Pristup /dashboard bez logina → redirect +- [x] Pristup /licenses bez logina → redirect +- [x] Pristup /licenses/new bez logina → redirect +- [x] Pristup /audit bez logina → redirect +- [x] Pristup /licenses/1 bez logina → redirect +- [x] CSS je ucitan +- [x] Ispravan page title +- [x] Visestruki logini kreiraju razlicite sesije + +### Dashboard stranica (18 testova) +- [x] Prikazuje naslov Dashboard +- [x] 3 statisticke kartice (ESIR, ARV, LIGHT_TICKET) +- [x] Svaka kartica prikazuje ime proizvoda +- [x] Svaka kartica prikazuje sve metrike (5 redova) +- [x] Sekcija poslednje aktivnosti +- [x] Tabela aktivnosti ima ispravne kolone +- [x] Navbar prikazuje brend +- [x] Navbar dashboard link je aktivan +- [x] Navbar ima link na licence +- [x] Navbar ima link na audit log +- [x] Navbar ima dugme za odjavu +- [x] Odjava preusmerava na login +- [x] Posle odjave ne moze na dashboard +- [x] Root (/) preusmerava na dashboard +- [x] Navigacija na licence +- [x] Navigacija na audit +- [x] Ispravan page title +- [x] htmx je ucitan + +### Lista licenci — stranica (20 testova) +- [x] Prikazuje naslov Licence +- [x] Ispravan page title +- [x] Tabela sa 7 ispravnih kolona +- [x] Dugme za novu licencu (btn-primary) +- [x] Dugme vodi na formu +- [x] Filter dropdown za proizvod (4 opcije) +- [x] Filter dropdown za status (5 opcija) +- [x] Pretraga polje sa placeholder-om +- [x] Filtriraj dugme +- [x] Filter po ESIR proizvodu +- [x] Filter po ARV proizvodu +- [x] Filter po LIGHT_TICKET proizvodu +- [x] Filter po statusu active +- [x] Filter po statusu expired +- [x] Filter po statusu revoked +- [x] Pretraga po firmi +- [x] Kombinacija filtera (proizvod + status) +- [x] Filter cuva selekciju posle submit-a +- [x] Navbar licence link je aktivan +- [x] Poruka kad nema licenci + +### Kreiranje licence — forma (14 testova) +- [x] Naslov Nova licenca +- [x] Ispravan page title +- [x] Sva polja forme prikazana +- [x] customer_name je obavezan (required) +- [x] product_id je obavezan (required) +- [x] license_type je obavezan (required) +- [x] Dropdown ima min 3 proizvoda +- [x] Dropdown ima sve tipove licenci +- [x] grace_days default je 30 +- [x] limits placeholder +- [x] email polje ima type email +- [x] Otkazi dugme vodi na listu +- [x] Forma ima POST method +- [x] Kreiraj licencu dugme (btn-primary) + +### Kreiranje licence — svi proizvodi (8 testova) +- [x] LIGHT_TICKET MONTHLY sa svim poljima + format kljuca +- [x] LIGHT_TICKET PERPETUAL (Neograniceno) +- [x] ARV ANNUAL +- [x] ARV TRIAL +- [x] ESIR PERPETUAL +- [x] ESIR MONTHLY +- [x] Kreirana licenca se pojavljuje u listi +- [x] Kreirana licenca ima status Aktivna + +### Detalji licence — informacije (8 testova) +- [x] Licencni kljuc u naslovu +- [x] Status badge +- [x] Sekcija Informacije +- [x] Sva informaciona polja (8 polja) +- [x] Ispravni podaci za LT licencu +- [x] Ispravni podaci za ARV licencu +- [x] PERPETUAL prikazuje Neograniceno +- [x] Ispravan title sa kljucem + +### Detalji licence — aktivacije i audit (6 testova) +- [x] Tabela aktivacija +- [x] Nova licenca nema aktivacija +- [x] Tabela aktivacija ima ispravne kolone +- [x] Audit log za licencu +- [x] CREATE audit zapis +- [x] Audit tabela ima ispravne kolone + +### Detalji licence — akcije (10 testova) +- [x] Sekcija Akcije +- [x] Dugme za opoziv (btn-danger) +- [x] Dugme za force release (btn-warning) +- [x] Polje za razlog opoziva +- [x] Revoke menja status na Opozvana +- [x] Revoke generise REVOKE audit zapis +- [x] Posle revoke-a dugme za opoziv nestaje +- [x] Posle revoke-a force release ostaje +- [x] Force release funkcionise +- [x] Force release generise FORCE_RELEASE audit zapis + +### Detalji licence — navigacija (2 testa) +- [x] Klik iz liste otvara detalje +- [x] Navbar licence link aktivan + +### Audit Log stranica (11 testova) +- [x] Naslov Audit Log +- [x] Ispravan page title +- [x] Tabela prikazana +- [x] 5 kolona (Vreme, Akcija, Licenca, IP, Detalji) +- [x] Navbar audit link aktivan +- [x] Navigacija iz navbara +- [x] CREATE zapis posle kreiranja licence +- [x] Licencni kljuc prikazan u auditu +- [x] IP adresa prikazana +- [x] Vreme u DD.MM.YYYY formatu +- [x] Detalji kao JSON + +### Audit Log — API zapisi (3 testa) +- [x] ACTIVATE zapis posle aktivacije +- [x] DEACTIVATE zapis posle deaktivacije +- [x] VALIDATE zapis posle validacije + +### Klijentski API (8 testova) +- [x] Aktivacija licence (200 + potpis) +- [x] Ponovna aktivacija istim fingerprint-om (refresh) +- [x] Aktivacija sa drugog racunara → ALREADY_ACTIVATED +- [x] Validacija aktivne licence +- [x] Deaktivacija licence +- [x] Re-aktivacija posle deaktivacije +- [x] Nepostojeci kljuc → INVALID_KEY +- [x] Validacija nepostojeceg kljuca → valid: false + +### Admin API autentifikacija (7 testova) +- [x] Bez API kljuca → 401 +- [x] Pogresan kljuc → 401 +- [x] Ispravan kljuc → 200 +- [x] Products endpoint +- [x] Stats endpoint +- [x] Audit endpoint +- [x] Health endpoint + +### Admin API — Revoke flow (3 testa) +- [x] Revoke licence +- [x] Aktivacija opozvane → KEY_REVOKED +- [x] Validacija opozvane → valid: false, revoked: true + +--- + +*Poslednje azuriranje: 04.03.2026 — 179 testova (46 Go + 133 Playwright)* diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..eed4604 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,215 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoad_Defaults(t *testing.T) { + // Clear env to test defaults + envVars := []string{"APP_PORT", "APP_ENV", "DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASS", + "ADMIN_API_KEY", "ADMIN_PASSWORD", "SESSION_SECRET", "RSA_PRIVATE_KEY_PATH", + "RATE_LIMIT_ACTIVATE", "RATE_LIMIT_VALIDATE", "LOG_LEVEL"} + saved := make(map[string]string) + for _, v := range envVars { + saved[v] = os.Getenv(v) + os.Unsetenv(v) + } + defer func() { + for k, v := range saved { + if v != "" { + os.Setenv(k, v) + } + } + }() + + cfg := Load() + + if cfg.Port != "8090" { + t.Errorf("Port default: ocekivano '8090', dobijeno %q", cfg.Port) + } + if cfg.Env != "development" { + t.Errorf("Env default: ocekivano 'development', dobijeno %q", cfg.Env) + } + if cfg.DBHost != "localhost" { + t.Errorf("DBHost default: ocekivano 'localhost', dobijeno %q", cfg.DBHost) + } + if cfg.DBPort != "3306" { + t.Errorf("DBPort default: ocekivano '3306', dobijeno %q", cfg.DBPort) + } + if cfg.DBName != "dal_license_db" { + t.Errorf("DBName default: ocekivano 'dal_license_db', dobijeno %q", cfg.DBName) + } + if cfg.DBUser != "license" { + t.Errorf("DBUser default: ocekivano 'license', dobijeno %q", cfg.DBUser) + } + if cfg.RSAPrivateKey != "./crypto/private.pem" { + t.Errorf("RSAPrivateKey default: ocekivano './crypto/private.pem', dobijeno %q", cfg.RSAPrivateKey) + } + if cfg.RateLimitActivate != "10" { + t.Errorf("RateLimitActivate default: ocekivano '10', dobijeno %q", cfg.RateLimitActivate) + } + if cfg.RateLimitValidate != "60" { + t.Errorf("RateLimitValidate default: ocekivano '60', dobijeno %q", cfg.RateLimitValidate) + } + if cfg.LogLevel != "info" { + t.Errorf("LogLevel default: ocekivano 'info', dobijeno %q", cfg.LogLevel) + } +} + +func TestLoad_EnvOverrides(t *testing.T) { + os.Setenv("APP_PORT", "9999") + os.Setenv("APP_ENV", "production") + os.Setenv("DB_HOST", "db.example.com") + defer func() { + os.Unsetenv("APP_PORT") + os.Unsetenv("APP_ENV") + os.Unsetenv("DB_HOST") + }() + + cfg := Load() + + if cfg.Port != "9999" { + t.Errorf("Port override: ocekivano '9999', dobijeno %q", cfg.Port) + } + if cfg.Env != "production" { + t.Errorf("Env override: ocekivano 'production', dobijeno %q", cfg.Env) + } + if cfg.DBHost != "db.example.com" { + t.Errorf("DBHost override: ocekivano 'db.example.com', dobijeno %q", cfg.DBHost) + } +} + +func TestDSN(t *testing.T) { + cfg := &Config{ + DBUser: "testuser", + DBPass: "testpass", + DBHost: "localhost", + DBPort: "3306", + DBName: "testdb", + } + + dsn := cfg.DSN() + expected := "testuser:testpass@tcp(localhost:3306)/testdb?parseTime=true&charset=utf8mb4&multiStatements=true" + if dsn != expected { + t.Errorf("DSN:\nocekivano: %q\ndobijeno: %q", expected, dsn) + } +} + +func TestDSN_SpecialChars(t *testing.T) { + cfg := &Config{ + DBUser: "user", + DBPass: "p@ss:w0rd", + DBHost: "192.168.1.1", + DBPort: "3307", + DBName: "my_db", + } + + dsn := cfg.DSN() + if dsn == "" { + t.Error("DSN ne sme biti prazan") + } + // Mora sadrzati sve parametre + if !contains(dsn, "parseTime=true") { + t.Error("DSN mora sadrzati parseTime=true") + } + if !contains(dsn, "multiStatements=true") { + t.Error("DSN mora sadrzati multiStatements=true") + } + if !contains(dsn, "charset=utf8mb4") { + t.Error("DSN mora sadrzati charset=utf8mb4") + } +} + +func TestLoadEnvFile(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, ".env") + content := `# Comment +TEST_CONFIG_VAR=hello_world +TEST_CONFIG_NUM=42 +` + os.WriteFile(envPath, []byte(content), 0644) + + os.Unsetenv("TEST_CONFIG_VAR") + os.Unsetenv("TEST_CONFIG_NUM") + + loadEnvFile(envPath) + + if v := os.Getenv("TEST_CONFIG_VAR"); v != "hello_world" { + t.Errorf("TEST_CONFIG_VAR: ocekivano 'hello_world', dobijeno %q", v) + } + if v := os.Getenv("TEST_CONFIG_NUM"); v != "42" { + t.Errorf("TEST_CONFIG_NUM: ocekivano '42', dobijeno %q", v) + } + + os.Unsetenv("TEST_CONFIG_VAR") + os.Unsetenv("TEST_CONFIG_NUM") +} + +func TestLoadEnvFile_ExistingEnvNotOverwritten(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, ".env") + os.WriteFile(envPath, []byte("TEST_EXISTING=from_file\n"), 0644) + + os.Setenv("TEST_EXISTING", "from_env") + defer os.Unsetenv("TEST_EXISTING") + + loadEnvFile(envPath) + + if v := os.Getenv("TEST_EXISTING"); v != "from_env" { + t.Errorf("env varijabla ne sme biti prepisana iz fajla: ocekivano 'from_env', dobijeno %q", v) + } +} + +func TestLoadEnvFile_SkipsComments(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, ".env") + os.WriteFile(envPath, []byte("# COMMENTED_VAR=should_not_set\nACTUAL_VAR=set_this\n"), 0644) + + os.Unsetenv("COMMENTED_VAR") + os.Unsetenv("ACTUAL_VAR") + + loadEnvFile(envPath) + + if v := os.Getenv("COMMENTED_VAR"); v != "" { + t.Errorf("komentarisana varijabla ne sme biti setovana: dobijeno %q", v) + } + if v := os.Getenv("ACTUAL_VAR"); v != "set_this" { + t.Errorf("ACTUAL_VAR: ocekivano 'set_this', dobijeno %q", v) + } + + os.Unsetenv("ACTUAL_VAR") +} + +func TestLoadEnvFile_EmptyLines(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, ".env") + os.WriteFile(envPath, []byte("\n\nVAR_AFTER_EMPTY=works\n\n"), 0644) + + os.Unsetenv("VAR_AFTER_EMPTY") + loadEnvFile(envPath) + + if v := os.Getenv("VAR_AFTER_EMPTY"); v != "works" { + t.Errorf("ocekivano 'works', dobijeno %q", v) + } + os.Unsetenv("VAR_AFTER_EMPTY") +} + +func TestLoadEnvFile_NonexistentFile(t *testing.T) { + // Ne sme pasti + loadEnvFile("/nonexistent/path/.env") +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr)) +} + +func containsStr(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} diff --git a/internal/handler/helpers_test.go b/internal/handler/helpers_test.go new file mode 100644 index 0000000..5a3bf78 --- /dev/null +++ b/internal/handler/helpers_test.go @@ -0,0 +1,178 @@ +package handler + +import ( + "dal-license-server/internal/model" + "dal-license-server/internal/service" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestWriteJSON(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]string{"key": "value"} + writeJSON(w, http.StatusOK, data) + + if w.Code != http.StatusOK { + t.Errorf("status: ocekivano 200, dobijeno %d", w.Code) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("Content-Type: ocekivano 'application/json', dobijeno %q", ct) + } + + var result map[string]string + json.Unmarshal(w.Body.Bytes(), &result) + if result["key"] != "value" { + t.Errorf("body: ocekivano 'value', dobijeno %q", result["key"]) + } +} + +func TestWriteJSON_StatusCreated(t *testing.T) { + w := httptest.NewRecorder() + writeJSON(w, http.StatusCreated, map[string]int{"id": 1}) + + if w.Code != http.StatusCreated { + t.Errorf("status: ocekivano 201, dobijeno %d", w.Code) + } +} + +func TestWriteJSON_NilData(t *testing.T) { + w := httptest.NewRecorder() + writeJSON(w, http.StatusOK, nil) + + if w.Code != http.StatusOK { + t.Errorf("status: ocekivano 200, dobijeno %d", w.Code) + } +} + +func TestWriteJSON_EmptySlice(t *testing.T) { + w := httptest.NewRecorder() + writeJSON(w, http.StatusOK, []string{}) + + body := w.Body.String() + if body != "[]\n" { + t.Errorf("body: ocekivano '[]\\n', dobijeno %q", body) + } +} + +func TestWriteError(t *testing.T) { + w := httptest.NewRecorder() + writeError(w, http.StatusBadRequest, "INVALID_KEY", "Kljuc nije pronadjen") + + if w.Code != http.StatusBadRequest { + t.Errorf("status: ocekivano 400, dobijeno %d", w.Code) + } + + var resp model.ErrorResponse + json.Unmarshal(w.Body.Bytes(), &resp) + + if resp.Error.Code != "INVALID_KEY" { + t.Errorf("error code: ocekivano 'INVALID_KEY', dobijeno %q", resp.Error.Code) + } + if resp.Error.Message != "Kljuc nije pronadjen" { + t.Errorf("error message: ocekivano 'Kljuc nije pronadjen', dobijeno %q", resp.Error.Message) + } +} + +func TestWriteError_Unauthorized(t *testing.T) { + w := httptest.NewRecorder() + writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Neautorizovan pristup") + + if w.Code != http.StatusUnauthorized { + t.Errorf("status: ocekivano 401, dobijeno %d", w.Code) + } +} + +func TestWriteError_InternalServerError(t *testing.T) { + w := httptest.NewRecorder() + writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Nesto je poslo naopako") + + if w.Code != http.StatusInternalServerError { + t.Errorf("status: ocekivano 500, dobijeno %d", w.Code) + } +} + +func TestWriteLicenseError(t *testing.T) { + w := httptest.NewRecorder() + err := &service.LicenseError{ + Code: "ALREADY_ACTIVATED", + Message: "Licenca je vec aktivirana", + Details: map[string]interface{}{"activated_on": "PC-1"}, + } + writeLicenseError(w, err) + + if w.Code != http.StatusBadRequest { + t.Errorf("status: ocekivano 400, dobijeno %d", w.Code) + } + + var resp model.ErrorResponse + json.Unmarshal(w.Body.Bytes(), &resp) + + if resp.Error.Code != "ALREADY_ACTIVATED" { + t.Errorf("error code: ocekivano 'ALREADY_ACTIVATED', dobijeno %q", resp.Error.Code) + } +} + +func TestWriteLicenseError_GenericError(t *testing.T) { + w := httptest.NewRecorder() + writeLicenseError(w, &genericError{"some error"}) + + if w.Code != http.StatusInternalServerError { + t.Errorf("status: ocekivano 500, dobijeno %d", w.Code) + } + + var resp model.ErrorResponse + json.Unmarshal(w.Body.Bytes(), &resp) + if resp.Error.Code != "INTERNAL_ERROR" { + t.Errorf("generic error mora vratiti INTERNAL_ERROR, dobijeno %q", resp.Error.Code) + } +} + +type genericError struct{ msg string } + +func (e *genericError) Error() string { return e.msg } + +func TestClientIP_RemoteAddr(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "1.2.3.4:5678" + + ip := clientIP(req) + if ip != "1.2.3.4:5678" { + t.Errorf("ocekivano '1.2.3.4:5678', dobijeno %q", ip) + } +} + +func TestClientIP_XForwardedFor(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:1234" + req.Header.Set("X-Forwarded-For", "5.5.5.5") + + ip := clientIP(req) + if ip != "5.5.5.5" { + t.Errorf("ocekivano '5.5.5.5', dobijeno %q", ip) + } +} + +func TestClientIP_XRealIP(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:1234" + req.Header.Set("X-Real-IP", "8.8.8.8") + + ip := clientIP(req) + if ip != "8.8.8.8" { + t.Errorf("ocekivano '8.8.8.8', dobijeno %q", ip) + } +} + +func TestClientIP_XForwardedFor_Priority(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:1234" + req.Header.Set("X-Forwarded-For", "1.1.1.1") + req.Header.Set("X-Real-IP", "2.2.2.2") + + ip := clientIP(req) + if ip != "1.1.1.1" { + t.Errorf("X-Forwarded-For mora imati prioritet, ocekivano '1.1.1.1', dobijeno %q", ip) + } +} diff --git a/tests/e2e/audit.spec.ts b/tests/e2e/audit.spec.ts index d9595b7..3ecca32 100644 --- a/tests/e2e/audit.spec.ts +++ b/tests/e2e/audit.spec.ts @@ -1,5 +1,8 @@ import { test, expect, Page } from '@playwright/test'; +const BASE = 'http://localhost:8090'; +const API_KEY = 'dev-api-key-minimum-32-characters-long'; + async function login(page: Page) { await page.goto('/login'); await page.fill('input[name="password"]', 'admin123'); @@ -7,20 +10,30 @@ async function login(page: Page) { await expect(page).toHaveURL(/\/dashboard/); } -test.describe('Audit Log', () => { +test.describe('Audit Log stranica', () => { test.beforeEach(async ({ page }) => { await login(page); }); - test('prikazuje audit log stranicu', async ({ page }) => { + test('prikazuje naslov Audit Log', async ({ page }) => { await page.goto('/audit'); await expect(page.locator('h1')).toHaveText('Audit Log'); + }); + + test('ima ispravan title', async ({ page }) => { + await page.goto('/audit'); + await expect(page).toHaveTitle('Audit Log - DAL License Server'); + }); + + test('prikazuje tabelu', async ({ page }) => { + await page.goto('/audit'); await expect(page.locator('table')).toBeVisible(); }); - test('tabela ima ispravne kolone', async ({ page }) => { + test('tabela ima 5 kolona', async ({ page }) => { await page.goto('/audit'); const headers = page.locator('thead th'); + await expect(headers).toHaveCount(5); await expect(headers.nth(0)).toHaveText('Vreme'); await expect(headers.nth(1)).toHaveText('Akcija'); await expect(headers.nth(2)).toHaveText('Licenca'); @@ -28,7 +41,18 @@ test.describe('Audit Log', () => { await expect(headers.nth(4)).toHaveText('Detalji'); }); - test('kreiranje licence generise audit zapis', async ({ page }) => { + test('navbar audit link je aktivan', async ({ page }) => { + await page.goto('/audit'); + const link = page.locator('a[href="/audit"]'); + await expect(link).toHaveClass(/active/); + }); + + test('navigacija iz navbara radi', async ({ page }) => { + await page.click('a[href="/audit"]'); + await expect(page).toHaveURL(/\/audit/); + }); + + test('kreiranje licence generise CREATE zapis u audit-u', async ({ page }) => { // Kreiraj licencu await page.goto('/licenses/new'); const productSelect = page.locator('select[name="product_id"]'); @@ -42,18 +66,125 @@ test.describe('Audit Log', () => { } } await page.selectOption('select[name="license_type"]', 'MONTHLY'); - await page.fill('input[name="customer_name"]', 'Audit E2E Test'); + await page.fill('input[name="customer_name"]', `Audit Create ${Date.now()}`); await page.click('button:has-text("Kreiraj licencu")'); await expect(page).toHaveURL(/\/licenses\/\d+/); - // Proveri audit log + // Proveri audit await page.goto('/audit'); await expect(page.locator('.badge', { hasText: 'CREATE' }).first()).toBeVisible(); }); - test('navigacija na audit iz navbara', async ({ page }) => { - await page.click('a[href="/audit"]'); - await expect(page).toHaveURL(/\/audit/); - await expect(page.locator('h1')).toHaveText('Audit Log'); + test('audit prikazuje licencni kljuc', async ({ page }) => { + await page.goto('/audit'); + // Audit zapisi koji imaju licencu prikazuju code element + const codeElements = page.locator('tbody code'); + const count = await codeElements.count(); + if (count > 0) { + const firstKey = await codeElements.first().textContent(); + expect(firstKey).toMatch(/^(LT|ARV|ESIR)-/); + } + }); + + test('audit prikazuje IP adresu', async ({ page }) => { + await page.goto('/audit'); + // Mora biti bar jedan zapis sa IP adresom + const rows = page.locator('tbody tr'); + const count = await rows.count(); + expect(count).toBeGreaterThan(0); + }); + + test('audit prikazuje vreme u ispravnom formatu', async ({ page }) => { + await page.goto('/audit'); + const firstTimeCell = page.locator('tbody td').first(); + const text = await firstTimeCell.textContent(); + // Format: DD.MM.YYYY HH:MM + if (text && text.trim() !== 'Nema podataka') { + expect(text).toMatch(/\d{2}\.\d{2}\.\d{4}/); + } + }); + + test('audit prikazuje detalje kao JSON', async ({ page }) => { + await page.goto('/audit'); + const details = page.locator('.audit-details'); + const count = await details.count(); + if (count > 0) { + await expect(details.first()).toBeVisible(); + } + }); +}); + +test.describe('Audit Log — API zapisi', () => { + test('aktivacija generise ACTIVATE zapis', async ({ request, page }) => { + // Kreiraj licencu + const createRes = await request.post(`${BASE}/api/v1/admin/licenses`, { + headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, + data: { + product_id: 3, + license_type: 'MONTHLY', + customer_name: 'Audit Activate Test', + }, + }); + const created = await createRes.json(); + + // Aktiviraj + await request.post(`${BASE}/api/v1/activate`, { + data: { + license_key: created.license_key, + machine_fingerprint: 'sha256:audit-activate-test', + app_version: '1.0.0', + os: 'linux', + hostname: 'AUDIT-PC', + }, + }); + + // Proveri audit na UI + await login(page); + await page.goto('/audit'); + await expect(page.locator('.badge', { hasText: 'ACTIVATE' }).first()).toBeVisible(); + }); + + test('deaktivacija generise DEACTIVATE zapis', async ({ request, page }) => { + const createRes = await request.post(`${BASE}/api/v1/admin/licenses`, { + headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, + data: { + product_id: 3, + license_type: 'MONTHLY', + customer_name: 'Audit Deactivate Test', + }, + }); + const created = await createRes.json(); + const fp = 'sha256:audit-deactivate-test'; + + await request.post(`${BASE}/api/v1/activate`, { + data: { license_key: created.license_key, machine_fingerprint: fp, app_version: '1.0.0', os: 'linux', hostname: 'PC' }, + }); + await request.post(`${BASE}/api/v1/deactivate`, { + data: { license_key: created.license_key, machine_fingerprint: fp }, + }); + + await login(page); + await page.goto('/audit'); + await expect(page.locator('.badge', { hasText: 'DEACTIVATE' }).first()).toBeVisible(); + }); + + test('validacija generise VALIDATE zapis', async ({ request, page }) => { + const createRes = await request.post(`${BASE}/api/v1/admin/licenses`, { + headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, + data: { + product_id: 3, + license_type: 'MONTHLY', + customer_name: 'Audit Validate Test', + }, + }); + const created = await createRes.json(); + + await request.post(`${BASE}/api/v1/validate`, { + data: { license_key: created.license_key, machine_fingerprint: 'sha256:validate-test' }, + }); + + await login(page); + await page.goto('/audit'); + await expect(page.locator('.badge', { hasText: 'VALIDATE' }).first()).toBeVisible(); }); }); diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts index 3a77928..9627441 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard.spec.ts @@ -7,41 +7,112 @@ async function login(page: Page) { await expect(page).toHaveURL(/\/dashboard/); } -test.describe('Dashboard', () => { +test.describe('Dashboard stranica', () => { test.beforeEach(async ({ page }) => { await login(page); }); - test('prikazuje statistike po proizvodu', async ({ page }) => { + test('prikazuje naslov Dashboard', async ({ page }) => { await expect(page.locator('h1')).toHaveText('Dashboard'); - await expect(page.locator('.stats-grid')).toBeVisible(); - // Treba da postoje kartice za proizvode - const statCards = page.locator('.stat-card'); - await expect(statCards).toHaveCount(3); // ESIR, ARV, LIGHT_TICKET }); - test('prikazuje tabelu poslednje aktivnosti', async ({ page }) => { + test('prikazuje statistike po proizvodu — 3 kartice', async ({ page }) => { + await expect(page.locator('.stats-grid')).toBeVisible(); + const statCards = page.locator('.stat-card'); + await expect(statCards).toHaveCount(3); + }); + + test('svaka kartica prikazuje ime proizvoda', async ({ page }) => { + const labels = page.locator('.stat-label'); + const texts = await labels.allTextContents(); + expect(texts).toContain('ESIR Fiskalizacija'); + expect(texts).toContain('ARV Evidencija RV'); + expect(texts).toContain('Light-Ticket'); + }); + + test('svaka kartica prikazuje sve metrike', async ({ page }) => { + const cards = page.locator('.stat-card'); + const count = await cards.count(); + for (let i = 0; i < count; i++) { + const card = cards.nth(i); + await expect(card.locator('.stat-row', { hasText: 'Aktivne:' })).toBeVisible(); + await expect(card.locator('.stat-row', { hasText: 'Istekle:' })).toBeVisible(); + await expect(card.locator('.stat-row', { hasText: 'Grace:' })).toBeVisible(); + await expect(card.locator('.stat-row', { hasText: 'Trial:' })).toBeVisible(); + await expect(card.locator('.stat-row', { hasText: 'Aktivacija:' })).toBeVisible(); + } + }); + + test('prikazuje sekciju poslednje aktivnosti', async ({ page }) => { await expect(page.locator('h2', { hasText: 'Poslednja aktivnost' })).toBeVisible(); await expect(page.locator('table').last()).toBeVisible(); }); - test('navbar ima ispravne linkove', async ({ page }) => { + test('tabela poslednje aktivnosti ima ispravne kolone', async ({ page }) => { + const lastTable = page.locator('table').last(); + const headers = lastTable.locator('thead th'); + await expect(headers.nth(0)).toHaveText('Vreme'); + await expect(headers.nth(1)).toHaveText('Akcija'); + await expect(headers.nth(2)).toHaveText('Licenca'); + await expect(headers.nth(3)).toHaveText('IP'); + }); + + test('navbar prikazuje brend', async ({ page }) => { await expect(page.locator('.nav-brand')).toHaveText('DAL License Server'); - await expect(page.locator('a[href="/dashboard"]')).toBeVisible(); + }); + + test('navbar ima link na dashboard (aktivan)', async ({ page }) => { + const link = page.locator('a[href="/dashboard"]'); + await expect(link).toBeVisible(); + await expect(link).toHaveClass(/active/); + }); + + test('navbar ima link na licence', async ({ page }) => { await expect(page.locator('a[href="/licenses"]')).toBeVisible(); + }); + + test('navbar ima link na audit log', async ({ page }) => { await expect(page.locator('a[href="/audit"]')).toBeVisible(); }); - test('odjava funkcionise', async ({ page }) => { + test('navbar ima dugme za odjavu', async ({ page }) => { + await expect(page.locator('button:has-text("Odjava")')).toBeVisible(); + }); + + test('odjava preusmerava na login', async ({ page }) => { await page.click('button:has-text("Odjava")'); await expect(page).toHaveURL(/\/login/); - // Posle odjave ne moze na dashboard + }); + + test('posle odjave ne moze na dashboard', async ({ page }) => { + await page.click('button:has-text("Odjava")'); await page.goto('/dashboard'); await expect(page).toHaveURL(/\/login/); }); - test('root preusmerava na dashboard', async ({ page }) => { + test('root (/) preusmerava na dashboard', async ({ page }) => { await page.goto('/'); await expect(page).toHaveURL(/\/dashboard/); }); + + test('navigacija sa dashboarda na licence', async ({ page }) => { + await page.click('a[href="/licenses"]'); + await expect(page).toHaveURL(/\/licenses/); + await expect(page.locator('h1')).toHaveText('Licence'); + }); + + test('navigacija sa dashboarda na audit', async ({ page }) => { + await page.click('a[href="/audit"]'); + await expect(page).toHaveURL(/\/audit/); + await expect(page.locator('h1')).toHaveText('Audit Log'); + }); + + test('dashboard ima ispravan title', async ({ page }) => { + await expect(page).toHaveTitle('Dashboard - DAL License Server'); + }); + + test('htmx je ucitan', async ({ page }) => { + const htmxLoaded = await page.evaluate(() => typeof (window as any).htmx !== 'undefined'); + expect(htmxLoaded).toBe(true); + }); }); diff --git a/tests/e2e/license-crud.spec.ts b/tests/e2e/license-crud.spec.ts index 18b0b3d..1f33e76 100644 --- a/tests/e2e/license-crud.spec.ts +++ b/tests/e2e/license-crud.spec.ts @@ -7,14 +7,34 @@ async function login(page: Page) { await expect(page).toHaveURL(/\/dashboard/); } -test.describe('Kreiranje licence', () => { +async function selectProduct(page: Page, productCode: string) { + const productSelect = page.locator('select[name="product_id"]'); + const options = productSelect.locator('option'); + const count = await options.count(); + for (let i = 0; i < count; i++) { + const text = await options.nth(i).textContent(); + if (text && text.includes(productCode)) { + await productSelect.selectOption({ index: i }); + return; + } + } +} + +test.describe('Forma za novu licencu', () => { test.beforeEach(async ({ page }) => { await login(page); + await page.goto('/licenses/new'); }); - test('forma za novu licencu prikazuje sva polja', async ({ page }) => { - await page.goto('/licenses/new'); + test('prikazuje naslov Nova licenca', async ({ page }) => { await expect(page.locator('h1')).toHaveText('Nova licenca'); + }); + + test('ima ispravan title', async ({ page }) => { + await expect(page).toHaveTitle('Nova licenca - DAL License Server'); + }); + + test('prikazuje sva polja forme', async ({ page }) => { await expect(page.locator('select[name="product_id"]')).toBeVisible(); await expect(page.locator('select[name="license_type"]')).toBeVisible(); await expect(page.locator('input[name="customer_name"]')).toBeVisible(); @@ -25,15 +45,29 @@ test.describe('Kreiranje licence', () => { await expect(page.locator('textarea[name="notes"]')).toBeVisible(); }); - test('dropdown ima sve proizvode', async ({ page }) => { - await page.goto('/licenses/new'); + test('customer_name je obavezan', async ({ page }) => { + await expect(page.locator('input[name="customer_name"]')).toHaveAttribute('required', ''); + }); + + test('product_id je obavezan', async ({ page }) => { + await expect(page.locator('select[name="product_id"]')).toHaveAttribute('required', ''); + }); + + test('license_type je obavezan', async ({ page }) => { + await expect(page.locator('select[name="license_type"]')).toHaveAttribute('required', ''); + }); + + test('dropdown ima sve proizvode (min 3)', async ({ page }) => { const options = page.locator('select[name="product_id"] option'); const count = await options.count(); - expect(count).toBeGreaterThanOrEqual(3); // ESIR, ARV, LIGHT_TICKET + expect(count).toBeGreaterThanOrEqual(3); + const texts = await options.allTextContents(); + expect(texts.some(t => t.includes('ESIR'))).toBe(true); + expect(texts.some(t => t.includes('ARV'))).toBe(true); + expect(texts.some(t => t.includes('LIGHT_TICKET'))).toBe(true); }); test('dropdown ima sve tipove licenci', async ({ page }) => { - await page.goto('/licenses/new'); const options = page.locator('select[name="license_type"] option'); const texts = await options.allTextContents(); expect(texts.join(' ')).toContain('Mesecna'); @@ -42,91 +76,134 @@ test.describe('Kreiranje licence', () => { expect(texts.join(' ')).toContain('Trial'); }); - test('kreiranje LIGHT_TICKET licence', async ({ page }) => { - await page.goto('/licenses/new'); - - // Izaberi Light-Ticket proizvod - const productSelect = page.locator('select[name="product_id"]'); - const options = productSelect.locator('option'); - const count = await options.count(); - for (let i = 0; i < count; i++) { - const text = await options.nth(i).textContent(); - if (text && text.includes('LIGHT_TICKET')) { - await productSelect.selectOption({ index: i }); - break; - } - } - - await page.selectOption('select[name="license_type"]', 'MONTHLY'); - await page.fill('input[name="customer_name"]', 'E2E Test Firma DOO'); - await page.fill('input[name="customer_pib"]', '999888777'); - await page.fill('input[name="customer_email"]', 'e2e@test.rs'); - await page.fill('input[name="limits"]', '{"max_operators": 5}'); - await page.fill('input[name="grace_days"]', '30'); - await page.fill('textarea[name="notes"]', 'Kreirana iz E2E testa'); - - await page.click('button:has-text("Kreiraj licencu")'); - - // Treba da preusmeriva na detalje licence - await expect(page).toHaveURL(/\/licenses\/\d+/); - await expect(page.locator('h1 code')).toBeVisible(); - // Proverava da je LT prefix - const licenseKey = await page.locator('h1 code').textContent(); - expect(licenseKey).toMatch(/^LT-/); + test('grace_days ima default 30', async ({ page }) => { + const val = await page.locator('input[name="grace_days"]').inputValue(); + expect(val).toBe('30'); }); - test('kreiranje ARV licence', async ({ page }) => { - await page.goto('/licenses/new'); - - const productSelect = page.locator('select[name="product_id"]'); - const options = productSelect.locator('option'); - const count = await options.count(); - for (let i = 0; i < count; i++) { - const text = await options.nth(i).textContent(); - if (text && text.includes('ARV')) { - await productSelect.selectOption({ index: i }); - break; - } - } - - await page.selectOption('select[name="license_type"]', 'ANNUAL'); - await page.fill('input[name="customer_name"]', 'E2E ARV Firma'); - await page.fill('input[name="customer_email"]', 'arv@test.rs'); - - await page.click('button:has-text("Kreiraj licencu")'); - - await expect(page).toHaveURL(/\/licenses\/\d+/); - const licenseKey = await page.locator('h1 code').textContent(); - expect(licenseKey).toMatch(/^ARV-/); + test('limits polje ima placeholder', async ({ page }) => { + await expect(page.locator('input[name="limits"]')).toHaveAttribute('placeholder', '{"max_operators": 3}'); }); - test('kreiranje ESIR licence', async ({ page }) => { - await page.goto('/licenses/new'); - - const productSelect = page.locator('select[name="product_id"]'); - const options = productSelect.locator('option'); - const count = await options.count(); - for (let i = 0; i < count; i++) { - const text = await options.nth(i).textContent(); - if (text && text.includes('ESIR')) { - await productSelect.selectOption({ index: i }); - break; - } - } - - await page.selectOption('select[name="license_type"]', 'PERPETUAL'); - await page.fill('input[name="customer_name"]', 'E2E ESIR Prodavnica'); - - await page.click('button:has-text("Kreiraj licencu")'); - - await expect(page).toHaveURL(/\/licenses\/\d+/); - const licenseKey = await page.locator('h1 code').textContent(); - expect(licenseKey).toMatch(/^ESIR-/); + test('email polje ima type email', async ({ page }) => { + await expect(page.locator('input[name="customer_email"]')).toHaveAttribute('type', 'email'); }); test('otkazi dugme vodi na listu licenci', async ({ page }) => { - await page.goto('/licenses/new'); await page.click('a:has-text("Otkazi")'); await expect(page).toHaveURL(/\/licenses$/); }); + + test('forma ima POST method i ispravnu action', async ({ page }) => { + const form = page.locator('form.form-card'); + await expect(form).toHaveAttribute('method', 'POST'); + await expect(form).toHaveAttribute('action', '/licenses'); + }); + + test('ima Kreiraj licencu dugme', async ({ page }) => { + await expect(page.locator('button:has-text("Kreiraj licencu")')).toBeVisible(); + await expect(page.locator('button:has-text("Kreiraj licencu")')).toHaveClass(/btn-primary/); + }); +}); + +test.describe('Kreiranje licence — svi proizvodi', () => { + test.beforeEach(async ({ page }) => { + await login(page); + }); + + test('kreiranje LIGHT_TICKET MONTHLY licence sa svim poljima', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'LIGHT_TICKET'); + await page.selectOption('select[name="license_type"]', 'MONTHLY'); + await page.fill('input[name="customer_name"]', 'E2E Komplet Test DOO'); + await page.fill('input[name="customer_pib"]', '111222333'); + await page.fill('input[name="customer_email"]', 'komplet@test.rs'); + await page.fill('input[name="limits"]', '{"max_operators": 5}'); + await page.fill('input[name="grace_days"]', '15'); + await page.fill('textarea[name="notes"]', 'Komplet E2E test sa svim poljima'); + + await page.click('button:has-text("Kreiraj licencu")'); + + await expect(page).toHaveURL(/\/licenses\/\d+/); + const key = await page.locator('h1 code').textContent(); + expect(key).toMatch(/^LT-/); + expect(key).toMatch(/^LT-[A-HJ-NP-Y2-9]{4}-[A-HJ-NP-Y2-9]{4}-[A-HJ-NP-Y2-9]{4}-[A-HJ-NP-Y2-9]{4}$/); + }); + + test('kreiranje LIGHT_TICKET PERPETUAL licence', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'LIGHT_TICKET'); + await page.selectOption('select[name="license_type"]', 'PERPETUAL'); + await page.fill('input[name="customer_name"]', 'E2E Perpetual LT'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + // Perpetual treba da prikaze Neograniceno za istek + await expect(page.locator('td', { hasText: 'Neograniceno' })).toBeVisible(); + }); + + test('kreiranje ARV ANNUAL licence', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'ARV'); + await page.selectOption('select[name="license_type"]', 'ANNUAL'); + await page.fill('input[name="customer_name"]', 'E2E ARV Godisnja'); + await page.fill('input[name="customer_email"]', 'arv@test.rs'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + const key = await page.locator('h1 code').textContent(); + expect(key).toMatch(/^ARV-/); + }); + + test('kreiranje ARV TRIAL licence', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'ARV'); + await page.selectOption('select[name="license_type"]', 'TRIAL'); + await page.fill('input[name="customer_name"]', 'E2E ARV Trial'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + }); + + test('kreiranje ESIR PERPETUAL licence', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'ESIR'); + await page.selectOption('select[name="license_type"]', 'PERPETUAL'); + await page.fill('input[name="customer_name"]', 'E2E ESIR Prodavnica'); + await page.fill('input[name="customer_pib"]', '555666777'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + const key = await page.locator('h1 code').textContent(); + expect(key).toMatch(/^ESIR-/); + }); + + test('kreiranje ESIR MONTHLY licence', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'ESIR'); + await page.selectOption('select[name="license_type"]', 'MONTHLY'); + await page.fill('input[name="customer_name"]', 'E2E ESIR Mesecna'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + }); + + test('kreirana licenca se pojavljuje u listi', async ({ page }) => { + const uniqueName = `E2E Lista Test ${Date.now()}`; + await page.goto('/licenses/new'); + await selectProduct(page, 'LIGHT_TICKET'); + await page.selectOption('select[name="license_type"]', 'MONTHLY'); + await page.fill('input[name="customer_name"]', uniqueName); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + + // Proveri da je u listi + await page.goto('/licenses'); + await expect(page.locator('td', { hasText: uniqueName })).toBeVisible(); + }); + + test('kreirana licenca ima status Aktivna', async ({ page }) => { + await page.goto('/licenses/new'); + await selectProduct(page, 'LIGHT_TICKET'); + await page.selectOption('select[name="license_type"]', 'MONTHLY'); + await page.fill('input[name="customer_name"]', 'E2E Status Check'); + await page.click('button:has-text("Kreiraj licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + await expect(page.locator('.badge.status-active')).toBeVisible(); + }); }); diff --git a/tests/e2e/license-detail.spec.ts b/tests/e2e/license-detail.spec.ts index 3f2dd2b..93579bc 100644 --- a/tests/e2e/license-detail.spec.ts +++ b/tests/e2e/license-detail.spec.ts @@ -7,111 +7,231 @@ async function login(page: Page) { await expect(page).toHaveURL(/\/dashboard/); } -async function createLicense(page: Page, customerName: string): Promise { +async function createLicense(page: Page, customerName: string, productCode = 'LIGHT_TICKET', licenseType = 'MONTHLY'): Promise { await page.goto('/licenses/new'); const productSelect = page.locator('select[name="product_id"]'); const options = productSelect.locator('option'); const count = await options.count(); for (let i = 0; i < count; i++) { const text = await options.nth(i).textContent(); - if (text && text.includes('LIGHT_TICKET')) { + if (text && text.includes(productCode)) { await productSelect.selectOption({ index: i }); break; } } - await page.selectOption('select[name="license_type"]', 'MONTHLY'); + await page.selectOption('select[name="license_type"]', licenseType); await page.fill('input[name="customer_name"]', customerName); await page.click('button:has-text("Kreiraj licencu")'); await expect(page).toHaveURL(/\/licenses\/\d+/); return page.url(); } -test.describe('Detalji licence', () => { +test.describe('Detalji licence — informacije', () => { test.beforeEach(async ({ page }) => { await login(page); }); - test('prikazuje informacije o licenci', async ({ page }) => { - const url = await createLicense(page, 'Detail Test Firma'); + test('prikazuje licencni kljuc u naslovu', async ({ page }) => { + await createLicense(page, 'Detail Info Test'); + await expect(page.locator('h1 code')).toBeVisible(); + const key = await page.locator('h1 code').textContent(); + expect(key).toMatch(/^LT-/); + }); + test('prikazuje status badge', async ({ page }) => { + await createLicense(page, 'Status Badge Test'); + await expect(page.locator('.page-header .badge')).toBeVisible(); + }); + + test('prikazuje sekciju Informacije', async ({ page }) => { + await createLicense(page, 'Info Section Test'); await expect(page.locator('h3', { hasText: 'Informacije' })).toBeVisible(); await expect(page.locator('.detail-table')).toBeVisible(); + }); - // Proverava osnovne podatke + test('prikazuje sve informacione polja', async ({ page }) => { + await createLicense(page, 'All Fields Test'); + const table = page.locator('.detail-table'); + await expect(table.locator('td', { hasText: 'Proizvod' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Tip' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Firma' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Izdata' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Istice' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Grace period' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Limiti' })).toBeVisible(); + await expect(table.locator('td', { hasText: 'Features' })).toBeVisible(); + }); + + test('prikazuje ispravne podatke za LT licencu', async ({ page }) => { + await createLicense(page, 'LT Data Check'); const detailText = await page.locator('.detail-table').textContent(); expect(detailText).toContain('LIGHT_TICKET'); expect(detailText).toContain('MONTHLY'); - expect(detailText).toContain('Detail Test Firma'); + expect(detailText).toContain('LT Data Check'); + expect(detailText).toContain('30 dana'); + }); + + test('prikazuje ispravne podatke za ARV licencu', async ({ page }) => { + await createLicense(page, 'ARV Data Check', 'ARV', 'ANNUAL'); + const detailText = await page.locator('.detail-table').textContent(); + expect(detailText).toContain('ARV'); + expect(detailText).toContain('ANNUAL'); + }); + + test('PERPETUAL prikazuje Neograniceno', async ({ page }) => { + await createLicense(page, 'Perpetual Check', 'ESIR', 'PERPETUAL'); + await expect(page.locator('td', { hasText: 'Neograniceno' })).toBeVisible(); + }); + + test('ima ispravan title sa kljucem', async ({ page }) => { + await createLicense(page, 'Title Check'); + const title = await page.title(); + expect(title).toContain('LT-'); + expect(title).toContain('DAL License Server'); + }); +}); + +test.describe('Detalji licence — aktivacije i audit', () => { + test.beforeEach(async ({ page }) => { + await login(page); }); test('prikazuje tabelu aktivacija', async ({ page }) => { - await createLicense(page, 'Activation Test Firma'); + await createLicense(page, 'Activation Table Test'); await expect(page.locator('h2', { hasText: 'Aktivacije' })).toBeVisible(); - // Nova licenca nema aktivacija + }); + + test('nova licenca nema aktivacija', async ({ page }) => { + await createLicense(page, 'No Activations Test'); await expect(page.locator('td', { hasText: 'Nema aktivacija' })).toBeVisible(); }); + test('tabela aktivacija ima ispravne kolone', async ({ page }) => { + await createLicense(page, 'Activation Cols Test'); + const table = page.locator('table').nth(1); // druga tabela + const headers = table.locator('thead th'); + await expect(headers.nth(0)).toHaveText('Hostname'); + await expect(headers.nth(1)).toHaveText('OS'); + await expect(headers.nth(2)).toHaveText('Verzija'); + await expect(headers.nth(3)).toHaveText('IP'); + }); + test('prikazuje audit log za licencu', async ({ page }) => { - await createLicense(page, 'Audit Test Firma'); + await createLicense(page, 'Audit Log Test'); await expect(page.locator('h2', { hasText: 'Audit Log' })).toBeVisible(); - // Kreiranje licence generise CREATE audit entry + }); + + test('kreiranje licence generise CREATE audit zapis', async ({ page }) => { + await createLicense(page, 'Create Audit Test'); await expect(page.locator('.badge', { hasText: 'CREATE' })).toBeVisible(); }); - test('prikazuje akcije (revoke, force release)', async ({ page }) => { - await createLicense(page, 'Actions Test Firma'); - await expect(page.locator('h3', { hasText: 'Akcije' })).toBeVisible(); - await expect(page.locator('button:has-text("Opozovi licencu")')).toBeVisible(); - await expect(page.locator('button:has-text("Force Release")')).toBeVisible(); + test('audit tabela ima ispravne kolone', async ({ page }) => { + await createLicense(page, 'Audit Cols Test'); + const table = page.locator('table').last(); + const headers = table.locator('thead th'); + await expect(headers.nth(0)).toHaveText('Vreme'); + await expect(headers.nth(1)).toHaveText('Akcija'); + await expect(headers.nth(2)).toHaveText('IP'); + await expect(headers.nth(3)).toHaveText('Detalji'); + }); +}); + +test.describe('Detalji licence — akcije', () => { + test.beforeEach(async ({ page }) => { + await login(page); }); - test('revoke licence funkcionise', async ({ page }) => { - await createLicense(page, 'Revoke Test Firma'); + test('prikazuje sekciju Akcije', async ({ page }) => { + await createLicense(page, 'Actions Section Test'); + await expect(page.locator('h3', { hasText: 'Akcije' })).toBeVisible(); + }); - // Prihvatamo confirm dialog + test('prikazuje dugme za opoziv', async ({ page }) => { + await createLicense(page, 'Revoke Button Test'); + await expect(page.locator('button:has-text("Opozovi licencu")')).toBeVisible(); + await expect(page.locator('button:has-text("Opozovi licencu")')).toHaveClass(/btn-danger/); + }); + + test('prikazuje dugme za force release', async ({ page }) => { + await createLicense(page, 'Release Button Test'); + await expect(page.locator('button:has-text("Force Release")')).toBeVisible(); + await expect(page.locator('button:has-text("Force Release")')).toHaveClass(/btn-warning/); + }); + + test('prikazuje polje za razlog opoziva', async ({ page }) => { + await createLicense(page, 'Reason Field Test'); + await expect(page.locator('input[name="reason"]')).toBeVisible(); + await expect(page.locator('input[name="reason"]')).toHaveAttribute('placeholder', 'Razlog opoziva'); + }); + + test('revoke licence menja status na Opozvana', async ({ page }) => { + await createLicense(page, 'Revoke Status Test'); page.on('dialog', dialog => dialog.accept()); - - await page.fill('input[name="reason"]', 'E2E test opozivanje'); + await page.fill('input[name="reason"]', 'E2E test razlog'); await page.click('button:has-text("Opozovi licencu")'); - - // Posle revoke-a ostajemo na istoj stranici await expect(page).toHaveURL(/\/licenses\/\d+/); - // Status treba biti Opozvana await expect(page.locator('.badge.status-revoked')).toBeVisible(); }); + test('revoke generise REVOKE audit zapis', async ({ page }) => { + await createLicense(page, 'Revoke Audit Test'); + page.on('dialog', dialog => dialog.accept()); + await page.fill('input[name="reason"]', 'Audit test'); + await page.click('button:has-text("Opozovi licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + await expect(page.locator('.badge', { hasText: 'REVOKE' })).toBeVisible(); + }); + test('posle revoke-a dugme za opoziv nestaje', async ({ page }) => { await createLicense(page, 'Revoke Hide Test'); - page.on('dialog', dialog => dialog.accept()); - await page.fill('input[name="reason"]', 'Test'); await page.click('button:has-text("Opozovi licencu")'); await expect(page).toHaveURL(/\/licenses\/\d+/); - - // Dugme za opoziv ne treba da postoji await expect(page.locator('button:has-text("Opozovi licencu")')).toHaveCount(0); - // Ali Force Release treba da postoji + }); + + test('posle revoke-a force release dugme ostaje', async ({ page }) => { + await createLicense(page, 'Revoke FR Test'); + page.on('dialog', dialog => dialog.accept()); + await page.click('button:has-text("Opozovi licencu")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page.locator('button:has-text("Force Release")')).toBeVisible(); }); test('force release funkcionise', async ({ page }) => { - await createLicense(page, 'Release Test Firma'); - + await createLicense(page, 'Force Release Test'); page.on('dialog', dialog => dialog.accept()); await page.click('button:has-text("Force Release")'); - await expect(page).toHaveURL(/\/licenses\/\d+/); }); + test('force release generise FORCE_RELEASE audit zapis', async ({ page }) => { + await createLicense(page, 'FR Audit Test'); + page.on('dialog', dialog => dialog.accept()); + await page.click('button:has-text("Force Release")'); + await expect(page).toHaveURL(/\/licenses\/\d+/); + await expect(page.locator('.badge', { hasText: 'FORCE_RELEASE' })).toBeVisible(); + }); +}); + +test.describe('Detalji licence — navigacija', () => { + test.beforeEach(async ({ page }) => { + await login(page); + }); + test('klik na licencu iz liste otvara detalje', async ({ page }) => { - await createLicense(page, 'Link Test Firma'); - + await createLicense(page, 'Navigation Test'); await page.goto('/licenses'); - // Klik na prvi link sa kodom licence - const licenseLink = page.locator('a:has(code)').first(); - await licenseLink.click(); - + const link = page.locator('a:has(code)').first(); + await link.click(); await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page.locator('h1 code')).toBeVisible(); }); + + test('navbar licence link je aktivan na detalj stranici', async ({ page }) => { + await createLicense(page, 'Navbar Active Test'); + const link = page.locator('a[href="/licenses"]'); + await expect(link).toHaveClass(/active/); + }); }); diff --git a/tests/e2e/licenses.spec.ts b/tests/e2e/licenses.spec.ts index f12575b..73e078d 100644 --- a/tests/e2e/licenses.spec.ts +++ b/tests/e2e/licenses.spec.ts @@ -7,21 +7,31 @@ async function login(page: Page) { await expect(page).toHaveURL(/\/dashboard/); } -test.describe('Lista licenci', () => { +test.describe('Lista licenci — stranica', () => { test.beforeEach(async ({ page }) => { await login(page); }); - test('prikazuje tabelu licenci', async ({ page }) => { + test('prikazuje naslov Licence', async ({ page }) => { await page.goto('/licenses'); await expect(page.locator('h1')).toHaveText('Licence'); - await expect(page.locator('table')).toBeVisible(); - // Tabela ima ispravne kolone + }); + + test('ima ispravan title', async ({ page }) => { + await page.goto('/licenses'); + await expect(page).toHaveTitle('Licence - DAL License Server'); + }); + + test('prikazuje tabelu sa ispravnim kolonama', async ({ page }) => { + await page.goto('/licenses'); const headers = page.locator('thead th'); await expect(headers.nth(0)).toHaveText('Kljuc'); await expect(headers.nth(1)).toHaveText('Proizvod'); await expect(headers.nth(2)).toHaveText('Firma'); await expect(headers.nth(3)).toHaveText('Tip'); + await expect(headers.nth(4)).toHaveText('Istice'); + await expect(headers.nth(5)).toHaveText('Aktivacija'); + await expect(headers.nth(6)).toHaveText('Status'); }); test('ima dugme za novu licencu', async ({ page }) => { @@ -29,33 +39,125 @@ test.describe('Lista licenci', () => { const btn = page.locator('a[href="/licenses/new"]'); await expect(btn).toBeVisible(); await expect(btn).toHaveText('Nova licenca'); + await expect(btn).toHaveClass(/btn-primary/); }); - test('filteri su prikazani', async ({ page }) => { + test('dugme nova licenca vodi na formu', async ({ page }) => { await page.goto('/licenses'); - await expect(page.locator('select[name="product"]')).toBeVisible(); - await expect(page.locator('select[name="status"]')).toBeVisible(); - await expect(page.locator('input[name="search"]')).toBeVisible(); + await page.click('a[href="/licenses/new"]'); + await expect(page).toHaveURL(/\/licenses\/new/); }); - test('filter po proizvodu radi', async ({ page }) => { + test('filter dropdown za proizvod sadrzi sve opcije', async ({ page }) => { + await page.goto('/licenses'); + const productSelect = page.locator('select[name="product"]'); + await expect(productSelect).toBeVisible(); + const options = await productSelect.locator('option').allTextContents(); + expect(options).toContain('Svi proizvodi'); + expect(options.some(o => o.includes('ESIR'))).toBe(true); + expect(options.some(o => o.includes('ARV'))).toBe(true); + expect(options.some(o => o.includes('Light-Ticket'))).toBe(true); + }); + + test('filter dropdown za status sadrzi sve opcije', async ({ page }) => { + await page.goto('/licenses'); + const statusSelect = page.locator('select[name="status"]'); + await expect(statusSelect).toBeVisible(); + const options = await statusSelect.locator('option').allTextContents(); + expect(options).toContain('Svi statusi'); + expect(options).toContain('Aktivne'); + expect(options).toContain('Istekle'); + expect(options).toContain('Opozvane'); + expect(options).toContain('Trial'); + }); + + test('pretraga polje postoji', async ({ page }) => { + await page.goto('/licenses'); + const search = page.locator('input[name="search"]'); + await expect(search).toBeVisible(); + await expect(search).toHaveAttribute('placeholder', 'Pretraga po firmi...'); + }); + + test('filtriraj dugme postoji', async ({ page }) => { + await page.goto('/licenses'); + await expect(page.locator('button:has-text("Filtriraj")')).toBeVisible(); + }); + + test('filter po proizvodu — ESIR', async ({ page }) => { await page.goto('/licenses'); await page.selectOption('select[name="product"]', 'ESIR'); await page.click('button:has-text("Filtriraj")'); await expect(page).toHaveURL(/product=ESIR/); }); - test('filter po statusu radi', async ({ page }) => { + test('filter po proizvodu — ARV', async ({ page }) => { + await page.goto('/licenses'); + await page.selectOption('select[name="product"]', 'ARV'); + await page.click('button:has-text("Filtriraj")'); + await expect(page).toHaveURL(/product=ARV/); + }); + + test('filter po proizvodu — LIGHT_TICKET', async ({ page }) => { + await page.goto('/licenses'); + await page.selectOption('select[name="product"]', 'LIGHT_TICKET'); + await page.click('button:has-text("Filtriraj")'); + await expect(page).toHaveURL(/product=LIGHT_TICKET/); + }); + + test('filter po statusu — active', async ({ page }) => { await page.goto('/licenses'); await page.selectOption('select[name="status"]', 'active'); await page.click('button:has-text("Filtriraj")'); await expect(page).toHaveURL(/status=active/); }); - test('pretraga po firmi radi', async ({ page }) => { + test('filter po statusu — expired', async ({ page }) => { + await page.goto('/licenses'); + await page.selectOption('select[name="status"]', 'expired'); + await page.click('button:has-text("Filtriraj")'); + await expect(page).toHaveURL(/status=expired/); + }); + + test('filter po statusu — revoked', async ({ page }) => { + await page.goto('/licenses'); + await page.selectOption('select[name="status"]', 'revoked'); + await page.click('button:has-text("Filtriraj")'); + await expect(page).toHaveURL(/status=revoked/); + }); + + test('pretraga po firmi', async ({ page }) => { await page.goto('/licenses'); await page.fill('input[name="search"]', 'TestFirma'); await page.click('button:has-text("Filtriraj")'); await expect(page).toHaveURL(/search=TestFirma/); }); + + test('kombinacija filtera — proizvod + status', async ({ page }) => { + await page.goto('/licenses'); + await page.selectOption('select[name="product"]', 'ESIR'); + await page.selectOption('select[name="status"]', 'active'); + await page.click('button:has-text("Filtriraj")'); + await expect(page).toHaveURL(/product=ESIR/); + await expect(page).toHaveURL(/status=active/); + }); + + test('filter cuva selekciju posle submit-a', async ({ page }) => { + await page.goto('/licenses?product=ESIR&status=active'); + const productVal = await page.locator('select[name="product"]').inputValue(); + const statusVal = await page.locator('select[name="status"]').inputValue(); + expect(productVal).toBe('ESIR'); + expect(statusVal).toBe('active'); + }); + + test('navbar licence link je aktivan', async ({ page }) => { + await page.goto('/licenses'); + const link = page.locator('a[href="/licenses"]'); + await expect(link).toHaveClass(/active/); + }); + + test('ako nema licenci prikazuje poruku', async ({ page }) => { + // Pretraga za nepostojeci termin + await page.goto('/licenses?search=NEPOSTOJECI_TERMIN_12345'); + await expect(page.locator('td', { hasText: 'Nema licenci' })).toBeVisible(); + }); }); diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts index 74c1518..48c670a 100644 --- a/tests/e2e/login.spec.ts +++ b/tests/e2e/login.spec.ts @@ -1,14 +1,29 @@ import { test, expect } from '@playwright/test'; -test.describe('Login', () => { - test('prikazuje login formu', async ({ page }) => { +test.describe('Login stranica', () => { + test('prikazuje login formu sa svim elementima', async ({ page }) => { await page.goto('/login'); await expect(page.locator('h1')).toHaveText('DAL License Server'); await expect(page.locator('input[name="password"]')).toBeVisible(); + await expect(page.locator('input[name="password"]')).toHaveAttribute('type', 'password'); + await expect(page.locator('input[name="password"]')).toHaveAttribute('required', ''); await expect(page.locator('button[type="submit"]')).toHaveText('Prijava'); + await expect(page.locator('label')).toHaveText('Lozinka'); }); - test('prijava sa ispravnom lozinkom', async ({ page }) => { + test('password polje ima autofocus', async ({ page }) => { + await page.goto('/login'); + await expect(page.locator('input[name="password"]')).toHaveAttribute('autofocus', ''); + }); + + test('forma ima ispravnu action i method', async ({ page }) => { + await page.goto('/login'); + const form = page.locator('form.login-form'); + await expect(form).toHaveAttribute('method', 'POST'); + await expect(form).toHaveAttribute('action', '/login'); + }); + + test('prijava sa ispravnom lozinkom preusmerava na dashboard', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="password"]', 'admin123'); await page.click('button[type="submit"]'); @@ -22,20 +37,88 @@ test.describe('Login', () => { await page.click('button[type="submit"]'); await expect(page.locator('.alert-error')).toHaveText('Pogresna lozinka'); await expect(page).toHaveURL(/\/login/); + // Password polje treba biti prazno posle greske + await expect(page.locator('input[name="password"]')).toBeVisible(); }); - test('pristup dashboard-u bez logina preusmerava na login', async ({ page }) => { + test('prijava sa praznom lozinkom — browser validacija', async ({ page }) => { + await page.goto('/login'); + // Ne popunjavamo password — klik na submit + await page.click('button[type="submit"]'); + // Ostajemo na login stranici (browser validation spreci submit) + await expect(page).toHaveURL(/\/login/); + }); + + test('prijava sa razlicitim pogresnim lozinkama', async ({ page }) => { + const pogresne = ['', ' ', 'admin', 'Admin123', 'ADMIN123', '12345', 'password']; + for (const lozinka of pogresne) { + if (lozinka === '') continue; // skip empty — browser validation + await page.goto('/login'); + await page.fill('input[name="password"]', lozinka); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/login/); + } + }); + + test('pristup /dashboard bez logina preusmerava na login', async ({ page }) => { await page.goto('/dashboard'); await expect(page).toHaveURL(/\/login/); }); - test('pristup licencama bez logina preusmerava na login', async ({ page }) => { + test('pristup /licenses bez logina preusmerava na login', async ({ page }) => { await page.goto('/licenses'); await expect(page).toHaveURL(/\/login/); }); - test('pristup auditu bez logina preusmerava na login', async ({ page }) => { + test('pristup /licenses/new bez logina preusmerava na login', async ({ page }) => { + await page.goto('/licenses/new'); + await expect(page).toHaveURL(/\/login/); + }); + + test('pristup /audit bez logina preusmerava na login', async ({ page }) => { await page.goto('/audit'); await expect(page).toHaveURL(/\/login/); }); + + test('pristup /licenses/1 bez logina preusmerava na login', async ({ page }) => { + await page.goto('/licenses/1'); + await expect(page).toHaveURL(/\/login/); + }); + + test('CSS je ucitan na login stranici', async ({ page }) => { + await page.goto('/login'); + const link = page.locator('link[rel="stylesheet"]'); + await expect(link).toHaveAttribute('href', '/static/css/style.css'); + }); + + test('login stranica ima ispravan title', async ({ page }) => { + await page.goto('/login'); + await expect(page).toHaveTitle('Prijava - DAL License Server'); + }); + + test('visestruki logini kreiraju razlicite sesije', async ({ page, context }) => { + // Login prvi put + await page.goto('/login'); + await page.fill('input[name="password"]', 'admin123'); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/dashboard/); + + const cookies1 = await context.cookies(); + const session1 = cookies1.find(c => c.name === 'dash_session'); + expect(session1).toBeDefined(); + + // Logout + await page.click('button:has-text("Odjava")'); + + // Login drugi put + await page.fill('input[name="password"]', 'admin123'); + await page.click('button[type="submit"]'); + + const cookies2 = await context.cookies(); + const session2 = cookies2.find(c => c.name === 'dash_session'); + expect(session2).toBeDefined(); + + // Sesije moraju biti razlicite + expect(session1!.value).not.toBe(session2!.value); + }); });