Verified Commit 7f7f38f1 authored by Cesar Hinojosa's avatar Cesar Hinojosa Committed by GitLab
Browse files

fix(audit_events): Add support for object-valued details.change (stored in...

fix(audit_events): Add support for object-valued details.change (stored in `details.ChangeObject`) and the details.changes array

Changelog: Improvements
parent ed3480dd
Loading
Loading
Loading
Loading
+63 −18
Original line number Diff line number Diff line
package gitlab

import (
	"encoding/json"
	"net/http"
	"time"
)
@@ -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"`
@@ -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.
//
+90 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import (
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

@@ -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)
}