Loading audit_events.go +63 −18 Original line number Diff line number Diff line package gitlab import ( "encoding/json" "net/http" "time" ) Loading Loading @@ -85,6 +86,8 @@ type AuditEventDetails struct { Add string `json:"add"` As string `json:"as"` Change string `json:"change"` ChangeObject json.RawMessage `json:"-"` Changes []AuditEventChange `json:"changes"` From string `json:"from"` To string `json:"to"` Remove string `json:"remove"` Loading @@ -101,6 +104,48 @@ type AuditEventDetails struct { EventName string `json:"event_name"` } // UnmarshalJSON implements the json.Unmarshaler interface. // // The audit events API returns "change" as a plain string for most events but // as an object for others (e.g. project_group_link_updated). A string value is // decoded into Change; any non-string shape is preserved in ChangeObject for // the caller to decode. func (d *AuditEventDetails) UnmarshalJSON(data []byte) error { type alias AuditEventDetails raw := struct { *alias Change json.RawMessage `json:"change"` }{ alias: (*alias)(d), } if err := json.Unmarshal(data, &raw); err != nil { return err } switch { case len(raw.Change) == 0: // No "change" field present. case json.Unmarshal(raw.Change, &d.Change) == nil: // "change" was a JSON string, decoded into Change. default: // "change" was an object or other non-string shape; keep it raw. d.ChangeObject = raw.Change } return nil } // AuditEventChange represents a single entry in the plural "changes" array // that some audit events emit. // // GitLab API docs: https://docs.gitlab.com/api/audit_events/ type AuditEventChange struct { Change string `json:"change"` From string `json:"from"` To string `json:"to"` } // ListAuditEventsOptions represents the available ListProjectAuditEvents(), // ListGroupAuditEvents() or ListInstanceAuditEvents() options. // Loading audit_events_test.go +90 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import ( "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) Loading Loading @@ -409,3 +410,92 @@ func TestAuditEventsService_GetProjectAuditEvent(t *testing.T) { require.Nil(t, ae) require.Equal(t, http.StatusNotFound, resp.StatusCode) } func TestAuditEventsService_ListProjectAuditEvents_ObjectChange(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN a project audit event whose details.change is an object, not a string mux.HandleFunc("/api/v4/projects/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "project_group_link_updated", "details": { "change": { "access_level": { "from": "Reporter", "to": "Planner" } } } } ]`) }) // WHEN listing the project's audit events events, resp, err := client.AuditEvents.ListProjectAuditEvents(6, nil) // THEN it decodes without error, the raw object bytes are preserved // in ChangeObject and Change stays empty. require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.JSONEq(t, `{"access_level":{"from":"Reporter","to":"Planner"}}`, string(events[0].Details.ChangeObject), ) assert.Empty(t, events[0].Details.Change) } func TestAuditEventsService_ListProjectAuditEvents_StringChange(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN the common case where details.change is a plain string mux.HandleFunc("/api/v4/projects/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "project_archived", "details": { "change": "archived" } } ]`) }) // WHEN listing the project's audit events events, resp, err := client.AuditEvents.ListProjectAuditEvents(6, nil) // THEN details.change is decoded into Change and ChangeObject stays empty require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.Equal(t, "archived", events[0].Details.Change) assert.Empty(t, events[0].Details.ChangeObject) } func TestAuditEventsService_ListGroupAuditEvents_ChangesArray(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN a group audit event that emits a plural details.changes array mux.HandleFunc("/api/v4/groups/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "group_share_with_group_link_updated", "details": { "changes": [ { "change": "access_level", "from": "Reporter", "to": "Planner" } ] } } ]`) }) // WHEN listing the group's audit events events, resp, err := client.AuditEvents.ListGroupAuditEvents(6, nil) // THEN the changes array is decoded require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.Len(t, events[0].Details.Changes, 1) change := events[0].Details.Changes[0] assert.Equal(t, "access_level", change.Change) assert.Equal(t, "Reporter", change.From) assert.Equal(t, "Planner", change.To) } Loading
audit_events.go +63 −18 Original line number Diff line number Diff line package gitlab import ( "encoding/json" "net/http" "time" ) Loading Loading @@ -85,6 +86,8 @@ type AuditEventDetails struct { Add string `json:"add"` As string `json:"as"` Change string `json:"change"` ChangeObject json.RawMessage `json:"-"` Changes []AuditEventChange `json:"changes"` From string `json:"from"` To string `json:"to"` Remove string `json:"remove"` Loading @@ -101,6 +104,48 @@ type AuditEventDetails struct { EventName string `json:"event_name"` } // UnmarshalJSON implements the json.Unmarshaler interface. // // The audit events API returns "change" as a plain string for most events but // as an object for others (e.g. project_group_link_updated). A string value is // decoded into Change; any non-string shape is preserved in ChangeObject for // the caller to decode. func (d *AuditEventDetails) UnmarshalJSON(data []byte) error { type alias AuditEventDetails raw := struct { *alias Change json.RawMessage `json:"change"` }{ alias: (*alias)(d), } if err := json.Unmarshal(data, &raw); err != nil { return err } switch { case len(raw.Change) == 0: // No "change" field present. case json.Unmarshal(raw.Change, &d.Change) == nil: // "change" was a JSON string, decoded into Change. default: // "change" was an object or other non-string shape; keep it raw. d.ChangeObject = raw.Change } return nil } // AuditEventChange represents a single entry in the plural "changes" array // that some audit events emit. // // GitLab API docs: https://docs.gitlab.com/api/audit_events/ type AuditEventChange struct { Change string `json:"change"` From string `json:"from"` To string `json:"to"` } // ListAuditEventsOptions represents the available ListProjectAuditEvents(), // ListGroupAuditEvents() or ListInstanceAuditEvents() options. // Loading
audit_events_test.go +90 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import ( "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) Loading Loading @@ -409,3 +410,92 @@ func TestAuditEventsService_GetProjectAuditEvent(t *testing.T) { require.Nil(t, ae) require.Equal(t, http.StatusNotFound, resp.StatusCode) } func TestAuditEventsService_ListProjectAuditEvents_ObjectChange(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN a project audit event whose details.change is an object, not a string mux.HandleFunc("/api/v4/projects/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "project_group_link_updated", "details": { "change": { "access_level": { "from": "Reporter", "to": "Planner" } } } } ]`) }) // WHEN listing the project's audit events events, resp, err := client.AuditEvents.ListProjectAuditEvents(6, nil) // THEN it decodes without error, the raw object bytes are preserved // in ChangeObject and Change stays empty. require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.JSONEq(t, `{"access_level":{"from":"Reporter","to":"Planner"}}`, string(events[0].Details.ChangeObject), ) assert.Empty(t, events[0].Details.Change) } func TestAuditEventsService_ListProjectAuditEvents_StringChange(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN the common case where details.change is a plain string mux.HandleFunc("/api/v4/projects/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "project_archived", "details": { "change": "archived" } } ]`) }) // WHEN listing the project's audit events events, resp, err := client.AuditEvents.ListProjectAuditEvents(6, nil) // THEN details.change is decoded into Change and ChangeObject stays empty require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.Equal(t, "archived", events[0].Details.Change) assert.Empty(t, events[0].Details.ChangeObject) } func TestAuditEventsService_ListGroupAuditEvents_ChangesArray(t *testing.T) { t.Parallel() mux, client := setup(t) // GIVEN a group audit event that emits a plural details.changes array mux.HandleFunc("/api/v4/groups/6/audit_events", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `[ { "id": 1, "event_name": "group_share_with_group_link_updated", "details": { "changes": [ { "change": "access_level", "from": "Reporter", "to": "Planner" } ] } } ]`) }) // WHEN listing the group's audit events events, resp, err := client.AuditEvents.ListGroupAuditEvents(6, nil) // THEN the changes array is decoded require.NoError(t, err) require.NotNil(t, resp) assert.Len(t, events, 1) assert.Len(t, events[0].Details.Changes, 1) change := events[0].Details.Changes[0] assert.Equal(t, "access_level", change.Change) assert.Equal(t, "Reporter", change.From) assert.Equal(t, "Planner", change.To) }