service_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package service
  2. import (
  3. "crypto/ed25519"
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/json"
  8. "fmt"
  9. "net"
  10. "testing"
  11. "github.com/glebarez/sqlite"
  12. "gorm.io/gorm"
  13. "wails-app/internal/dao"
  14. )
  15. func TestValidateAddressRejectsUnsafeInputs(t *testing.T) {
  16. tests := []struct {
  17. name string
  18. ip string
  19. port int
  20. }{
  21. {name: "ipv4 loopback", ip: "127.0.0.1", port: 8443},
  22. {name: "localhost", ip: "localhost", port: 8443},
  23. {name: "ipv4 unspecified", ip: "0.0.0.0", port: 8443},
  24. {name: "limited broadcast", ip: "255.255.255.255", port: 8443},
  25. {name: "ipv4 multicast", ip: "239.1.2.3", port: 8443},
  26. {name: "ipv6 multicast", ip: "ff02::1", port: 8443},
  27. {name: "ipv6 loopback", ip: "::1", port: 8443},
  28. {name: "ipv4 mapped loopback", ip: "::ffff:127.0.0.1", port: 8443},
  29. {name: "ipv4 mapped unspecified", ip: "::ffff:0.0.0.0", port: 8443},
  30. {name: "port zero", ip: "192.0.2.10", port: 0},
  31. {name: "port exceeds range", ip: "192.0.2.10", port: 65536},
  32. }
  33. for _, test := range tests {
  34. t.Run(test.name, func(t *testing.T) {
  35. if _, err := ValidateAddress(test.ip, test.port); err == nil {
  36. t.Fatalf("ValidateAddress(%q, %d) should be rejected", test.ip, test.port)
  37. }
  38. })
  39. }
  40. }
  41. func TestFindProvisionReaderReusesSoftDeletedSameDevice(t *testing.T) {
  42. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  43. if err != nil {
  44. t.Fatal(err)
  45. }
  46. if err := db.AutoMigrate(&dao.UHFReader{}); err != nil {
  47. t.Fatal(err)
  48. }
  49. row := &dao.UHFReader{DeviceCode: "MOCK-GATE-001", DeviceID: "MOCK-EDGE-001", DeviceName: "old", DeviceType: "gate"}
  50. if err := db.Create(row).Error; err != nil {
  51. t.Fatal(err)
  52. }
  53. if err := db.Delete(row).Error; err != nil {
  54. t.Fatal(err)
  55. }
  56. found, ok, err := findProvisionReader(db, "MOCK-EDGE-001", "MOCK-GATE-001")
  57. if err != nil || !ok || found.ID != row.ID || !found.DeletedAt.Valid {
  58. t.Fatalf("expected soft-deleted row to be reused, row=%+v found=%v ok=%v err=%v", row, found, ok, err)
  59. }
  60. found.DeletedAt = gorm.DeletedAt{}
  61. found.DeviceName = "restored"
  62. if err := db.Unscoped().Save(&found).Error; err != nil {
  63. t.Fatal(err)
  64. }
  65. var active dao.UHFReader
  66. if err := db.First(&active, row.ID).Error; err != nil {
  67. t.Fatal(err)
  68. }
  69. if active.DeviceName != "restored" {
  70. t.Fatalf("expected restored row, got %+v", active)
  71. }
  72. }
  73. func TestFindProvisionReaderRejectsCodeOwnedByOtherDevice(t *testing.T) {
  74. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  75. if err != nil {
  76. t.Fatal(err)
  77. }
  78. if err := db.AutoMigrate(&dao.UHFReader{}); err != nil {
  79. t.Fatal(err)
  80. }
  81. if err := db.Create(&dao.UHFReader{DeviceCode: "MOCK-GATE-001", DeviceID: "OTHER-EDGE", DeviceName: "other", DeviceType: "gate"}).Error; err != nil {
  82. t.Fatal(err)
  83. }
  84. if _, ok, err := findProvisionReader(db, "MOCK-EDGE-001", "MOCK-GATE-001"); err == nil || ok {
  85. t.Fatalf("expected code conflict, ok=%v err=%v", ok, err)
  86. }
  87. }
  88. func TestValidateDiscoveryResponse(t *testing.T) {
  89. publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
  90. if err != nil {
  91. t.Fatal(err)
  92. }
  93. keyHash := sha256.Sum256(publicKey)
  94. request := discoveryRequestPacket{Schema: "discovery.request.v1", RequestID: "request-1", Nonce: "nonce-1"}
  95. packet := discoveryResponsePacket{
  96. Schema: "discovery.response.v1", RequestID: request.RequestID, Nonce: request.Nonce,
  97. DeviceID: "SN-1", DeviceCode: "EDGE-1", DeviceType: "lpr-gate", DeviceModel: "LPR-200",
  98. FirmwareVersion: "2.1.0", IP: "192.0.2.10", ConfigURL: "https://192.0.2.10:8443/api/v1/provision",
  99. PublicKeyFingerprint: "SHA256:" + fmt.Sprintf("%x", keyHash), SigningPublicKey: base64.StdEncoding.EncodeToString(publicKey),
  100. }
  101. packet.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, discoverySigningPayload(packet)))
  102. raw, err := json.Marshal(packet)
  103. if err != nil {
  104. t.Fatal(err)
  105. }
  106. result, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request)
  107. if err != nil {
  108. t.Fatalf("expected valid discovery response: %v", err)
  109. }
  110. if !result.SignatureVerified || result.ConfigPort != 8443 || result.DeviceID != "SN-1" {
  111. t.Fatalf("unexpected discovery result: %#v", result)
  112. }
  113. packet.Signature = base64.StdEncoding.EncodeToString([]byte("invalid"))
  114. raw, _ = json.Marshal(packet)
  115. if _, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request); err == nil {
  116. t.Fatal("invalid signature must be rejected")
  117. }
  118. packet.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, discoverySigningPayload(packet)))
  119. packet.Nonce = "unexpected-nonce"
  120. raw, _ = json.Marshal(packet)
  121. if _, err := validateDiscoveryResponse(raw, net.ParseIP("192.0.2.10"), request); err == nil {
  122. t.Fatal("response with a different nonce must be rejected")
  123. }
  124. }
  125. func TestDiscoveryBroadcastTargetsAcceptsExplicitIPv4(t *testing.T) {
  126. targets, err := discoveryBroadcastTargets("192.168.10.255")
  127. if err != nil || len(targets) != 1 || !targets[0].Equal(net.ParseIP("192.168.10.255")) {
  128. t.Fatalf("unexpected broadcast targets: %#v, %v", targets, err)
  129. }
  130. if _, err := discoveryBroadcastTargets("ff02::1"); err == nil {
  131. t.Fatal("IPv6 multicast must not be accepted as an IPv4 UDP discovery target")
  132. }
  133. }
  134. func TestValidateAddressAllowsUnicastIPs(t *testing.T) {
  135. for _, rawIP := range []string{"192.0.2.10", "2001:db8::10"} {
  136. ip, err := ValidateAddress(rawIP, 8443)
  137. if err != nil {
  138. t.Fatalf("ValidateAddress(%q) returned error: %v", rawIP, err)
  139. }
  140. if !ip.Equal(net.ParseIP(rawIP)) {
  141. t.Fatalf("ValidateAddress(%q) returned %v", rawIP, ip)
  142. }
  143. }
  144. }
  145. func TestDirectedBroadcastCheck(t *testing.T) {
  146. network := net.ParseIP("192.168.50.12")
  147. mask := net.CIDRMask(24, 32)
  148. if !isDirectedBroadcastForNetwork(net.ParseIP("192.168.50.255"), network, mask) {
  149. t.Fatal("expected subnet broadcast address to be rejected")
  150. }
  151. if isDirectedBroadcastForNetwork(net.ParseIP("192.168.50.26"), network, mask) {
  152. t.Fatal("ordinary unicast address must not be classified as broadcast")
  153. }
  154. }