Skip to content

Commit a18b3e4

Browse files
taggable
1 parent ed4991e commit a18b3e4

File tree

5 files changed

+340
-11
lines changed

5 files changed

+340
-11
lines changed

pkg/model/model/asset_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ func TestAsset_Visit(t *testing.T) {
4141
update: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Source: SeedSource}},
4242
expected: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Source: SeedSource}},
4343
},
44+
{
45+
name: "visit propagates tags",
46+
existing: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"production", "web"}}}},
47+
update: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"critical", "monitored"}}}},
48+
expected: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"production", "web", "critical", "monitored"}}}},
49+
},
50+
{
51+
name: "visit with duplicate tags",
52+
existing: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"production", "web"}}}},
53+
update: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"production", "critical"}}}},
54+
expected: Asset{DNS: "example.com", Name: "1.2.3.4", BaseAsset: BaseAsset{Tags: Tags{Tags: []string{"production", "web", "critical"}}}},
55+
},
4456
}
4557

4658
for _, tt := range tests {
@@ -53,6 +65,7 @@ func TestAsset_Visit(t *testing.T) {
5365
assert.Equal(t, tt.expected.Private, tt.existing.Private)
5466
assert.Equal(t, tt.expected.Origins, tt.existing.Origins)
5567
assert.Equal(t, tt.expected.GetLabels(), tt.existing.GetLabels())
68+
assert.Equal(t, tt.expected.Tags.Tags, tt.existing.Tags.Tags)
5669
})
5770
}
5871
}

pkg/model/model/port.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Port struct {
2929
Capability string `neo4j:"capability" json:"capability,omitempty" desc:"Capability that discovered this port." example:"portscan"`
3030
TTL int64 `neo4j:"ttl" json:"ttl" desc:"Time-to-live for the port record (Unix timestamp)." example:"1706353200"`
3131
Parent GraphModelWrapper `neo4j:"-" json:"parent" desc:"Port parent asset."`
32+
Tags
3233
}
3334

3435
const PortLabel = "Port"
@@ -77,6 +78,7 @@ func (p *Port) Visit(other Port) {
7778
p.Service = other.Service
7879
}
7980
p.Parent = other.Parent
81+
p.Tags.Visit(other.Tags)
8082
}
8183

8284
func (p *Port) IsClass(value string) bool {

pkg/model/model/port_test.go

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,114 @@ func TestPort_IsClass(t *testing.T) {
122122
}
123123

124124
func TestPort_Visit(t *testing.T) {
125-
asset := Asset{}
126-
port1 := NewPort("tcp", 80, &asset)
127-
port2 := Port{
128-
Status: "inactive",
129-
Service: "http",
130-
TTL: 12345,
125+
tests := []struct {
126+
name string
127+
existing Port
128+
update Port
129+
validate func(*testing.T, Port)
130+
}{
131+
{
132+
name: "basic visit updates status, service, and TTL",
133+
existing: func() Port {
134+
asset := Asset{}
135+
return NewPort("tcp", 80, &asset)
136+
}(),
137+
update: Port{
138+
Status: "inactive",
139+
Service: "http",
140+
TTL: 12345,
141+
},
142+
validate: func(t *testing.T, p Port) {
143+
assert.Equal(t, "inactive", p.Status)
144+
assert.Equal(t, "http", p.Service)
145+
assert.Equal(t, int64(12345), p.TTL)
146+
},
147+
},
148+
{
149+
name: "visit propagates tags without duplicates",
150+
existing: func() Port {
151+
asset := Asset{}
152+
port := NewPort("tcp", 80, &asset)
153+
port.Tags = Tags{Tags: []string{"production", "web"}}
154+
return port
155+
}(),
156+
update: Port{
157+
Tags: Tags{Tags: []string{"critical", "monitored"}},
158+
},
159+
validate: func(t *testing.T, p Port) {
160+
assert.Equal(t, []string{"production", "web", "critical", "monitored"}, p.Tags.Tags)
161+
},
162+
},
163+
{
164+
name: "visit with duplicate tags only adds new ones",
165+
existing: func() Port {
166+
asset := Asset{}
167+
port := NewPort("tcp", 443, &asset)
168+
port.Tags = Tags{Tags: []string{"production", "web"}}
169+
return port
170+
}(),
171+
update: Port{
172+
Tags: Tags{Tags: []string{"production", "critical"}},
173+
},
174+
validate: func(t *testing.T, p Port) {
175+
assert.Equal(t, []string{"production", "web", "critical"}, p.Tags.Tags)
176+
},
177+
},
178+
{
179+
name: "visit with empty tags preserves existing",
180+
existing: func() Port {
181+
asset := Asset{}
182+
port := NewPort("tcp", 22, &asset)
183+
port.Tags = Tags{Tags: []string{"ssh", "admin"}}
184+
return port
185+
}(),
186+
update: Port{
187+
Status: Active,
188+
},
189+
validate: func(t *testing.T, p Port) {
190+
assert.Equal(t, []string{"ssh", "admin"}, p.Tags.Tags)
191+
},
192+
},
193+
{
194+
name: "visit updates service and propagates tags",
195+
existing: func() Port {
196+
asset := Asset{}
197+
port := NewPort("tcp", 3306, &asset)
198+
port.Tags = Tags{Tags: []string{"database"}}
199+
return port
200+
}(),
201+
update: Port{
202+
Service: "mysql",
203+
Tags: Tags{Tags: []string{"production", "critical"}},
204+
},
205+
validate: func(t *testing.T, p Port) {
206+
assert.Equal(t, "mysql", p.Service)
207+
assert.Equal(t, []string{"database", "production", "critical"}, p.Tags.Tags)
208+
},
209+
},
210+
{
211+
name: "visit does not update status when pending",
212+
existing: func() Port {
213+
asset := Asset{}
214+
port := NewPort("tcp", 80, &asset)
215+
port.Status = Active
216+
return port
217+
}(),
218+
update: Port{
219+
Status: Pending,
220+
},
221+
validate: func(t *testing.T, p Port) {
222+
assert.Equal(t, Active, p.Status)
223+
},
224+
},
131225
}
132226

133-
port1.Visit(port2)
134-
135-
assert.Equal(t, "inactive", port1.Status)
136-
assert.Equal(t, "http", port1.Service)
137-
assert.Equal(t, int64(12345), port1.TTL)
227+
for _, tt := range tests {
228+
t.Run(tt.name, func(t *testing.T) {
229+
tt.existing.Visit(tt.update)
230+
tt.validate(t, tt.existing)
231+
})
232+
}
138233
}
139234

140235
func TestPortConditions(t *testing.T) {

pkg/model/model/tags.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package model
22

33
import "slices"
44

5+
type Taggable interface {
6+
GetTags() []string
7+
AppendTags(...string)
8+
}
9+
510
type Tags struct {
611
Tags []string `json:"tags,omitempty" neo4j:"tags"`
712
}
@@ -19,3 +24,11 @@ func (t *Tags) Visit(other Tags) {
1924
}
2025
}
2126
}
27+
28+
func (t *Tags) GetTags() []string {
29+
return t.Tags
30+
}
31+
32+
func (t *Tags) AppendTags(tags ...string) {
33+
t.Tags = append(t.Tags, tags...)
34+
}

pkg/model/model/tags_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package model
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestTags_GetTags(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
tags Tags
13+
expected []string
14+
}{
15+
{
16+
name: "empty tags",
17+
tags: Tags{},
18+
expected: nil,
19+
},
20+
{
21+
name: "single tag",
22+
tags: Tags{Tags: []string{"production"}},
23+
expected: []string{"production"},
24+
},
25+
{
26+
name: "multiple tags",
27+
tags: Tags{Tags: []string{"production", "critical", "web"}},
28+
expected: []string{"production", "critical", "web"},
29+
},
30+
}
31+
32+
for _, tt := range tests {
33+
t.Run(tt.name, func(t *testing.T) {
34+
result := tt.tags.GetTags()
35+
assert.Equal(t, tt.expected, result)
36+
})
37+
}
38+
}
39+
40+
func TestTags_AppendTags(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
initial Tags
44+
toAppend []string
45+
expected []string
46+
}{
47+
{
48+
name: "append to empty",
49+
initial: Tags{},
50+
toAppend: []string{"production"},
51+
expected: []string{"production"},
52+
},
53+
{
54+
name: "append single tag",
55+
initial: Tags{Tags: []string{"production"}},
56+
toAppend: []string{"critical"},
57+
expected: []string{"production", "critical"},
58+
},
59+
{
60+
name: "append multiple tags",
61+
initial: Tags{Tags: []string{"production"}},
62+
toAppend: []string{"critical", "web", "database"},
63+
expected: []string{"production", "critical", "web", "database"},
64+
},
65+
{
66+
name: "append duplicate tags (no deduplication in AppendTags)",
67+
initial: Tags{Tags: []string{"production"}},
68+
toAppend: []string{"production", "critical"},
69+
expected: []string{"production", "production", "critical"},
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
tt.initial.AppendTags(tt.toAppend...)
76+
assert.Equal(t, tt.expected, tt.initial.Tags)
77+
})
78+
}
79+
}
80+
81+
func TestTags_Merge(t *testing.T) {
82+
tests := []struct {
83+
name string
84+
existing Tags
85+
update Tags
86+
expected []string
87+
}{
88+
{
89+
name: "merge into empty",
90+
existing: Tags{},
91+
update: Tags{Tags: []string{"production", "critical"}},
92+
expected: []string{"production", "critical"},
93+
},
94+
{
95+
name: "merge with empty",
96+
existing: Tags{Tags: []string{"production"}},
97+
update: Tags{},
98+
expected: []string{"production"},
99+
},
100+
{
101+
name: "merge replaces existing",
102+
existing: Tags{Tags: []string{"staging", "test"}},
103+
update: Tags{Tags: []string{"production", "critical"}},
104+
expected: []string{"production", "critical"},
105+
},
106+
{
107+
name: "merge with nil",
108+
existing: Tags{Tags: []string{"production"}},
109+
update: Tags{Tags: nil},
110+
expected: []string{"production"},
111+
},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
tt.existing.Merge(tt.update)
117+
assert.Equal(t, tt.expected, tt.existing.Tags)
118+
})
119+
}
120+
}
121+
122+
func TestTags_Visit(t *testing.T) {
123+
tests := []struct {
124+
name string
125+
existing Tags
126+
update Tags
127+
expected []string
128+
}{
129+
{
130+
name: "visit with empty tags",
131+
existing: Tags{Tags: []string{"production"}},
132+
update: Tags{},
133+
expected: []string{"production"},
134+
},
135+
{
136+
name: "visit empty with tags",
137+
existing: Tags{},
138+
update: Tags{Tags: []string{"production", "critical"}},
139+
expected: []string{"production", "critical"},
140+
},
141+
{
142+
name: "visit with new tags",
143+
existing: Tags{Tags: []string{"production"}},
144+
update: Tags{Tags: []string{"critical", "web"}},
145+
expected: []string{"production", "critical", "web"},
146+
},
147+
{
148+
name: "visit with duplicate tags",
149+
existing: Tags{Tags: []string{"production", "web"}},
150+
update: Tags{Tags: []string{"production", "critical"}},
151+
expected: []string{"production", "web", "critical"},
152+
},
153+
{
154+
name: "visit with all duplicate tags",
155+
existing: Tags{Tags: []string{"production", "critical"}},
156+
update: Tags{Tags: []string{"production", "critical"}},
157+
expected: []string{"production", "critical"},
158+
},
159+
{
160+
name: "visit preserves order",
161+
existing: Tags{Tags: []string{"a", "b", "c"}},
162+
update: Tags{Tags: []string{"d", "e", "f"}},
163+
expected: []string{"a", "b", "c", "d", "e", "f"},
164+
},
165+
{
166+
name: "visit with nil",
167+
existing: Tags{Tags: []string{"production"}},
168+
update: Tags{Tags: nil},
169+
expected: []string{"production"},
170+
},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
tt.existing.Visit(tt.update)
176+
assert.Equal(t, tt.expected, tt.existing.Tags)
177+
})
178+
}
179+
}
180+
181+
func TestTags_VisitDeduplication(t *testing.T) {
182+
// Test that Visit properly deduplicates tags
183+
existing := Tags{Tags: []string{"tag1", "tag2"}}
184+
185+
// First visit
186+
existing.Visit(Tags{Tags: []string{"tag3", "tag1"}})
187+
assert.Equal(t, []string{"tag1", "tag2", "tag3"}, existing.Tags, "should not duplicate tag1")
188+
189+
// Second visit
190+
existing.Visit(Tags{Tags: []string{"tag4", "tag2", "tag5"}})
191+
assert.Equal(t, []string{"tag1", "tag2", "tag3", "tag4", "tag5"}, existing.Tags, "should not duplicate tag2")
192+
193+
// Third visit with all duplicates
194+
existing.Visit(Tags{Tags: []string{"tag1", "tag2", "tag3"}})
195+
assert.Equal(t, []string{"tag1", "tag2", "tag3", "tag4", "tag5"}, existing.Tags, "should not add any duplicates")
196+
}
197+
198+
func TestTaggableInterface(t *testing.T) {
199+
// Test that Tags implements Taggable interface
200+
var taggable Taggable = &Tags{}
201+
202+
taggable.AppendTags("test1", "test2")
203+
tags := taggable.GetTags()
204+
205+
assert.Equal(t, []string{"test1", "test2"}, tags)
206+
}

0 commit comments

Comments
 (0)