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 <noreply@anthropic.com>
This commit is contained in:
djuka 2026-03-04 07:58:24 +00:00
parent e7823e9273
commit 1b8db5e4a7
9 changed files with 1388 additions and 258 deletions

View File

@ -1,105 +1,258 @@
# DAL License Server — Test Checklista # DAL License Server — Test Checklista
## Health ## Ukupno testova: 179
- [ ] GET /api/v1/health vraca {"status":"ok"} - Go unit testovi: 46
- Playwright E2E testovi: 133
## Proizvodi ## Pokretanje testova
- [ ] GET /api/v1/admin/products vraca 3 proizvoda (ESIR, ARV, LIGHT_TICKET)
## Kreiranje licence ```bash
- [ ] Kreiranje ESIR licence sa svim poljima # Go unit testovi
- [ ] Kreiranje ARV licence sa svim poljima go test ./internal/... -v -count=1
- [ ] 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
## Lista licenci # Playwright E2E testovi (server mora biti pokrenut na :8090)
- [ ] GET /api/v1/admin/licenses vraca sve licence npx playwright test
- [ ] Filter po proizvodu radi ispravno
## Detalji licence # Sve zajedno
- [ ] GET /api/v1/admin/licenses/{id} vraca sve podatke go test ./internal/... -v -count=1 && npx playwright test
- [ ] 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
--- ---
*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)*

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,5 +1,8 @@
import { test, expect, Page } from '@playwright/test'; 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) { async function login(page: Page) {
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="password"]', 'admin123'); await page.fill('input[name="password"]', 'admin123');
@ -7,20 +10,30 @@ async function login(page: Page) {
await expect(page).toHaveURL(/\/dashboard/); await expect(page).toHaveURL(/\/dashboard/);
} }
test.describe('Audit Log', () => { test.describe('Audit Log stranica', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await login(page); await login(page);
}); });
test('prikazuje audit log stranicu', async ({ page }) => { test('prikazuje naslov Audit Log', async ({ page }) => {
await page.goto('/audit'); await page.goto('/audit');
await expect(page.locator('h1')).toHaveText('Audit Log'); 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(); await expect(page.locator('table')).toBeVisible();
}); });
test('tabela ima ispravne kolone', async ({ page }) => { test('tabela ima 5 kolona', async ({ page }) => {
await page.goto('/audit'); await page.goto('/audit');
const headers = page.locator('thead th'); const headers = page.locator('thead th');
await expect(headers).toHaveCount(5);
await expect(headers.nth(0)).toHaveText('Vreme'); await expect(headers.nth(0)).toHaveText('Vreme');
await expect(headers.nth(1)).toHaveText('Akcija'); await expect(headers.nth(1)).toHaveText('Akcija');
await expect(headers.nth(2)).toHaveText('Licenca'); await expect(headers.nth(2)).toHaveText('Licenca');
@ -28,7 +41,18 @@ test.describe('Audit Log', () => {
await expect(headers.nth(4)).toHaveText('Detalji'); 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 // Kreiraj licencu
await page.goto('/licenses/new'); await page.goto('/licenses/new');
const productSelect = page.locator('select[name="product_id"]'); 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.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 page.click('button:has-text("Kreiraj licencu")');
await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page).toHaveURL(/\/licenses\/\d+/);
// Proveri audit log // Proveri audit
await page.goto('/audit'); await page.goto('/audit');
await expect(page.locator('.badge', { hasText: 'CREATE' }).first()).toBeVisible(); await expect(page.locator('.badge', { hasText: 'CREATE' }).first()).toBeVisible();
}); });
test('navigacija na audit iz navbara', async ({ page }) => { test('audit prikazuje licencni kljuc', async ({ page }) => {
await page.click('a[href="/audit"]'); await page.goto('/audit');
await expect(page).toHaveURL(/\/audit/); // Audit zapisi koji imaju licencu prikazuju code element
await expect(page.locator('h1')).toHaveText('Audit Log'); 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();
}); });
}); });

View File

@ -7,41 +7,112 @@ async function login(page: Page) {
await expect(page).toHaveURL(/\/dashboard/); await expect(page).toHaveURL(/\/dashboard/);
} }
test.describe('Dashboard', () => { test.describe('Dashboard stranica', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await login(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('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('h2', { hasText: 'Poslednja aktivnost' })).toBeVisible();
await expect(page.locator('table').last()).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('.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(); 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(); 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 page.click('button:has-text("Odjava")');
await expect(page).toHaveURL(/\/login/); 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 page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/); await expect(page).toHaveURL(/\/login/);
}); });
test('root preusmerava na dashboard', async ({ page }) => { test('root (/) preusmerava na dashboard', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await expect(page).toHaveURL(/\/dashboard/); 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);
});
}); });

View File

@ -7,14 +7,34 @@ async function login(page: Page) {
await expect(page).toHaveURL(/\/dashboard/); 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 }) => { test.beforeEach(async ({ page }) => {
await login(page); await login(page);
await page.goto('/licenses/new');
}); });
test('forma za novu licencu prikazuje sva polja', async ({ page }) => { test('prikazuje naslov Nova licenca', async ({ page }) => {
await page.goto('/licenses/new');
await expect(page.locator('h1')).toHaveText('Nova licenca'); 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="product_id"]')).toBeVisible();
await expect(page.locator('select[name="license_type"]')).toBeVisible(); await expect(page.locator('select[name="license_type"]')).toBeVisible();
await expect(page.locator('input[name="customer_name"]')).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(); await expect(page.locator('textarea[name="notes"]')).toBeVisible();
}); });
test('dropdown ima sve proizvode', async ({ page }) => { test('customer_name je obavezan', async ({ page }) => {
await page.goto('/licenses/new'); 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 options = page.locator('select[name="product_id"] option');
const count = await options.count(); 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 }) => { test('dropdown ima sve tipove licenci', async ({ page }) => {
await page.goto('/licenses/new');
const options = page.locator('select[name="license_type"] option'); const options = page.locator('select[name="license_type"] option');
const texts = await options.allTextContents(); const texts = await options.allTextContents();
expect(texts.join(' ')).toContain('Mesecna'); expect(texts.join(' ')).toContain('Mesecna');
@ -42,91 +76,134 @@ test.describe('Kreiranje licence', () => {
expect(texts.join(' ')).toContain('Trial'); expect(texts.join(' ')).toContain('Trial');
}); });
test('kreiranje LIGHT_TICKET licence', async ({ page }) => { test('grace_days ima default 30', async ({ page }) => {
await page.goto('/licenses/new'); const val = await page.locator('input[name="grace_days"]').inputValue();
expect(val).toBe('30');
// 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('kreiranje ARV licence', async ({ page }) => { test('limits polje ima placeholder', async ({ page }) => {
await page.goto('/licenses/new'); await expect(page.locator('input[name="limits"]')).toHaveAttribute('placeholder', '{"max_operators": 3}');
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('kreiranje ESIR licence', async ({ page }) => { test('email polje ima type email', async ({ page }) => {
await page.goto('/licenses/new'); await expect(page.locator('input[name="customer_email"]')).toHaveAttribute('type', 'email');
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('otkazi dugme vodi na listu licenci', async ({ page }) => { test('otkazi dugme vodi na listu licenci', async ({ page }) => {
await page.goto('/licenses/new');
await page.click('a:has-text("Otkazi")'); await page.click('a:has-text("Otkazi")');
await expect(page).toHaveURL(/\/licenses$/); 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();
});
}); });

View File

@ -7,111 +7,231 @@ async function login(page: Page) {
await expect(page).toHaveURL(/\/dashboard/); await expect(page).toHaveURL(/\/dashboard/);
} }
async function createLicense(page: Page, customerName: string): Promise<string> { async function createLicense(page: Page, customerName: string, productCode = 'LIGHT_TICKET', licenseType = 'MONTHLY'): Promise<string> {
await page.goto('/licenses/new'); await page.goto('/licenses/new');
const productSelect = page.locator('select[name="product_id"]'); const productSelect = page.locator('select[name="product_id"]');
const options = productSelect.locator('option'); const options = productSelect.locator('option');
const count = await options.count(); const count = await options.count();
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const text = await options.nth(i).textContent(); const text = await options.nth(i).textContent();
if (text && text.includes('LIGHT_TICKET')) { if (text && text.includes(productCode)) {
await productSelect.selectOption({ index: i }); await productSelect.selectOption({ index: i });
break; 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.fill('input[name="customer_name"]', customerName);
await page.click('button:has-text("Kreiraj licencu")'); await page.click('button:has-text("Kreiraj licencu")');
await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page).toHaveURL(/\/licenses\/\d+/);
return page.url(); return page.url();
} }
test.describe('Detalji licence', () => { test.describe('Detalji licence — informacije', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await login(page); await login(page);
}); });
test('prikazuje informacije o licenci', async ({ page }) => { test('prikazuje licencni kljuc u naslovu', async ({ page }) => {
const url = await createLicense(page, 'Detail Test Firma'); 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('h3', { hasText: 'Informacije' })).toBeVisible();
await expect(page.locator('.detail-table')).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(); const detailText = await page.locator('.detail-table').textContent();
expect(detailText).toContain('LIGHT_TICKET'); expect(detailText).toContain('LIGHT_TICKET');
expect(detailText).toContain('MONTHLY'); 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 }) => { 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(); 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(); 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 }) => { 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(); 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(); await expect(page.locator('.badge', { hasText: 'CREATE' })).toBeVisible();
}); });
test('prikazuje akcije (revoke, force release)', async ({ page }) => { test('audit tabela ima ispravne kolone', async ({ page }) => {
await createLicense(page, 'Actions Test Firma'); await createLicense(page, 'Audit Cols Test');
await expect(page.locator('h3', { hasText: 'Akcije' })).toBeVisible(); const table = page.locator('table').last();
await expect(page.locator('button:has-text("Opozovi licencu")')).toBeVisible(); const headers = table.locator('thead th');
await expect(page.locator('button:has-text("Force Release")')).toBeVisible(); 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('revoke licence funkcionise', async ({ page }) => { test.describe('Detalji licence — akcije', () => {
await createLicense(page, 'Revoke Test Firma'); test.beforeEach(async ({ page }) => {
await login(page);
});
// Prihvatamo confirm dialog test('prikazuje sekciju Akcije', async ({ page }) => {
await createLicense(page, 'Actions Section Test');
await expect(page.locator('h3', { hasText: 'Akcije' })).toBeVisible();
});
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()); page.on('dialog', dialog => dialog.accept());
await page.fill('input[name="reason"]', 'E2E test razlog');
await page.fill('input[name="reason"]', 'E2E test opozivanje');
await page.click('button:has-text("Opozovi licencu")'); await page.click('button:has-text("Opozovi licencu")');
// Posle revoke-a ostajemo na istoj stranici
await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page).toHaveURL(/\/licenses\/\d+/);
// Status treba biti Opozvana
await expect(page.locator('.badge.status-revoked')).toBeVisible(); 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 }) => { test('posle revoke-a dugme za opoziv nestaje', async ({ page }) => {
await createLicense(page, 'Revoke Hide Test'); await createLicense(page, 'Revoke Hide Test');
page.on('dialog', dialog => dialog.accept()); page.on('dialog', dialog => dialog.accept());
await page.fill('input[name="reason"]', 'Test');
await page.click('button:has-text("Opozovi licencu")'); await page.click('button:has-text("Opozovi licencu")');
await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page).toHaveURL(/\/licenses\/\d+/);
// Dugme za opoziv ne treba da postoji
await expect(page.locator('button:has-text("Opozovi licencu")')).toHaveCount(0); 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(); await expect(page.locator('button:has-text("Force Release")')).toBeVisible();
}); });
test('force release funkcionise', async ({ page }) => { test('force release funkcionise', async ({ page }) => {
await createLicense(page, 'Release Test Firma'); await createLicense(page, 'Force Release Test');
page.on('dialog', dialog => dialog.accept()); page.on('dialog', dialog => dialog.accept());
await page.click('button:has-text("Force Release")'); await page.click('button:has-text("Force Release")');
await expect(page).toHaveURL(/\/licenses\/\d+/); 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 }) => { 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'); await page.goto('/licenses');
// Klik na prvi link sa kodom licence const link = page.locator('a:has(code)').first();
const licenseLink = page.locator('a:has(code)').first(); await link.click();
await licenseLink.click();
await expect(page).toHaveURL(/\/licenses\/\d+/); await expect(page).toHaveURL(/\/licenses\/\d+/);
await expect(page.locator('h1 code')).toBeVisible(); 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/);
});
}); });

View File

@ -7,21 +7,31 @@ async function login(page: Page) {
await expect(page).toHaveURL(/\/dashboard/); await expect(page).toHaveURL(/\/dashboard/);
} }
test.describe('Lista licenci', () => { test.describe('Lista licenci — stranica', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await login(page); await login(page);
}); });
test('prikazuje tabelu licenci', async ({ page }) => { test('prikazuje naslov Licence', async ({ page }) => {
await page.goto('/licenses'); await page.goto('/licenses');
await expect(page.locator('h1')).toHaveText('Licence'); 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'); const headers = page.locator('thead th');
await expect(headers.nth(0)).toHaveText('Kljuc'); await expect(headers.nth(0)).toHaveText('Kljuc');
await expect(headers.nth(1)).toHaveText('Proizvod'); await expect(headers.nth(1)).toHaveText('Proizvod');
await expect(headers.nth(2)).toHaveText('Firma'); await expect(headers.nth(2)).toHaveText('Firma');
await expect(headers.nth(3)).toHaveText('Tip'); 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 }) => { test('ima dugme za novu licencu', async ({ page }) => {
@ -29,33 +39,125 @@ test.describe('Lista licenci', () => {
const btn = page.locator('a[href="/licenses/new"]'); const btn = page.locator('a[href="/licenses/new"]');
await expect(btn).toBeVisible(); await expect(btn).toBeVisible();
await expect(btn).toHaveText('Nova licenca'); 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 page.goto('/licenses');
await expect(page.locator('select[name="product"]')).toBeVisible(); await page.click('a[href="/licenses/new"]');
await expect(page.locator('select[name="status"]')).toBeVisible(); await expect(page).toHaveURL(/\/licenses\/new/);
await expect(page.locator('input[name="search"]')).toBeVisible();
}); });
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.goto('/licenses');
await page.selectOption('select[name="product"]', 'ESIR'); await page.selectOption('select[name="product"]', 'ESIR');
await page.click('button:has-text("Filtriraj")'); await page.click('button:has-text("Filtriraj")');
await expect(page).toHaveURL(/product=ESIR/); 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.goto('/licenses');
await page.selectOption('select[name="status"]', 'active'); await page.selectOption('select[name="status"]', 'active');
await page.click('button:has-text("Filtriraj")'); await page.click('button:has-text("Filtriraj")');
await expect(page).toHaveURL(/status=active/); 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.goto('/licenses');
await page.fill('input[name="search"]', 'TestFirma'); await page.fill('input[name="search"]', 'TestFirma');
await page.click('button:has-text("Filtriraj")'); await page.click('button:has-text("Filtriraj")');
await expect(page).toHaveURL(/search=TestFirma/); 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();
});
}); });

View File

@ -1,14 +1,29 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test.describe('Login', () => { test.describe('Login stranica', () => {
test('prikazuje login formu', async ({ page }) => { test('prikazuje login formu sa svim elementima', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
await expect(page.locator('h1')).toHaveText('DAL License Server'); await expect(page.locator('h1')).toHaveText('DAL License Server');
await expect(page.locator('input[name="password"]')).toBeVisible(); 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('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.goto('/login');
await page.fill('input[name="password"]', 'admin123'); await page.fill('input[name="password"]', 'admin123');
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
@ -22,20 +37,88 @@ test.describe('Login', () => {
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await expect(page.locator('.alert-error')).toHaveText('Pogresna lozinka'); await expect(page.locator('.alert-error')).toHaveText('Pogresna lozinka');
await expect(page).toHaveURL(/\/login/); 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 page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/); 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 page.goto('/licenses');
await expect(page).toHaveURL(/\/login/); 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 page.goto('/audit');
await expect(page).toHaveURL(/\/login/); 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);
});
}); });