Skip to content

Commit a9eee47

Browse files
Matovidloclaude
andcommitted
test: add unit tests for notification mapper, naming generator, and validation
Add unit tests following established patterns from configmetadata mapper: - TestNotificationMapper_MapBeforeLocalSave: verifies config.json and meta.json are correctly written when saving notification to local filesystem - TestNotificationMapper_MapAfterLocalLoad: verifies Notification fields are correctly populated when loading from local config.json - TestNotificationPath: verifies naming generator produces correct path for notification objects (e.g. "my-config/notifications/sub-abc123") - TestValidateNotificationFilters_*: validates filter field/operator combinations - TestFindSimilarFieldNames: validates fuzzy matching for typo detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0ba3a57 commit a9eee47

4 files changed

Lines changed: 315 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package notification_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/keboola/keboola-sdk-go/v2/pkg/keboola"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/keboola/keboola-as-code/internal/pkg/filesystem"
11+
"github.com/keboola/keboola-as-code/internal/pkg/mapper/notification"
12+
"github.com/keboola/keboola-as-code/internal/pkg/model"
13+
"github.com/keboola/keboola-as-code/internal/pkg/service/common/dependencies"
14+
)
15+
16+
func TestNotificationMapper_MapAfterLocalLoad(t *testing.T) {
17+
t.Parallel()
18+
d := dependencies.NewMocked(t, t.Context())
19+
logger := d.DebugLogger()
20+
mockedState := d.MockedState()
21+
mockedState.Mapper().AddMapper(notification.NewMapper(mockedState))
22+
23+
notifKey := model.NotificationKey{
24+
BranchID: 123,
25+
ComponentID: keboola.ComponentID("ex-generic-v2"),
26+
ConfigID: keboola.ConfigID("my-config"),
27+
ID: keboola.NotificationSubscriptionID("sub-123"),
28+
}
29+
manifest := &model.NotificationManifest{
30+
NotificationKey: notifKey,
31+
Paths: model.Paths{
32+
AbsPath: model.NewAbsPath("main/extractor/ex-generic-v2/my-config", "notifications/sub-123"),
33+
},
34+
}
35+
36+
// Write config.json and meta.json to the mocked FS.
37+
fs := mockedState.ObjectsRoot()
38+
ctx := t.Context()
39+
require.NoError(t, fs.Mkdir(ctx, manifest.Path()))
40+
configJSON := `{
41+
"event": "job-failed",
42+
"filters": [{"field": "job.configuration.id", "value": "my-config", "operator": "=="}],
43+
"recipient": {"channel": "email", "address": "user@example.com"}
44+
}`
45+
require.NoError(t, fs.WriteFile(ctx, filesystem.NewRawFile(
46+
mockedState.NamingGenerator().ConfigFilePath(manifest.Path()),
47+
configJSON,
48+
)))
49+
require.NoError(t, fs.WriteFile(ctx, filesystem.NewRawFile(
50+
mockedState.NamingGenerator().MetaFilePath(manifest.Path()),
51+
`{}`,
52+
)))
53+
54+
notif := &model.Notification{NotificationKey: notifKey}
55+
recipe := model.NewLocalLoadRecipe(mockedState.FileLoader(), manifest, notif)
56+
require.NoError(t, mockedState.Mapper().MapAfterLocalLoad(ctx, recipe))
57+
assert.Empty(t, logger.WarnAndErrorMessages())
58+
59+
assert.Equal(t, keboola.NotificationEventJobFailed, notif.Event)
60+
assert.Equal(t, keboola.NotificationRecipient{
61+
Channel: keboola.NotificationChannelEmail,
62+
Address: "user@example.com",
63+
}, notif.Recipient)
64+
require.Len(t, notif.Filters, 1)
65+
assert.Equal(t, keboola.NotificationFilter{
66+
Field: "job.configuration.id",
67+
Value: "my-config",
68+
Operator: keboola.NotificationFilterOperatorEquals,
69+
}, notif.Filters[0])
70+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package notification_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/keboola/keboola-sdk-go/v2/pkg/keboola"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/keboola/keboola-as-code/internal/pkg/mapper/notification"
11+
"github.com/keboola/keboola-as-code/internal/pkg/model"
12+
"github.com/keboola/keboola-as-code/internal/pkg/service/common/dependencies"
13+
)
14+
15+
func TestNotificationMapper_MapBeforeLocalSave(t *testing.T) {
16+
t.Parallel()
17+
d := dependencies.NewMocked(t, t.Context())
18+
logger := d.DebugLogger()
19+
mockedState := d.MockedState()
20+
mockedState.Mapper().AddMapper(notification.NewMapper(mockedState))
21+
22+
notifKey := model.NotificationKey{
23+
BranchID: 123,
24+
ComponentID: keboola.ComponentID("ex-generic-v2"),
25+
ConfigID: keboola.ConfigID("my-config"),
26+
ID: keboola.NotificationSubscriptionID("sub-123"),
27+
}
28+
manifest := &model.NotificationManifest{
29+
NotificationKey: notifKey,
30+
Paths: model.Paths{
31+
AbsPath: model.NewAbsPath("main/extractor/ex-generic-v2/my-config", "notifications/sub-123"),
32+
},
33+
}
34+
notif := &model.Notification{
35+
NotificationKey: notifKey,
36+
Event: keboola.NotificationEventJobFailed,
37+
Recipient: keboola.NotificationRecipient{
38+
Channel: keboola.NotificationChannelEmail,
39+
Address: "user@example.com",
40+
},
41+
Filters: []keboola.NotificationFilter{
42+
{
43+
Field: "job.configuration.id",
44+
Value: "my-config",
45+
Operator: keboola.NotificationFilterOperatorEquals,
46+
},
47+
},
48+
}
49+
state := &model.NotificationState{
50+
NotificationManifest: manifest,
51+
Local: notif,
52+
}
53+
54+
recipe := model.NewLocalSaveRecipe(state.Manifest(), state.Local, model.NewChangedFields())
55+
require.NoError(t, mockedState.Mapper().MapBeforeLocalSave(t.Context(), recipe))
56+
assert.Empty(t, logger.WarnAndErrorMessages())
57+
58+
assert.Len(t, recipe.Files.All(), 2)
59+
60+
configPath := mockedState.NamingGenerator().ConfigFilePath(manifest.Path())
61+
metaPath := mockedState.NamingGenerator().MetaFilePath(manifest.Path())
62+
63+
configFile := recipe.Files.GetOneByTag(model.FileKindObjectConfig)
64+
require.NotNil(t, configFile)
65+
assert.Equal(t, configPath, configFile.Path())
66+
configRaw, err := configFile.ToRawFile()
67+
require.NoError(t, err)
68+
assert.JSONEq(t, `{
69+
"event": "job-failed",
70+
"filters": [{"field": "job.configuration.id", "value": "my-config", "operator": "=="}],
71+
"recipient": {"channel": "email", "address": "user@example.com"}
72+
}`, configRaw.Content)
73+
74+
metaFile := recipe.Files.GetOneByTag(model.FileKindObjectMeta)
75+
require.NotNil(t, metaFile)
76+
assert.Equal(t, metaPath, metaFile.Path())
77+
metaRaw, err := metaFile.ToRawFile()
78+
require.NoError(t, err)
79+
assert.JSONEq(t, `{}`, metaRaw.Content)
80+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package model
2+
3+
import (
4+
"testing"
5+
6+
"github.com/keboola/keboola-sdk-go/v2/pkg/keboola"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestValidateNotificationFilters_ValidFields(t *testing.T) {
11+
t.Parallel()
12+
13+
validFilters := []keboola.NotificationFilter{
14+
{Field: "branch.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "123"},
15+
{Field: "job.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "456"},
16+
{Field: "job.component.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "ex-generic-v2"},
17+
{Field: "job.configuration.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "789"},
18+
{Field: "job.token.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "token-123"},
19+
{Field: "project.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "proj-456"},
20+
{Field: "eventType", Operator: keboola.NotificationFilterOperatorEquals, Value: "job-failed"},
21+
}
22+
23+
err := ValidateNotificationFilters(validFilters)
24+
assert.NoError(t, err, "valid filter fields should not return error")
25+
}
26+
27+
func TestValidateNotificationFilters_DeprecatedFields(t *testing.T) {
28+
t.Parallel()
29+
30+
tests := []struct {
31+
name string
32+
field string
33+
correctField string
34+
}{
35+
{"configId", "configId", "job.configuration.id"},
36+
{"componentId", "componentId", "job.component.id"},
37+
{"branchId", "branchId", "branch.id"},
38+
{"jobId", "jobId", "job.id"},
39+
{"tokenId", "tokenId", "job.token.id"},
40+
{"projectId", "projectId", "project.id"},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
t.Parallel()
46+
47+
filters := []keboola.NotificationFilter{
48+
{Field: tt.field, Operator: keboola.NotificationFilterOperatorEquals, Value: "123"},
49+
}
50+
51+
err := ValidateNotificationFilters(filters)
52+
assert.Error(t, err, "deprecated field should return error")
53+
assert.Contains(t, err.Error(), "deprecated field name")
54+
assert.Contains(t, err.Error(), tt.field)
55+
assert.Contains(t, err.Error(), tt.correctField)
56+
})
57+
}
58+
}
59+
60+
func TestValidateNotificationFilters_InvalidFields(t *testing.T) {
61+
t.Parallel()
62+
63+
tests := []struct {
64+
name string
65+
field string
66+
}{
67+
{"completely_invalid", "foobar"},
68+
{"another_invalid", "xyz123"},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
t.Parallel()
74+
75+
filters := []keboola.NotificationFilter{
76+
{Field: tt.field, Operator: keboola.NotificationFilterOperatorEquals, Value: "123"},
77+
}
78+
79+
err := ValidateNotificationFilters(filters)
80+
assert.Error(t, err, "invalid field should return error")
81+
assert.Contains(t, err.Error(), "invalid field name")
82+
assert.Contains(t, err.Error(), tt.field)
83+
})
84+
}
85+
}
86+
87+
func TestValidateNotificationFilters_EmptyFilters(t *testing.T) {
88+
t.Parallel()
89+
90+
err := ValidateNotificationFilters([]keboola.NotificationFilter{})
91+
assert.NoError(t, err, "empty filters should not return error")
92+
}
93+
94+
func TestValidateNotificationFilters_MultipleFilters(t *testing.T) {
95+
t.Parallel()
96+
97+
filters := []keboola.NotificationFilter{
98+
{Field: "branch.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "123"},
99+
{Field: "configId", Operator: keboola.NotificationFilterOperatorEquals, Value: "456"}, // deprecated
100+
{Field: "job.token.id", Operator: keboola.NotificationFilterOperatorEquals, Value: "789"},
101+
}
102+
103+
err := ValidateNotificationFilters(filters)
104+
assert.Error(t, err, "should fail on first invalid filter")
105+
assert.Contains(t, err.Error(), "filter[1]")
106+
assert.Contains(t, err.Error(), "configId")
107+
}
108+
109+
func TestFindSimilarFieldNames(t *testing.T) {
110+
t.Parallel()
111+
112+
tests := []struct {
113+
name string
114+
input string
115+
expectedSuggestions []string
116+
}{
117+
{
118+
name: "config",
119+
input: "config",
120+
expectedSuggestions: []string{"job.configuration.id"},
121+
},
122+
{
123+
name: "job",
124+
input: "job",
125+
expectedSuggestions: []string{"job.id", "job.component.id", "job.configuration.id", "job.token.id"},
126+
},
127+
{
128+
name: "branch",
129+
input: "branch",
130+
expectedSuggestions: []string{"branch.id"},
131+
},
132+
{
133+
name: "completely_invalid",
134+
input: "xyz",
135+
expectedSuggestions: []string{},
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
t.Parallel()
142+
143+
suggestions := findSimilarFieldNames(tt.input)
144+
145+
// Check that all expected suggestions are present
146+
for _, expected := range tt.expectedSuggestions {
147+
assert.Contains(t, suggestions, expected, "should contain suggested field")
148+
}
149+
})
150+
}
151+
}

internal/pkg/naming/generator_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ import (
1010
. "github.com/keboola/keboola-as-code/internal/pkg/model"
1111
)
1212

13+
func TestNotificationPath(t *testing.T) {
14+
t.Parallel()
15+
g := NewGenerator(TemplateWithIds(), NewRegistry())
16+
notification := &Notification{
17+
NotificationKey: NotificationKey{
18+
BranchID: 123,
19+
ComponentID: "ex-generic-v2",
20+
ConfigID: "my-config",
21+
ID: "abc123",
22+
},
23+
}
24+
assert.Equal(t, "my-config-path/notifications/sub-abc123", g.NotificationPath("my-config-path", notification).Path())
25+
}
26+
1327
func TestUniquePathSameObjectType(t *testing.T) {
1428
t.Parallel()
1529
g := NewGenerator(TemplateWithIds(), NewRegistry())

0 commit comments

Comments
 (0)