package l2tp import ( "net/netip" "strings" "testing" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" ) func TestValidateDefaults(t *testing.T) { cfg, err := New().Validate(map[string]interface{}{}) if err != nil { t.Fatal(err) } defaults := []struct { key string expected string }{ {"ip_range", "10.9.0.100-10.9.0.200"}, {"local_ip", "10.9.0.1"}, {"ike_proposal", "aes256-sha256-modp2048,aes128-sha1-modp1024"}, {"esp_proposal", "aes256-sha256,aes128-sha1"}, } for _, d := range defaults { if got := service.Str(cfg, d.key); got != d.expected { t.Errorf("%s: got %q, want %q", d.key, got, d.expected) } } if service.Str(cfg, "dns") == "" { t.Error("dns default should not be empty") } if service.Str(cfg, "client_subnet") == "" { t.Error("client_subnet default should not be empty") } // Verify client_subnet covers the ip_range prefix, err := netip.ParsePrefix(service.Str(cfg, "client_subnet")) if err != nil { t.Fatalf("client_subnet parse: %v", err) } start, _, _ := parseIPRange(service.Str(cfg, "ip_range")) if !prefix.Contains(start) { t.Errorf("client_subnet %s does not contain start of ip_range %s", prefix, start) } } func TestValidateRejectsInvalid(t *testing.T) { s := New() cases := []struct { name string cfg map[string]interface{} }{ {"invalid local_ip", map[string]interface{}{ "local_ip": "not-an-ip", }}, {"invalid ip_range format", map[string]interface{}{ "ip_range": "10.0.0.100", }}, {"ip_range start > end", map[string]interface{}{ "ip_range": "10.9.0.200-10.9.0.100", }}, {"local_ip in ip_range", map[string]interface{}{ "local_ip": "10.9.0.150", "ip_range": "10.9.0.100-10.9.0.200", }}, {"client_subnet too small", map[string]interface{}{ "ip_range": "10.9.0.100-10.9.0.200", "client_subnet": "10.9.0.0/30", }}, {"bad username whitespace", map[string]interface{}{ "username": "bad user", }}, } for _, tc := range cases { if _, err := s.Validate(tc.cfg); err == nil { t.Errorf("%s: expected error, got nil", tc.name) } } } func TestBuildChapSecrets(t *testing.T) { // With default user only secrets := buildChapSecrets("vpnuser", "s3cret", nil) if !strings.Contains(secrets, "vpnuser") || !strings.Contains(secrets, "s3cret") { t.Errorf("chap-secrets missing default user:\n%s", secrets) } if !strings.Contains(secrets, "l2tpd") { t.Errorf("chap-secrets missing server name:\n%s", secrets) } // With dial users users := []service.DialUser{ {Username: "alice", Password: "pw1"}, {Username: "bob", Password: "pw2"}, } secrets = buildChapSecrets("admin", "pass", users) for _, u := range []string{"admin", "alice", "bob"} { if !strings.Contains(secrets, u) { t.Errorf("chap-secrets missing user %q:\n%s", u, secrets) } } // Duplicate user should be deduplicated secrets = buildChapSecrets("alice", "override", []service.DialUser{{Username: "alice", Password: "ignored"}}) count := strings.Count(secrets, "alice") if count != 1 { t.Errorf("expected alice once, got %d times in:\n%s", count, secrets) } // Empty default user — dial users still appear secrets = buildChapSecrets("", "", []service.DialUser{{Username: "x", Password: "y"}}) if !strings.Contains(secrets, "x") { t.Errorf("chap-secrets should still include dial users:\n%s", secrets) } if strings.Contains(secrets, `""`) { t.Errorf("empty default user should not produce empty-quoted entry:\n%s", secrets) } } func TestParseIPRange(t *testing.T) { cases := []struct { input string wantErr bool start string end string }{ {"10.0.0.1-10.0.0.100", false, "10.0.0.1", "10.0.0.100"}, {"10.0.0.1", true, "", ""}, {"10.0.0.1-10.0.0.2-10.0.0.3", true, "", ""}, {"not-ip-also-not", true, "", ""}, } for _, tc := range cases { start, end, err := parseIPRange(tc.input) if tc.wantErr { if err == nil { t.Errorf("parseIPRange(%q): expected error", tc.input) } continue } if err != nil { t.Errorf("parseIPRange(%q): unexpected error: %v", tc.input, err) continue } if start.String() != tc.start || end.String() != tc.end { t.Errorf("parseIPRange(%q): got %s-%s, want %s-%s", tc.input, start, end, tc.start, tc.end) } } } func TestEnsureExclusiveListener(t *testing.T) { dataDir := t.TempDir() t.Setenv("VPN_DATA_DIR", dataDir) if err := store.Init(dataDir); err != nil { t.Fatalf("store.Init: %v", err) } // Create a running L2TP tunnel in DB running := &model.Tunnel{ Name: "l2tp-existing", Protocol: "l2tp", State: "running", } if err := store.DB.Create(running).Error; err != nil { t.Fatal(err) } s := &Service{} newTunnel := &model.Tunnel{ID: running.ID + 1} if err := s.ensureExclusiveListener(newTunnel); err == nil { t.Fatal("expected error when another L2TP instance is running") } // After marking the existing one as stopped, it should pass store.DB.Model(running).Update("state", "stopped") if err := s.ensureExclusiveListener(newTunnel); err != nil { t.Fatalf("expected no error after existing stopped: %v", err) } }