Vlasnik izlazi iz DAL price za ovaj projekat - postaje samostalan proizvod. Preimenovanje je mehanicko (import putanje, imena fajlova/baze/UI naslova), NIJEDNA linija poslovne logike u internal/handler ili internal/service nije dirana - go build, go vet i go test ./... prolaze cisto pod novim imenom. - go.mod: module dal-license-server -> licence-server; 28 import linija u 13 .go fajlova usaglaseno - internal/config/config.go (+test): default DB_NAME licence_db - HTML sabloni (7 stranica) + 7 Playwright e2e spec fajlova: "DAL License Server" -> "Licence Server" u title/nav-brand, testovi usaglaseni u istom prolazu (nista ne moze da se razdvoji) - package.json/package-lock.json/.gitignore/.claude/project.json: ime projekta/binarnog fajla/work_dir - Usput: package.json je u repository.url nosio OTVORENU Gitea lozinku u cistom tekstu - uklonjena (URL sad bez kredencijala). Lozinka je i dalje u staroj git istoriji - preporuka: rotirati je posebno. - CLAUDE.md/README/API/TESTING/docs/SPEC/ARCHITECTURE/SETUP: naslovi, ASCII dijagrami, git clone URL, mysqldump primer, systemd predlozak u SETUP.md zamenjen stvarnim (obrazac terminia.service - journal log + graceful shutdown, ne stari minimalni predlozak) - Debrendiranje: "Univerzalni licencni server za sve DAL proizvode" -> "za vise proizvoda i klijenata"; potpis "Nenad Djukic / DAL d.o.o." -> "Nenad Djukic". NE dirano: nazivi ESIR/ARV/LIGHT_TICKET (tudji proizvodi, ne DAL brend), "DAL" kao stvaran naziv org-a u RBAC modelu (docs/loggerservice/README.md) - to je podatak, ne branding ovog servera. - docs/asp-terminia/, docs/loggerservice/: prozni pomeni imena servera Baza (dal_license_db -> licence_db) i deploy na 151 idu u posebnom koraku, posle preimenovanja Gitea repoa - vidi CLAUDE.md istorijsku belesku na dnu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
216 lines
5.5 KiB
Go
216 lines
5.5 KiB
Go
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 != "licence_db" {
|
|
t.Errorf("DBName default: ocekivano 'licence_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
|
|
}
|