Skip to content

Commit 9f05d31

Browse files
authored
Merge pull request #31 from grafana/get-alert-groups-by-teamID
alert_groups: add additional options as query parameters
2 parents 034f790 + ee19829 commit 9f05d31

File tree

2 files changed

+203
-1
lines changed

2 files changed

+203
-1
lines changed

alert_group.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package aapi
33
import (
44
"fmt"
55
"net/http"
6+
"regexp"
7+
"time"
68
)
79

810
// AlertGroupService handles requests to the on-call alert_groups endpoint.
@@ -41,20 +43,85 @@ type AlertGroup struct {
4143
Permalinks map[string]string `json:"permalinks"`
4244
}
4345

46+
// validateTimeRange validates if the time range string matches the expected format
47+
// Expected format: %Y-%m-%dT%H:%M:%S_%Y-%m-%dT%H:%M:%S
48+
func validateTimeRange(timeRange string) error {
49+
if timeRange == "" {
50+
return nil
51+
}
52+
53+
// Check if the string matches the expected format
54+
pattern := `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}_\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$`
55+
matched, err := regexp.MatchString(pattern, timeRange)
56+
if err != nil {
57+
return fmt.Errorf("error validating time range format: %v", err)
58+
}
59+
if !matched {
60+
return fmt.Errorf("invalid time range format. Expected format: YYYY-MM-DDThh:mm:ss_YYYY-MM-DDThh:mm:ss")
61+
}
62+
63+
// Split the time range into start and end times
64+
times := regexp.MustCompile(`_`).Split(timeRange, 2)
65+
if len(times) != 2 {
66+
return fmt.Errorf("invalid time range format: missing separator '_'")
67+
}
68+
69+
// Parse both times to ensure they are valid
70+
startTime, err := time.Parse("2006-01-02T15:04:05", times[0])
71+
if err != nil {
72+
return fmt.Errorf("invalid start time format: %v", err)
73+
}
74+
75+
endTime, err := time.Parse("2006-01-02T15:04:05", times[1])
76+
if err != nil {
77+
return fmt.Errorf("invalid end time format: %v", err)
78+
}
79+
80+
// Validate that end time is after start time
81+
if endTime.Before(startTime) {
82+
return fmt.Errorf("end time must be after start time")
83+
}
84+
85+
return nil
86+
}
87+
4488
// ListAlertGroupOptions represent filter options supported by the on-call alert_groups API.
4589
type ListAlertGroupOptions struct {
4690
ListOptions
4791
AlertGroupID string `url:"alert_group_id,omitempty" json:"alert_group_id,omitempty"`
4892
RouteID string `url:"route_id,omitempty" json:"route_id,omitempty"`
4993
IntegrationID string `url:"integration_id,omitempty" json:"integration_id,omitempty" `
5094
State string `url:"state,omitempty" json:"state,omitempty" `
51-
Name string `url:"name,omitempty" json:"name,omitempty"`
95+
TeamID string `url:"team_id,omitempty" json:"team_id,omitempty"`
96+
// StartedAt is a time range in ISO 8601 format with start and end timestamps separated by underscore.
97+
// Expected format: %Y-%m-%dT%H:%M:%S_%Y-%m-%dT%H:%M:%S
98+
// Example: "2024-03-20T10:00:00_2024-03-21T10:00:00"
99+
StartedAt string `url:"started_at,omitempty" json:"started_at,omitempty"`
100+
// Labels are matching labels that can be passed multiple times.
101+
// Expected format: key1:value1
102+
// Example: ["env:prod", "severity:high"]
103+
Labels []string `url:"label,omitempty" json:"label,omitempty"`
104+
Name string `url:"name,omitempty" json:"name,omitempty"`
105+
}
106+
107+
// Validate checks if the options are valid
108+
func (o *ListAlertGroupOptions) Validate() error {
109+
if err := validateTimeRange(o.StartedAt); err != nil {
110+
return err
111+
}
112+
return nil
52113
}
53114

54115
// ListAlertGroups fetches all on-call alerts for authorized organization.
55116
//
56117
// https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/
57118
func (service *AlertGroupService) ListAlertGroups(opt *ListAlertGroupOptions) (*PaginatedAlertGroupsResponse, *http.Response, error) {
119+
if opt != nil {
120+
if err := opt.Validate(); err != nil {
121+
return nil, nil, err
122+
}
123+
}
124+
58125
u := fmt.Sprintf("%s/", service.url)
59126

60127
req, err := service.client.NewRequest("GET", u, opt)

alert_group_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,138 @@ func TestListAlertGroup(t *testing.T) {
7373
t.Errorf(" returned\n %+v, \nwant\n %+v", alerts, want)
7474
}
7575
}
76+
77+
func TestValidateTimeRange(t *testing.T) {
78+
tests := []struct {
79+
name string
80+
timeRange string
81+
wantErr bool
82+
}{
83+
{
84+
name: "valid time range",
85+
timeRange: "2024-03-20T10:00:00_2024-03-21T10:00:00",
86+
wantErr: false,
87+
},
88+
{
89+
name: "empty time range",
90+
timeRange: "",
91+
wantErr: false,
92+
},
93+
{
94+
name: "invalid format - missing separator",
95+
timeRange: "2024-03-20T10:00:002024-03-21T10:00:00",
96+
wantErr: true,
97+
},
98+
{
99+
name: "invalid format - wrong date format",
100+
timeRange: "2024/03/20T10:00:00_2024/03/21T10:00:00",
101+
wantErr: true,
102+
},
103+
{
104+
name: "invalid time - end before start",
105+
timeRange: "2024-03-21T10:00:00_2024-03-20T10:00:00",
106+
wantErr: true,
107+
},
108+
{
109+
name: "invalid time - invalid hour",
110+
timeRange: "2024-03-20T25:00:00_2024-03-21T10:00:00",
111+
wantErr: true,
112+
},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
err := validateTimeRange(tt.timeRange)
118+
if (err != nil) != tt.wantErr {
119+
t.Errorf("validateTimeRange() error = %v, wantErr %v", err, tt.wantErr)
120+
}
121+
})
122+
}
123+
}
124+
125+
func TestListAlertGroupsValidation(t *testing.T) {
126+
tests := []struct {
127+
name string
128+
options *ListAlertGroupOptions
129+
wantErr bool
130+
}{
131+
{
132+
name: "valid options",
133+
options: &ListAlertGroupOptions{
134+
StartedAt: "2024-03-20T10:00:00_2024-03-21T10:00:00",
135+
},
136+
wantErr: false,
137+
},
138+
{
139+
name: "invalid time range",
140+
options: &ListAlertGroupOptions{
141+
StartedAt: "invalid-time-range",
142+
},
143+
wantErr: true,
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
err := tt.options.Validate()
150+
if (err != nil) != tt.wantErr {
151+
t.Errorf("ListAlertGroupOptions.Validate() error = %v, wantErr %v", err, tt.wantErr)
152+
}
153+
})
154+
}
155+
}
156+
157+
func TestListAlertGroupQueryURL(t *testing.T) {
158+
tests := []struct {
159+
name string
160+
options *ListAlertGroupOptions
161+
expectedURL string
162+
}{
163+
{
164+
name: "single label",
165+
options: &ListAlertGroupOptions{
166+
Labels: []string{"env:prod"},
167+
},
168+
expectedURL: "/api/v1/alert_groups/?label=env%3Aprod",
169+
},
170+
{
171+
name: "multiple labels",
172+
options: &ListAlertGroupOptions{
173+
Labels: []string{"env:prod", "severity:high", "team:backend"},
174+
},
175+
expectedURL: "/api/v1/alert_groups/?label=env%3Aprod&label=severity%3Ahigh&label=team%3Abackend",
176+
},
177+
{
178+
name: "empty labels",
179+
options: &ListAlertGroupOptions{
180+
Labels: []string{},
181+
},
182+
expectedURL: "/api/v1/alert_groups/",
183+
},
184+
}
185+
186+
for _, tt := range tests {
187+
t.Run(tt.name, func(t *testing.T) {
188+
mux, server, client := setup(t)
189+
defer teardown(server)
190+
191+
var capturedURL string
192+
mux.HandleFunc("/api/v1/alert_groups/", func(w http.ResponseWriter, r *http.Request) {
193+
capturedURL = r.URL.String()
194+
t.Logf("Request URL: %s", capturedURL)
195+
w.WriteHeader(http.StatusOK)
196+
// Add minimal response body
197+
fmt.Fprint(w, `{"count": 0, "next": null, "previous": null, "results": []}`)
198+
})
199+
200+
_, _, err := client.AlertGroups.ListAlertGroups(tt.options)
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
205+
if capturedURL != tt.expectedURL {
206+
t.Errorf("Request URL = %v, want %v", capturedURL, tt.expectedURL)
207+
}
208+
})
209+
}
210+
}

0 commit comments

Comments
 (0)