From 22f33af4397c2a93ea9939bee9a91be22b5c9abc Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Fri, 20 Feb 2026 21:49:37 +0000 Subject: [PATCH 1/9] Implement basic bindings for /notification endpoints. Signed-off-by: SolarFactories --- client.go | 2 + notification.go | 272 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 notification.go diff --git a/client.go b/client.go index 4a75d7b..dff410d 100644 --- a/client.go +++ b/client.go @@ -47,6 +47,7 @@ type Client struct { LDAP LDAPService License LicenseService Metrics MetricsService + Notification NotificationService OIDC OIDCService Permission PermissionService Policy PolicyService @@ -100,6 +101,7 @@ func NewClient(baseURL string, options ...ClientOption) (*Client, error) { client.LDAP = LDAPService{client: &client} client.License = LicenseService{client: &client} client.Metrics = MetricsService{client: &client} + client.Notification = NotificationService{client: &client} client.OIDC = OIDCService{client: &client} client.Permission = PermissionService{client: &client} client.Policy = PolicyService{client: &client} diff --git a/notification.go b/notification.go new file mode 100644 index 0000000..8e78149 --- /dev/null +++ b/notification.go @@ -0,0 +1,272 @@ +package dtrack + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/uuid" +) + +type NotificationService struct { + client *Client +} + +type NotificationPublisher struct { + UUID uuid.UUID `json:"uuid"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + PublisherClass string `json:"publisherClass"` + Template string `json:"template,omitempty"` + TemplateMIMEType string `json:"templateMimeType"` + DefaultPublisher bool `json:"defaultPublisher"` +} + +type NotificationRule struct { + UUID uuid.UUID `json:"uuid"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + NotifyChildren bool `json:"notifyChildren"` + LogSuccessfulPublish bool `json:"logSuccessfulPublish"` + Scope NotificationRuleScope `json:"scope"` + NotificationLevel NotificationRuleLevel `json:"notificationLevel,omitempty"` + NotifyOn NotificationRuleNotifyOn `json:"notifyOn,omitempty"` + TriggerType NotificationRuleTriggerType `json:"triggerType"` + Message string `json:"message,omitempty"` + PublisherConfig string `json:"publisherConfig,omitempty"` + ScheduleLastTriggeredAt int64 `json:"scheduleLastTriggeredAt"` + ScheduleNextTriggerAt int64 `json:"scheduleNextTriggerAt"` + ScheduleCron string `json:"scheduleCron"` + ScheduleSkipUnchanged bool `json:"scheduleSkipUnchanged"` + Publisher NotificationPublisher `json:"publisher"` + Projects []Project `json:"projects"` + Tags []Tag `json:"tags"` + Teams []Team `json:"teams"` +} + +type CreateScheduledNotificationRuleRequest struct { + Name string `json:"name"` + Scope NotificationRuleScope `json:"scope"` + NotificatonLevel NotificationRuleLevel `json:"notificationLevel"` + Publisher NotificationPublisher `json:"publisher"` +} + +type NotificationRuleScope string + +const ( + NotificationRuleScopeSystem NotificationRuleScope = "SYSTEM" + NotificationRuleScopePortfolio NotificationRuleScope = "PORTFOLIO" +) + +type NotificationRuleLevel string + +const ( + NotificationRuleLevelInformational NotificationRuleLevel = "INFORMATIONAL" + NotificationRuleLevelWarning NotificationRuleLevel = "WARNING" + NotificationRuleLevelError NotificationRuleLevel = "ERROR" +) + +type NotificationRuleNotifyOn string + +type NotificationRuleTriggerType string + +const ( + NotificationRuleTriggerTypeEvent NotificationRuleTriggerType = "EVENT" + NotificationRuleTriggerTypeSchedule NotificationRuleTriggerType = "SCHEDULE" +) + +func (ns NotificationService) GetAllPublishers(ctx context.Context) (ps []NotificationPublisher, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/publisher") + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ps) + return +} + +func (ns NotificationService) CreatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/publisher", withBody(publisher)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &p) + return +} + +func (ns NotificationService) UpdatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher", withBody(publisher)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &p) + return +} + +func (ns NotificationService) DeletePubisher(ctx context.Context, publisherUUID uuid.UUID) (err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/publisher/%s", publisherUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) RestoreDefaultTemplates(ctx context.Context) (err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/restoreDefaultTemplates") + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) TestRule(ctx context.Context, ruleUUID uuid.UUID) (err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/publisher/test/%s", ruleUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) TestSMTP(ctx context.Context, destination string) (err error) { + // TODO: Version Check + // TODO: `application/x-www-form-urlencoded` Request Content-Type. + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/test/smtp", withBody(destination)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) AddProjectToRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) RemoveProjectFromRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) AddTeamToRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +func (ns NotificationService) RemoveTeamFromRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} + +type GetAllRulesFilterOptions struct { + TriggerType NotificationRuleTriggerType +} + +func withGetAllRulesFilterOptions(filterOptions GetAllRulesFilterOptions) requestOption { + return func(req *http.Request) error { + query := req.URL.Query() + if len(filterOptions.TriggerType) > 0 { + query.Set("triggerType", string(filterOptions.TriggerType)) + } + req.URL.RawQuery = query.Encode() + return nil + } +} + +func (ns NotificationService) GetAllRules(ctx context.Context, po PageOptions, so SortOptions, filterOptions GetAllRulesFilterOptions) (p Page[NotificationRule], err error) { + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/rule", withPageOptions(po), withSortOptions(so), withGetAllRulesFilterOptions(filterOptions)) + if err != nil { + return + } + + res, err := ns.client.doRequest(req, &p.Items) + if err != nil { + return + } + + p.TotalCount = res.TotalCount + return +} + +func (ns NotificationService) CreateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule", withBody(ruleReq)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ruleRes) + return +} + +func (ns NotificationService) UpdateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/rule", withBody(ruleReq)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &ruleRes) + return +} + +func (ns NotificationService) DeleteRule(ctx context.Context, rule NotificationRule) (err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodDelete, "api/v1/notification/rule", withBody(rule)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, nil) + return +} + +func (ns NotificationService) CreateScheduledRule(ctx context.Context, schedule CreateScheduledNotificationRuleRequest) (r NotificationRule, err error) { + // TODO: Version Check + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule/scheduled", withBody(schedule)) + if err != nil { + return + } + + _, err = ns.client.doRequest(req, &r) + return +} From e270bad9dfc8ecd5ba9c4a502e74f4485c2e3d56 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Fri, 20 Feb 2026 22:27:09 +0000 Subject: [PATCH 2/9] Added minimum API version checks to /notification endpoint bindings. Signed-off-by: SolarFactories --- notification.go | 101 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 17 deletions(-) diff --git a/notification.go b/notification.go index 8e78149..32b5ac6 100644 --- a/notification.go +++ b/notification.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "github.com/google/uuid" ) @@ -76,7 +77,11 @@ const ( ) func (ns NotificationService) GetAllPublishers(ctx context.Context) (ps []NotificationPublisher, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/publisher") if err != nil { return @@ -87,7 +92,11 @@ func (ns NotificationService) GetAllPublishers(ctx context.Context) (ps []Notifi } func (ns NotificationService) CreatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/publisher", withBody(publisher)) if err != nil { return @@ -98,7 +107,11 @@ func (ns NotificationService) CreatePublisher(ctx context.Context, publisher Not } func (ns NotificationService) UpdatePublisher(ctx context.Context, publisher NotificationPublisher) (p NotificationPublisher, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher", withBody(publisher)) if err != nil { return @@ -109,7 +122,11 @@ func (ns NotificationService) UpdatePublisher(ctx context.Context, publisher Not } func (ns NotificationService) DeletePubisher(ctx context.Context, publisherUUID uuid.UUID) (err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/publisher/%s", publisherUUID.String())) if err != nil { return @@ -120,7 +137,11 @@ func (ns NotificationService) DeletePubisher(ctx context.Context, publisherUUID } func (ns NotificationService) RestoreDefaultTemplates(ctx context.Context) (err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.6.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/restoreDefaultTemplates") if err != nil { return @@ -131,7 +152,11 @@ func (ns NotificationService) RestoreDefaultTemplates(ctx context.Context) (err } func (ns NotificationService) TestRule(ctx context.Context, ruleUUID uuid.UUID) (err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.12.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/publisher/test/%s", ruleUUID.String())) if err != nil { return @@ -142,9 +167,14 @@ func (ns NotificationService) TestRule(ctx context.Context, ruleUUID uuid.UUID) } func (ns NotificationService) TestSMTP(ctx context.Context, destination string) (err error) { - // TODO: Version Check - // TODO: `application/x-www-form-urlencoded` Request Content-Type. - req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/test/smtp", withBody(destination)) + err = ns.client.assertServerVersionAtLeast("3.4.0") + if err != nil { + return + } + values := url.Values{} + values.Set("destination", destination) + + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/publisher/test/smtp", withBody(values)) if err != nil { return } @@ -154,7 +184,11 @@ func (ns NotificationService) TestSMTP(ctx context.Context, destination string) } func (ns NotificationService) AddProjectToRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) if err != nil { return @@ -165,7 +199,11 @@ func (ns NotificationService) AddProjectToRule(ctx context.Context, ruleUUID, pr } func (ns NotificationService) RemoveProjectFromRule(ctx context.Context, ruleUUID, projectUUID uuid.UUID) (r NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/project/%s", ruleUUID.String(), projectUUID.String())) if err != nil { return @@ -176,7 +214,11 @@ func (ns NotificationService) RemoveProjectFromRule(ctx context.Context, ruleUUI } func (ns NotificationService) AddTeamToRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.7.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) if err != nil { return @@ -187,7 +229,11 @@ func (ns NotificationService) AddTeamToRule(ctx context.Context, ruleUUID, teamU } func (ns NotificationService) RemoveTeamFromRule(ctx context.Context, ruleUUID, teamUUID uuid.UUID) (r NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.7.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/notification/rule/%s/team/%s", ruleUUID.String(), teamUUID.String())) if err != nil { return @@ -213,6 +259,11 @@ func withGetAllRulesFilterOptions(filterOptions GetAllRulesFilterOptions) reques } func (ns NotificationService) GetAllRules(ctx context.Context, po PageOptions, so SortOptions, filterOptions GetAllRulesFilterOptions) (p Page[NotificationRule], err error) { + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodGet, "api/v1/notification/rule", withPageOptions(po), withSortOptions(so), withGetAllRulesFilterOptions(filterOptions)) if err != nil { return @@ -228,7 +279,11 @@ func (ns NotificationService) GetAllRules(ctx context.Context, po PageOptions, s } func (ns NotificationService) CreateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule", withBody(ruleReq)) if err != nil { return @@ -239,7 +294,11 @@ func (ns NotificationService) CreateRule(ctx context.Context, ruleReq Notificati } func (ns NotificationService) UpdateRule(ctx context.Context, ruleReq NotificationRule) (ruleRes NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPost, "api/v1/notification/rule", withBody(ruleReq)) if err != nil { return @@ -250,7 +309,11 @@ func (ns NotificationService) UpdateRule(ctx context.Context, ruleReq Notificati } func (ns NotificationService) DeleteRule(ctx context.Context, rule NotificationRule) (err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("3.2.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodDelete, "api/v1/notification/rule", withBody(rule)) if err != nil { return @@ -261,7 +324,11 @@ func (ns NotificationService) DeleteRule(ctx context.Context, rule NotificationR } func (ns NotificationService) CreateScheduledRule(ctx context.Context, schedule CreateScheduledNotificationRuleRequest) (r NotificationRule, err error) { - // TODO: Version Check + err = ns.client.assertServerVersionAtLeast("4.13.0") + if err != nil { + return + } + req, err := ns.client.newRequest(ctx, http.MethodPut, "api/v1/notification/rule/scheduled", withBody(schedule)) if err != nil { return From 818bcb1a9745bd18926ba829af1ef8ecf0975dbb Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Fri, 20 Feb 2026 22:59:24 +0000 Subject: [PATCH 3/9] Added test for Publisher CRUD operations. Signed-off-by: SolarFactories --- notification.go | 2 +- notification_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 notification_test.go diff --git a/notification.go b/notification.go index 32b5ac6..36836ea 100644 --- a/notification.go +++ b/notification.go @@ -121,7 +121,7 @@ func (ns NotificationService) UpdatePublisher(ctx context.Context, publisher Not return } -func (ns NotificationService) DeletePubisher(ctx context.Context, publisherUUID uuid.UUID) (err error) { +func (ns NotificationService) DeletePublisher(ctx context.Context, publisherUUID uuid.UUID) (err error) { err = ns.client.assertServerVersionAtLeast("4.6.0") if err != nil { return diff --git a/notification_test.go b/notification_test.go new file mode 100644 index 0000000..50b06bd --- /dev/null +++ b/notification_test.go @@ -0,0 +1,86 @@ +package dtrack + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPublishers(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + // Create + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Publisher", + Description: "Test_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Template", + }) + { + require.NoError(t, err) + require.NotZero(t, publisher.UUID) + require.Equal(t, publisher.Name, "Test_Publisher") + require.Equal(t, publisher.Description, "Test_Description") + require.Equal(t, publisher.PublisherClass, "org.dependencytrack.notification.publisher.ConsolePublisher") + require.Equal(t, publisher.TemplateMIMEType, "text/plain") + require.Equal(t, publisher.Template, "Test_Template") + require.Equal(t, publisher.DefaultPublisher, false) + } + // Update + { + updatedReq := publisher + updatedReq.Description = "Test_Updated_Description" + updated, err := client.Notification.UpdatePublisher(ctx, updatedReq) + require.NoError(t, err) + require.Equal(t, updated.UUID, publisher.UUID) + require.Equal(t, updated.Name, publisher.Name) + require.Equal(t, updated.Description, "Test_Updated_Description") + require.Equal(t, updated.PublisherClass, publisher.PublisherClass) + require.Equal(t, updated.Template, publisher.Template) + require.Equal(t, updated.TemplateMIMEType, publisher.TemplateMIMEType) + require.Equal(t, updated.DefaultPublisher, publisher.DefaultPublisher) + } + // Fetch + { + allPublishers, err := client.Notification.GetAllPublishers(ctx) + require.NoError(t, err) + found := NotificationPublisher{} + for _, pub := range allPublishers { + if pub.UUID == publisher.UUID { + found = pub + break + } + } + require.Equal(t, found.UUID, publisher.UUID) + require.Equal(t, found.Name, publisher.Name) + require.Equal(t, found.Description, "Test_Updated_Description") + require.Equal(t, found.PublisherClass, publisher.PublisherClass) + require.Equal(t, found.Template, publisher.Template) + require.Equal(t, found.TemplateMIMEType, publisher.TemplateMIMEType) + require.Equal(t, found.DefaultPublisher, publisher.DefaultPublisher) + } + // Delete + { + err := client.Notification.DeletePublisher(ctx, publisher.UUID) + require.NoError(t, err) + } + // Check absence + { + allPublishers, err := client.Notification.GetAllPublishers(ctx) + require.NoError(t, err) + found := NotificationPublisher{} + for _, pub := range allPublishers { + if pub.UUID == publisher.UUID { + found = pub + break + } + } + require.Zero(t, found.UUID) + } +} From 3666d5036556cf5d14308005b7750d65f02f1fda Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 23 Feb 2026 20:33:30 +0000 Subject: [PATCH 4/9] Added tests for Notification Rules. Signed-off-by: SolarFactories --- notification.go | 18 ++++---- notification_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/notification.go b/notification.go index 36836ea..00b6ea3 100644 --- a/notification.go +++ b/notification.go @@ -31,18 +31,18 @@ type NotificationRule struct { LogSuccessfulPublish bool `json:"logSuccessfulPublish"` Scope NotificationRuleScope `json:"scope"` NotificationLevel NotificationRuleLevel `json:"notificationLevel,omitempty"` - NotifyOn NotificationRuleNotifyOn `json:"notifyOn,omitempty"` + NotifyOn []NotificationRuleNotifyOn `json:"notifyOn,omitempty"` TriggerType NotificationRuleTriggerType `json:"triggerType"` Message string `json:"message,omitempty"` PublisherConfig string `json:"publisherConfig,omitempty"` - ScheduleLastTriggeredAt int64 `json:"scheduleLastTriggeredAt"` - ScheduleNextTriggerAt int64 `json:"scheduleNextTriggerAt"` - ScheduleCron string `json:"scheduleCron"` - ScheduleSkipUnchanged bool `json:"scheduleSkipUnchanged"` - Publisher NotificationPublisher `json:"publisher"` - Projects []Project `json:"projects"` - Tags []Tag `json:"tags"` - Teams []Team `json:"teams"` + ScheduleLastTriggeredAt int64 `json:"scheduleLastTriggeredAt,omitempty"` + ScheduleNextTriggerAt int64 `json:"scheduleNextTriggerAt,omitempty"` + ScheduleCron string `json:"scheduleCron,omitempty"` + ScheduleSkipUnchanged bool `json:"scheduleSkipUnchanged,omitempty"` + Publisher NotificationPublisher `json:"publisher,omitempty"` + Projects []Project `json:"projects.omitempty"` + Tags []Tag `json:"tags,omitempty"` + Teams []Team `json:"teams,omitempty"` } type CreateScheduledNotificationRuleRequest struct { diff --git a/notification_test.go b/notification_test.go index 50b06bd..9176cc2 100644 --- a/notification_test.go +++ b/notification_test.go @@ -84,3 +84,110 @@ func TestPublishers(t *testing.T) { require.Zero(t, found.UUID) } } + +func TestRules(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + // Create + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + { + require.NoError(t, err) + require.NotZero(t, rule.UUID) + require.Equal(t, rule.Name, "Test_Rule_Name") + require.Equal(t, rule.Scope, NotificationRuleScopePortfolio) + require.Equal(t, rule.TriggerType, NotificationRuleTriggerTypeEvent) + require.Equal(t, rule.Enabled, true) + require.Equal(t, rule.NotifyChildren, true) + require.Equal(t, rule.LogSuccessfulPublish, false) + require.Empty(t, rule.NotificationLevel) + require.Empty(t, rule.NotifyOn) + require.Empty(t, rule.Message) + require.Empty(t, rule.PublisherConfig) + require.Empty(t, rule.ScheduleLastTriggeredAt) + require.Empty(t, rule.ScheduleNextTriggerAt) + require.Empty(t, rule.ScheduleCron) + require.Empty(t, rule.ScheduleSkipUnchanged) + require.Equal(t, rule.Publisher.UUID, publisher.UUID) + require.Empty(t, rule.Projects) + require.Empty(t, rule.Tags) + require.Empty(t, rule.Teams) + } + // Update + { + updatedReq := rule + updatedReq.PublisherConfig = "{\"Key\": \"Publisher Config\"}" + updatedRes, err := client.Notification.UpdateRule(ctx, updatedReq) + require.NoError(t, err) + require.Equal(t, updatedRes, updatedReq) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Equal(t, found.UUID, rule.UUID) + require.Equal(t, found.Name, rule.Name) + require.Equal(t, found.Enabled, rule.Enabled) + require.Equal(t, found.NotifyChildren, rule.NotifyChildren) + require.Equal(t, found.LogSuccessfulPublish, rule.LogSuccessfulPublish) + require.Equal(t, found.Scope, rule.Scope) + require.Equal(t, found.NotificationLevel, rule.NotificationLevel) + require.Equal(t, found.NotifyOn, rule.NotifyOn) + require.Equal(t, found.TriggerType, rule.TriggerType) + require.Equal(t, found.Message, rule.Message) + require.Equal(t, found.PublisherConfig, "{\"Key\": \"Publisher Config\"}") + require.Equal(t, found.ScheduleLastTriggeredAt, rule.ScheduleLastTriggeredAt) + require.Equal(t, found.ScheduleNextTriggerAt, rule.ScheduleNextTriggerAt) + require.Equal(t, found.ScheduleCron, rule.ScheduleCron) + require.Equal(t, found.ScheduleSkipUnchanged, rule.ScheduleSkipUnchanged) + require.Equal(t, found.Publisher, rule.Publisher) + require.Equal(t, found.Projects, rule.Projects) + require.Equal(t, found.Tags, rule.Tags) + require.Equal(t, found.Teams, rule.Teams) + } + // Delete + { + err := client.Notification.DeleteRule(ctx, rule) + require.NoError(t, err) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Zero(t, found.UUID) + } +} From 6dcb235131096537d2149d1ecd055e89d154dfad Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 23 Feb 2026 21:03:27 +0000 Subject: [PATCH 5/9] Added tests for adding project to notification rule. Fixed error from dot rather comma within field tag for Projects. Signed-off-by: SolarFactories --- notification.go | 2 +- notification_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/notification.go b/notification.go index 00b6ea3..3c84c21 100644 --- a/notification.go +++ b/notification.go @@ -40,7 +40,7 @@ type NotificationRule struct { ScheduleCron string `json:"scheduleCron,omitempty"` ScheduleSkipUnchanged bool `json:"scheduleSkipUnchanged,omitempty"` Publisher NotificationPublisher `json:"publisher,omitempty"` - Projects []Project `json:"projects.omitempty"` + Projects []Project `json:"projects,omitempty"` Tags []Tag `json:"tags,omitempty"` Teams []Team `json:"teams,omitempty"` } diff --git a/notification_test.go b/notification_test.go index 9176cc2..524b2d8 100644 --- a/notification_test.go +++ b/notification_test.go @@ -191,3 +191,81 @@ func TestRules(t *testing.T) { require.Zero(t, found.UUID) } } + +func TestRuleProjects(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + PermissionPortfolioManagement, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Projects_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.ConsolePublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Projects_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + require.NoError(t, err) + project, err := client.Project.Create(ctx, Project{ + Name: "Test_Rule_Projects_Project", + }) + require.NoError(t, err) + // Add Project + { + updated, err := client.Notification.AddProjectToRule(ctx, rule.UUID, project.UUID) + require.NoError(t, err) + require.Equal(t, updated.UUID, rule.UUID) + require.Equal(t, updated.Projects, []Project{project}) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.NotZero(t, found.UUID) + require.Empty(t, project.Tags) + require.Empty(t, project.Properties) + + project.Tags = nil + project.Properties = nil + require.Equal(t, found.Projects, []Project{project}) + } + // Remove Project + { + updated, err := client.Notification.RemoveProjectFromRule(ctx, rule.UUID, project.UUID) + require.NoError(t, err) + require.Equal(t, updated, rule) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, found.Projects) + } +} From b82901019da995a4178a50ab989ab8de8df934c6 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 23 Feb 2026 21:30:48 +0000 Subject: [PATCH 6/9] Added test for adding team to notification rules. Signed-off-by: SolarFactories --- notification_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/notification_test.go b/notification_test.go index 524b2d8..473d01e 100644 --- a/notification_test.go +++ b/notification_test.go @@ -269,3 +269,84 @@ func TestRuleProjects(t *testing.T) { require.Empty(t, found.Projects) } } + +func TestRuleTeams(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + PermissionAccessManagement, + }, + }) + publisher, err := client.Notification.CreatePublisher(ctx, NotificationPublisher{ + Name: "Test_Rule_Tags_Publisher", + Description: "Test_Rule_Description", + PublisherClass: "org.dependencytrack.notification.publisher.SendMailPublisher", + TemplateMIMEType: "text/plain", + Template: "Test_Rule_Template", + }) + require.NoError(t, err) + rule, err := client.Notification.CreateRule(ctx, NotificationRule{ + Name: "Test_Rule_Tags_Name", + Scope: NotificationRuleScopePortfolio, + TriggerType: NotificationRuleTriggerTypeEvent, + Publisher: publisher, + }) + require.NoError(t, err) + team, err := client.Team.Create(ctx, Team{ + Name: "Test_Rule_Teams_Team", + }) + require.NoError(t, err) + // Add Team + { + updated, err := client.Notification.AddTeamToRule(ctx, rule.UUID, team.UUID) + require.NoError(t, err) + + require.Empty(t, team.APIKeys) + require.Empty(t, team.MappedOIDCGroups) + team.APIKeys = nil + team.MappedOIDCGroups = nil + + require.Equal(t, updated.Teams, []Team{team}) + updated.Teams = []Team{} + require.Equal(t, updated, rule) + } + // Fetch + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, team.Permissions) + team.Permissions = nil + require.Equal(t, found.Teams, []Team{team}) + } + // Remove Team + { + updated, err := client.Notification.RemoveTeamFromRule(ctx, rule.UUID, team.UUID) + require.NoError(t, err) + require.Equal(t, updated, rule) + } + // Check Absence + { + allRules, err := FetchAll(func(po PageOptions) (Page[NotificationRule], error) { + return client.Notification.GetAllRules(ctx, po, SortOptions{}, GetAllRulesFilterOptions{}) + }) + require.NoError(t, err) + found := NotificationRule{} + for _, rule_ := range allRules { + if rule_.UUID == rule.UUID { + found = rule_ + break + } + } + require.Empty(t, found.Teams) + } +} From 43933b74981f05928465912a638f89c902dddfc7 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 22 Mar 2026 20:41:47 +0000 Subject: [PATCH 7/9] Fix typo in CreateScheduledNotificationRuleRequest from NotificatonLevel to NotificationLevel. Signed-off-by: SolarFactories --- notification.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notification.go b/notification.go index 3c84c21..27a434b 100644 --- a/notification.go +++ b/notification.go @@ -46,10 +46,10 @@ type NotificationRule struct { } type CreateScheduledNotificationRuleRequest struct { - Name string `json:"name"` - Scope NotificationRuleScope `json:"scope"` - NotificatonLevel NotificationRuleLevel `json:"notificationLevel"` - Publisher NotificationPublisher `json:"publisher"` + Name string `json:"name"` + Scope NotificationRuleScope `json:"scope"` + NotificationLevel NotificationRuleLevel `json:"notificationLevel"` + Publisher NotificationPublisher `json:"publisher"` } type NotificationRuleScope string From 2e397307bd1010adfc9c9ecbc59a576b814d350d Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 30 Mar 2026 22:33:40 +0100 Subject: [PATCH 8/9] Introduce Publisher struct, used for schedule notification rules, as it just uses a UUID, differing from NotificationPublisher. Signed-off-by: SolarFactories --- notification.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/notification.go b/notification.go index 27a434b..18c764e 100644 --- a/notification.go +++ b/notification.go @@ -49,7 +49,11 @@ type CreateScheduledNotificationRuleRequest struct { Name string `json:"name"` Scope NotificationRuleScope `json:"scope"` NotificationLevel NotificationRuleLevel `json:"notificationLevel"` - Publisher NotificationPublisher `json:"publisher"` + Publisher Publisher `json:"publisher"` +} + +type Publisher struct { + UUID uuid.UUID `json:"uuid"` } type NotificationRuleScope string From 4044f1fd0f86be93fb5438fdc4f04bec4757a1f4 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 12 Apr 2026 17:02:50 +0100 Subject: [PATCH 9/9] Add missing Accept Content-Type on OIDCService.Login for text/plain. Signed-off-by: SolarFactories --- oidc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc.go b/oidc.go index 5d1faf8..3b49077 100644 --- a/oidc.go +++ b/oidc.go @@ -243,7 +243,7 @@ func (s OIDCService) Login(ctx context.Context, tokens OIDCTokens) (token string body.Set("idToken", tokens.ID) body.Set("accessToken", tokens.Access) - req, err := s.client.newRequest(ctx, http.MethodPost, "api/v1/user/oidc/login", withBody(body)) + req, err := s.client.newRequest(ctx, http.MethodPost, "api/v1/user/oidc/login", withBody(body), withAcceptContentType("text/plain")) if err != nil { return }