package service import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "net" "testing" "github.com/glebarez/sqlite" "gorm.io/gorm" "wails-app/internal/dao" ) func TestValidateAddressRejectsUnsafeInputs(t *testing.T) { tests := []struct { name string ip string port int }{ {name: "ipv4 loopback", ip: "127.0.0.1", port: 8443}, {name: "localhost", ip: "localhost", port: 8443}, {name: "ipv4 unspecified", ip: "0.0.0.0", port: 8443}, {name: "limited broadcast", ip: "255.255.255.255", port: 8443}, {name: "ipv4 multicast", ip: "239.1.2.3", port: 8443}, {name: "ipv6 multicast", ip: "ff02::1", port: 8443}, {name: "ipv6 loopback", ip: "::1", port: 8443}, {name: "ipv4 mapped loopback", ip: "::ffff:127.0.0.1", port: 8443}, {name: "ipv4 mapped unspecified", ip: "::ffff:0.0.0.0", port: 8443}, {name: "port zero", ip: "192.0.2.10", port: 0}, {name: "port exceeds range", ip: "192.0.2.10", port: 65536}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if _, err := ValidateAddress(test.ip, test.port); err == nil { t.Fatalf("ValidateAddress(%q, %d) should be rejected", test.ip, test.port) } }) } } func TestFindProvisionReaderReusesSoftDeletedSameDevice(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&dao.UHFReader{}); err != nil { t.Fatal(err) } row := &dao.UHFReader{DeviceCode: "MOCK-GATE-001", DeviceID: "MOCK-EDGE-001", DeviceName: "old", DeviceType: "gate"} if err := db.Create(row).Error; err != nil { t.Fatal(err) } if err := db.Delete(row).Error; err != nil { t.Fatal(err) } found, ok, err := findProvisionReader(db, "MOCK-EDGE-001", "MOCK-GATE-001") if err != nil || !ok || found.ID != row.ID || !found.DeletedAt.Valid { t.Fatalf("expected soft-deleted row to be reused, row=%+v found=%v ok=%v err=%v", row, found, ok, err) } found.DeletedAt = gorm.DeletedAt{} found.DeviceName = "restored" if err := db.Unscoped().Save(&found).Error; err != nil { t.Fatal(err) } var active dao.UHFReader if err := db.First(&active, row.ID).Error; err != nil { t.Fatal(err) } if active.DeviceName != "restored" { t.Fatalf("expected restored row, got %+v", active) } } func TestFindProvisionReaderRejectsCodeOwnedByOtherDevice(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&dao.UHFReader{}); err != nil { t.Fatal(err) } if err := db.Create(&dao.UHFReader{DeviceCode: "MOCK-GATE-001", DeviceID: "OTHER-EDGE", DeviceName: "other", DeviceType: "gate"}).Error; err != nil { t.Fatal(err) } if _, ok, err := findProvisionReader(db, "MOCK-EDGE-001", "MOCK-GATE-001"); err == nil || ok { t.Fatalf("expected code conflict, ok=%v err=%v", ok, err) } } func TestValidateDiscoveryResponse(t *testing.T) { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } keyHash := sha256.Sum256(publicKey) request := discoveryRequestPacket{Schema: "discovery.request.v1", RequestID: "request-1", Nonce: "nonce-1"} packet := discoveryResponsePacket{ Schema: "discovery.response.v1", RequestID: request.RequestID, Nonce: request.Nonce, DeviceID: "SN-1", DeviceCode: "EDGE-1", DeviceType: "lpr-gate", DeviceModel: "LPR-200", FirmwareVersion: "2.1.0", IP: "192.0.2.10", ConfigURL: "https://192.0.2.10:8443/api/v1/provision", PublicKeyFingerprint: "SHA256:" + fmt.Sprintf("%x", keyHash), SigningPublicKey: base64.StdEncoding.EncodeToString(publicKey), } packet.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, discoverySigningPayload(packet))) raw, err := json.Marshal(packet) if err != nil { t.Fatal(err) } result, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request) if err != nil { t.Fatalf("expected valid discovery response: %v", err) } if !result.SignatureVerified || result.ConfigPort != 8443 || result.DeviceID != "SN-1" { t.Fatalf("unexpected discovery result: %#v", result) } packet.Signature = base64.StdEncoding.EncodeToString([]byte("invalid")) raw, _ = json.Marshal(packet) if _, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request); err == nil { t.Fatal("invalid signature must be rejected") } packet.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, discoverySigningPayload(packet))) packet.Nonce = "unexpected-nonce" raw, _ = json.Marshal(packet) if _, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request); err == nil { t.Fatal("response with a different nonce must be rejected") } } func TestDiscoveryBroadcastTargetsAcceptsExplicitIPv4(t *testing.T) { targets, err := discoveryBroadcastTargets("192.168.10.255") if err != nil || len(targets) != 1 || !targets[0].Equal(net.ParseIP("192.168.10.255")) { t.Fatalf("unexpected broadcast targets: %#v, %v", targets, err) } if _, err := discoveryBroadcastTargets("ff02::1"); err == nil { t.Fatal("IPv6 multicast must not be accepted as an IPv4 UDP discovery target") } } func TestValidateAddressAllowsUnicastIPs(t *testing.T) { for _, rawIP := range []string{"192.0.2.10", "2001:db8::10"} { ip, err := ValidateAddress(rawIP, 8443) if err != nil { t.Fatalf("ValidateAddress(%q) returned error: %v", rawIP, err) } if !ip.Equal(net.ParseIP(rawIP)) { t.Fatalf("ValidateAddress(%q) returned %v", rawIP, ip) } } } func TestDirectedBroadcastCheck(t *testing.T) { network := net.ParseIP("192.168.50.12") mask := net.CIDRMask(24, 32) if !isDirectedBroadcastForNetwork(net.ParseIP("192.168.50.255"), network, mask) { t.Fatal("expected subnet broadcast address to be rejected") } if isDirectedBroadcastForNetwork(net.ParseIP("192.168.50.26"), network, mask) { t.Fatal("ordinary unicast address must not be classified as broadcast") } }