Skip to content

Commit d6452a5

Browse files
committed
updated
1 parent 7495e32 commit d6452a5

File tree

11 files changed

+694
-49
lines changed

11 files changed

+694
-49
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"Bash(curl:*)",
88
"Bash(taskkill:*)",
99
"mcp__chakra-ui",
10-
"mcp__serena"
10+
"mcp__serena",
11+
"Bash(sqlite3:*)",
12+
"Bash(dir:*)",
13+
"Bash(timeout:*)"
1114
],
1215
"deny": [],
1316
"ask": []

ThingConnect.Pulse.Server/config.schema.json

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
"additionalProperties": false,
77
"required": ["version", "defaults", "groups", "targets"],
88
"properties": {
9-
"version": { "type": "integer", "const": 1 },
9+
"version": { "type": ["integer", "string"], "enum": [1, "1"] },
1010
"defaults": {
1111
"type": "object",
1212
"additionalProperties": false,
1313
"properties": {
14-
"interval_seconds": { "type": "integer", "minimum": 1, "default": 10 },
15-
"timeout_ms": { "type": "integer", "minimum": 100, "default": 1500 },
16-
"retries": { "type": "integer", "minimum": 0, "default": 1 },
14+
"interval_seconds": { "type": ["integer", "string"], "minimum": 1, "default": 10 },
15+
"timeout_ms": { "type": ["integer", "string"], "minimum": 100, "default": 1500 },
16+
"retries": { "type": ["integer", "string"], "minimum": 0, "default": 1 },
1717
"http": {
1818
"type": "object",
1919
"additionalProperties": false,
@@ -31,18 +31,18 @@
3131
"minItems": 1,
3232
"items": {
3333
"type": "object",
34-
"additionalProperties": false,
3534
"required": ["id", "name"],
3635
"properties": {
3736
"id": {
3837
"type": "string",
3938
"pattern": "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$"
4039
},
4140
"name": { "type": "string", "minLength": 1 },
42-
"parent_id": { "type": "string" },
41+
"parent_id": { "type": ["string", "null"] },
4342
"color": { "type": "string", "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
44-
"sort_order": { "type": "integer" }
45-
}
43+
"sort_order": { "type": ["integer", "string"] }
44+
},
45+
"additionalProperties": false
4646
}
4747
},
4848
"targets": {
@@ -58,66 +58,51 @@
5858
},
5959
"ipv4": {
6060
"type": "string",
61-
"pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)$"
61+
"pattern": "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$"
6262
},
6363
"ipv6": {
6464
"type": "string",
6565
"pattern": "^[0-9A-Fa-f:]+$"
6666
},
6767
"cidr": {
6868
"type": "string",
69-
"pattern": "^(?:((?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d))\\/(?:[0-9]|[12][0-9]|3[0-2]))|((?:[0-9A-Fa-f:]+)\\/(?:[0-9]|[1-9]\\d|1[01]\\d|12[0-8])))$"
69+
"pattern": "^(?:(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\/(?:[0-9]|[12][0-9]|3[0-2]))|(?:(?:[0-9A-Fa-f:]+)\\/(?:[0-9]|[1-9]\\d|1[01]\\d|12[0-8]))$"
7070
},
7171
"wildcard": {
7272
"type": "string",
7373
"description": "IPv4 wildcard like 10.10.1.* (expands 1..254 by default)",
7474
"pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}\\*$"
7575
},
7676
"probeType": { "enum": ["icmp", "tcp", "http"] },
77-
"baseTarget": {
77+
"target": {
7878
"type": "object",
79-
"additionalProperties": false,
8079
"required": ["type", "group"],
8180
"properties": {
8281
"type": { "$ref": "#/definitions/probeType" },
8382
"group": { "type": "string" },
84-
"name": { "type": "string" },
85-
"interval_seconds": { "type": "integer", "minimum": 1 },
86-
"timeout_ms": { "type": "integer", "minimum": 100 },
87-
"retries": { "type": "integer", "minimum": 0 },
88-
"expected_rtt_ms": { "type": "integer", "minimum": 1 },
89-
"enabled": { "type": "boolean", "default": true },
90-
"notes": { "type": "string" }
91-
}
92-
},
93-
"locationHost": {
94-
"oneOf": [
95-
{ "properties": { "host": { "oneOf": [ { "$ref": "#/definitions/hostname" }, { "$ref": "#/definitions/ipv4" }, { "$ref": "#/definitions/ipv6" } ] } }, "required": ["host"], "additionalProperties": true },
96-
{ "properties": { "cidr": { "$ref": "#/definitions/cidr" } }, "required": ["cidr"], "additionalProperties": true },
97-
{ "properties": { "wildcard": { "$ref": "#/definitions/wildcard" } }, "required": ["wildcard"], "additionalProperties": true }
98-
]
99-
},
100-
"target": {
83+
"name": { "type": ["string", "null"] },
84+
"host": { "type": ["string", "null"] },
85+
"cidr": { "type": ["string", "null"] },
86+
"wildcard": { "type": ["string", "null"] },
87+
"port": { "type": ["integer", "string", "null"], "minimum": 1, "maximum": 65535 },
88+
"http_path": { "type": ["string", "null"] },
89+
"http_match": { "type": ["string", "null"] },
90+
"interval_seconds": { "type": ["integer", "string", "null"], "minimum": 1 },
91+
"timeout_ms": { "type": ["integer", "string", "null"], "minimum": 100 },
92+
"retries": { "type": ["integer", "string", "null"], "minimum": 0 },
93+
"expected_rtt_ms": { "type": ["integer", "string", "null"], "minimum": 1 },
94+
"enabled": { "type": ["boolean", "null"] },
95+
"notes": { "type": ["string", "null"] }
96+
},
97+
"anyOf": [
98+
{ "required": ["host"] },
99+
{ "required": ["cidr"] },
100+
{ "required": ["wildcard"] }
101+
],
101102
"allOf": [
102-
{ "$ref": "#/definitions/baseTarget" },
103-
{ "$ref": "#/definitions/locationHost" },
104103
{
105-
"type": "object",
106-
"properties": {
107-
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
108-
"http_path": { "type": "string", "default": "/" },
109-
"http_match": { "type": "string" }
110-
},
111-
"allOf": [
112-
{
113-
"if": { "properties": { "type": { "const": "tcp" } } },
114-
"then": { "required": ["port"] }
115-
},
116-
{
117-
"if": { "properties": { "type": { "const": "http" } } },
118-
"then": { "properties": { "port": { "default": 80 }, "http_path": { "minLength": 1 } } }
119-
}
120-
]
104+
"if": { "properties": { "type": { "const": "tcp" } } },
105+
"then": { "required": ["port"] }
121106
}
122107
]
123108
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using Microsoft.Extensions.Logging;
2+
using NUnit.Framework;
3+
using System.Net;
4+
using System.Text.RegularExpressions;
5+
6+
namespace ThingConnect.Pulse.Tests;
7+
8+
[TestFixture]
9+
public class CidrExpansionTests
10+
{
11+
private static readonly Regex CidrRegex = new(@"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/(\d{1,2})$",
12+
RegexOptions.Compiled);
13+
14+
// Standalone CIDR expansion method for testing - copied from DiscoveryService
15+
private IEnumerable<string> ExpandCidr(string cidr)
16+
{
17+
Match match = CidrRegex.Match(cidr);
18+
if (!match.Success)
19+
{
20+
Console.WriteLine($"WARNING: Invalid CIDR format: {cidr}");
21+
yield break;
22+
}
23+
24+
string baseIp = match.Groups[1].Value;
25+
int prefixLength = int.Parse(match.Groups[2].Value);
26+
27+
if (prefixLength < 0 || prefixLength > 32)
28+
{
29+
Console.WriteLine($"WARNING: Invalid CIDR prefix length: {prefixLength}");
30+
yield break;
31+
}
32+
33+
if (!IPAddress.TryParse(baseIp, out IPAddress? ipAddress))
34+
{
35+
Console.WriteLine($"WARNING: Invalid IP address in CIDR: {baseIp}");
36+
yield break;
37+
}
38+
39+
byte[] addressBytes = ipAddress.GetAddressBytes();
40+
uint addressInt = BitConverter.ToUInt32(addressBytes.Reverse().ToArray(), 0);
41+
42+
int hostBits = 32 - prefixLength;
43+
uint hostCount = (uint)(1 << hostBits);
44+
uint networkAddress = addressInt & (0xFFFFFFFF << hostBits);
45+
46+
// Skip network and broadcast addresses for practical use
47+
uint startAddress = networkAddress + 1;
48+
uint endAddress = networkAddress + hostCount - 1;
49+
50+
for (uint address = startAddress; address < endAddress && address > networkAddress; address++)
51+
{
52+
byte[] bytes = BitConverter.GetBytes(address).Reverse().ToArray();
53+
var ip = new IPAddress(bytes);
54+
yield return ip.ToString();
55+
}
56+
}
57+
58+
[Test]
59+
public void TestCidrExpansion_With24Subnet_ShouldReturn254IPs()
60+
{
61+
// Arrange
62+
string cidr = "10.18.8.0/24";
63+
64+
// Act
65+
var expandedIPs = ExpandCidr(cidr).ToList();
66+
67+
// Assert
68+
Console.WriteLine($"Testing CIDR expansion for: {cidr}");
69+
Console.WriteLine($"Expected: 254 IP addresses from 10.18.8.1 to 10.18.8.254");
70+
Console.WriteLine($"Actual count: {expandedIPs.Count}");
71+
72+
Assert.That(expandedIPs.Count, Is.EqualTo(254), $"CIDR /24 should expand to 254 IPs but got {expandedIPs.Count}");
73+
74+
if (expandedIPs.Count > 0)
75+
{
76+
Console.WriteLine($"First IP: {expandedIPs.First()}");
77+
Console.WriteLine($"Last IP: {expandedIPs.Last()}");
78+
79+
Assert.That(expandedIPs.First(), Is.EqualTo("10.18.8.1"), "First IP should be .1");
80+
Assert.That(expandedIPs.Last(), Is.EqualTo("10.18.8.254"), "Last IP should be .254");
81+
82+
// Print first and last 5 IPs for debugging
83+
Console.WriteLine("First 5 IPs:");
84+
foreach (var ip in expandedIPs.Take(5))
85+
{
86+
Console.WriteLine($" {ip}");
87+
}
88+
Console.WriteLine("Last 5 IPs:");
89+
foreach (var ip in expandedIPs.TakeLast(5))
90+
{
91+
Console.WriteLine($" {ip}");
92+
}
93+
}
94+
}
95+
96+
[Test]
97+
public void TestCidrExpansion_With30Subnet_ShouldReturn2IPs()
98+
{
99+
// Arrange
100+
string cidr = "192.168.1.0/30";
101+
102+
// Act
103+
var expandedIPs = ExpandCidr(cidr).ToList();
104+
105+
// Assert
106+
Console.WriteLine($"Testing CIDR expansion for: {cidr}");
107+
Console.WriteLine($"Actual count: {expandedIPs.Count}");
108+
109+
Assert.That(expandedIPs.Count, Is.EqualTo(2), "CIDR /30 should expand to 2 IPs");
110+
Assert.That(expandedIPs[0], Is.EqualTo("192.168.1.1"));
111+
Assert.That(expandedIPs[1], Is.EqualTo("192.168.1.2"));
112+
113+
Console.WriteLine("All IPs:");
114+
foreach (var ip in expandedIPs)
115+
{
116+
Console.WriteLine($" {ip}");
117+
}
118+
}
119+
120+
[Test]
121+
public void TestCidrExpansion_WithInvalidFormat_ShouldReturnEmpty()
122+
{
123+
// Arrange
124+
string invalidCidr = "invalid-cidr";
125+
126+
// Act
127+
var expandedIPs = ExpandCidr(invalidCidr).ToList();
128+
129+
// Assert
130+
Console.WriteLine($"Testing invalid CIDR: {invalidCidr}");
131+
Console.WriteLine($"Actual count: {expandedIPs.Count}");
132+
133+
Assert.That(expandedIPs.Count, Is.EqualTo(0), "Invalid CIDR should return empty list");
134+
}
135+
136+
[Test]
137+
public void TestCidrExpansion_UserScenario_10_18_8_0_24()
138+
{
139+
// Arrange - This is the exact CIDR from the user's config
140+
string cidr = "10.18.8.0/24";
141+
142+
// Act
143+
var expandedIPs = ExpandCidr(cidr).ToList();
144+
145+
// Assert
146+
Console.WriteLine($"Testing USER'S CIDR: {cidr}");
147+
Console.WriteLine($"Expected: 254 IP addresses from 10.18.8.1 to 10.18.8.254");
148+
Console.WriteLine($"Actual count: {expandedIPs.Count}");
149+
150+
Assert.That(expandedIPs.Count, Is.EqualTo(254), $"User's CIDR {cidr} should expand to 254 IPs but got {expandedIPs.Count}");
151+
152+
if (expandedIPs.Count > 0)
153+
{
154+
Console.WriteLine($"First IP: {expandedIPs.First()}");
155+
Console.WriteLine($"Last IP: {expandedIPs.Last()}");
156+
157+
Assert.That(expandedIPs.First(), Is.EqualTo("10.18.8.1"), "First IP should be .1");
158+
Assert.That(expandedIPs.Last(), Is.EqualTo("10.18.8.254"), "Last IP should be .254");
159+
160+
// Print first and last 3 IPs for debugging
161+
Console.WriteLine("First 3 IPs:");
162+
foreach (var ip in expandedIPs.Take(3))
163+
{
164+
Console.WriteLine($" {ip}");
165+
}
166+
Console.WriteLine("Last 3 IPs:");
167+
foreach (var ip in expandedIPs.TakeLast(3))
168+
{
169+
Console.WriteLine($" {ip}");
170+
}
171+
}
172+
else
173+
{
174+
Assert.Fail("CRITICAL: CIDR expansion returned ZERO IPs! This explains why CIDR is not working.");
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)