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:
parent
e7823e9273
commit
1b8db5e4a7
345
TESTING.md
345
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)*
|
||||
|
||||
215
internal/config/config_test.go
Normal file
215
internal/config/config_test.go
Normal 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
|
||||
}
|
||||
178
internal/handler/helpers_test.go
Normal file
178
internal/handler/helpers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,111 +7,231 @@ async function login(page: Page) {
|
||||
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');
|
||||
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('revoke licence funkcionise', async ({ page }) => {
|
||||
await createLicense(page, 'Revoke Test Firma');
|
||||
test.describe('Detalji licence — akcije', () => {
|
||||
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());
|
||||
|
||||
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/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user