Skip to content

Commit

Permalink
fix: Ignore ScheduledAction's omitempty when SupportHours is specified
Browse files Browse the repository at this point in the history
Fixes PagerDuty#468. When `Service.SupportHours` is specified, `Service.ScheduledActions` is required, otherwise the PagerDuty API returns the following error:

```
HTTP response failed with status code 400, message: Invalid Input Provided (code: 2001): Scheduled actions is required.
```

This implements a custom `MarshalJSON` function for `Service`, which ignores `ScheduledActions`'s `omitempty` when `SupportHours` is specified, and enforces it otherwise.
  • Loading branch information
pw-mdb committed Feb 5, 2025
1 parent 9831333 commit df5dcfa
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
82 changes: 82 additions & 0 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pagerduty

import (
"context"
"encoding/json"
"fmt"
"net/http"

Expand Down Expand Up @@ -76,6 +77,7 @@ type ServiceRuleActions struct {
}

// Service represents something you monitor (like a web service, email service, or database service).
// Do not update this struct without also updating serviceToMarshal!
type Service struct {
APIObject
Name string `json:"name,omitempty"`
Expand All @@ -100,6 +102,86 @@ type Service struct {
AutoPauseNotificationsParameters *AutoPauseNotificationsParameters `json:"auto_pause_notifications_parameters,omitempty"`
}

// A Service that is going to be marshaled.
// Do not update this struct without also updating Service!
type serviceToMarshal struct {
APIObject
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
AutoResolveTimeout *uint `json:"auto_resolve_timeout,omitempty"`
AcknowledgementTimeout *uint `json:"acknowledgement_timeout,omitempty"`
CreateAt string `json:"created_at,omitempty"`
Status string `json:"status,omitempty"`
LastIncidentTimestamp string `json:"last_incident_timestamp,omitempty"`
Integrations []Integration `json:"integrations,omitempty"`
EscalationPolicy EscalationPolicy `json:"escalation_policy,omitempty"`
Teams []Team `json:"teams,omitempty"`
IncidentUrgencyRule *IncidentUrgencyRule `json:"incident_urgency_rule,omitempty"`
SupportHours *SupportHours `json:"support_hours,omitempty"`
// Because of the logic in MarshalJSON, ScheduledActions cannot have omitempty.
ScheduledActions []ScheduledAction `json:"scheduled_actions"`
AlertCreation string `json:"alert_creation,omitempty"`
AlertGrouping string `json:"alert_grouping,omitempty"`
AlertGroupingTimeout *uint `json:"alert_grouping_timeout,omitempty"`
AlertGroupingParameters *AlertGroupingParameters `json:"alert_grouping_parameters,omitempty"`
ResponsePlay *APIObject `json:"response_play,omitempty"`
Addons []Addon `json:"addons,omitempty"`
AutoPauseNotificationsParameters *AutoPauseNotificationsParameters `json:"auto_pause_notifications_parameters,omitempty"`
}

func (s Service) MarshalJSON() ([]byte, error) {
// Marshal and unmarshal the Service so that we can get the data that would be written to the wire.
sm := serviceToMarshal{
APIObject: s.APIObject,
Name: s.Name,
Description: s.Description,
AutoResolveTimeout: s.AutoResolveTimeout,
AcknowledgementTimeout: s.AcknowledgementTimeout,
CreateAt: s.CreateAt,
Status: s.Status,
LastIncidentTimestamp: s.LastIncidentTimestamp,
Integrations: s.Integrations,
EscalationPolicy: s.EscalationPolicy,
Teams: s.Teams,
IncidentUrgencyRule: s.IncidentUrgencyRule,
SupportHours: s.SupportHours,
ScheduledActions: s.ScheduledActions,
AlertCreation: s.AlertCreation,
AlertGrouping: s.AlertGrouping,
AlertGroupingTimeout: s.AlertGroupingTimeout,
AlertGroupingParameters: s.AlertGroupingParameters,
ResponsePlay: s.ResponsePlay,
Addons: s.Addons,
AutoPauseNotificationsParameters: s.AutoPauseNotificationsParameters,
}

sb, err := json.Marshal(sm)
if err != nil {
return nil, err
}

sd := map[string]interface{}{}
err = json.Unmarshal(sb, &sd)
if err != nil {
return nil, err
}

// If support_hours is specified, scheduled_actions also has to be specified.
if sd["support_hours"] != nil {
if sd["scheduled_actions"] == nil {
// This marshalls to JSON's "null".
sd["scheduled_actions"] = interface{}(nil)
}
// Otherwise, enforce Service.ScheduledActions's omitempty behavior.
} else if sd["scheduled_actions"] == nil {
delete(sd, "scheduled_actions")
} else if v, ok := sd["scheduled_actions"].([]interface{}); ok && len(v) == 0 {
delete(sd, "scheduled_actions")
}

return json.Marshal(sd)
}

// AutoPauseNotificationsParameters defines how alerts on the service will be automatically paused
type AutoPauseNotificationsParameters struct {
Enabled bool `json:"enabled"`
Expand Down
100 changes: 100 additions & 0 deletions service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pagerduty
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"testing"
Expand Down Expand Up @@ -141,12 +142,54 @@ func TestService_Create(t *testing.T) {

mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")

defer r.Body.Close()
body, err := io.ReadAll(r.Body)
testEqual(t, nil, err)
testEqual(t, []uint8(`{"service":{"escalation_policy":{"teams":null},"name":"foo"}}`), body)

_, _ = w.Write([]byte(`{"service": {"id": "1","name":"foo"}}`))
})

client := defaultTestClient(server.URL, "foo")
input := Service{
Name: "foo",
}
res, err := client.CreateService(input)

want := &Service{
APIObject: APIObject{
ID: "1",
},
Name: "foo",
}

if err != nil {
t.Fatal(err)
}
testEqual(t, want, res)
}

// Create Service with empty ScheduledActions
func TestService_CreateWithEmptyScheduledActions(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")

defer r.Body.Close()
body, err := io.ReadAll(r.Body)
testEqual(t, nil, err)
testEqual(t, []uint8(`{"service":{"escalation_policy":{"teams":null},"name":"foo"}}`), body)

_, _ = w.Write([]byte(`{"service": {"id": "1","name":"foo"}}`))
})

client := defaultTestClient(server.URL, "foo")
input := Service{
Name: "foo",
ScheduledActions: []ScheduledAction{},
}
res, err := client.CreateService(input)

Expand All @@ -163,6 +206,63 @@ func TestService_Create(t *testing.T) {
testEqual(t, want, res)
}

// Create Service with SupportHours
func TestService_CreateWithSupportHours(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")

defer r.Body.Close()
body, err := io.ReadAll(r.Body)
testEqual(t, nil, err)
testEqual(t, []uint8(`{"service":{"escalation_policy":{"teams":null},"name":"foo","scheduled_actions":null,"support_hours":{"days_of_week":[1]}}}`), body)

_, _ = w.Write([]byte(`{"service": {"id": "1","name":"foo"}}`))
})

client := defaultTestClient(server.URL, "foo")
input := Service{
Name: "foo",
SupportHours: &SupportHours{DaysOfWeek: []uint{1}},
}
_, err := client.CreateService(input)

if err != nil {
t.Fatal(err)
}
}

// Create Service with SupportHours and empty ScheduledActions
func TestService_CreateWithSupportHoursAndEmptyScheduledActions(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
defer r.Body.Close()

body, err := io.ReadAll(r.Body)
testEqual(t, nil, err)
testEqual(t, []uint8(`{"service":{"escalation_policy":{"teams":null},"name":"foo","scheduled_actions":[],"support_hours":{"days_of_week":[1]}}}`), body)

_, _ = w.Write([]byte(`{"service": {"id": "1","name":"foo"}}`))
})

client := defaultTestClient(server.URL, "foo")
input := Service{
Name: "foo",
SupportHours: &SupportHours{DaysOfWeek: []uint{1}},
ScheduledActions: []ScheduledAction{},
}
_, err := client.CreateService(input)

if err != nil {
t.Fatal(err)
}
}

// Create Service with AlertGroupingParameters of type time
func TestService_CreateWithAlertGroupParamsTime(t *testing.T) {
setup()
Expand Down

0 comments on commit df5dcfa

Please sign in to comment.