Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions github/orgs_audit_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
)

// GetAuditLogOptions sets up optional parameters to query audit-log endpoint.
Expand Down Expand Up @@ -57,12 +58,43 @@ type AuditEntry struct {
}

// UnmarshalJSON implements the json.Unmarshaler interface.
//
// GitHub's audit-log API occasionally returns "org" as a JSON array of strings
// and "org_id" as a JSON array of integers instead of the documented scalar
// types. This implementation normalises both fields to their scalar forms
// (joining multiple org names with a comma, and using the first org_id) so
// callers always receive a consistent type regardless of the API response shape.
func (a *AuditEntry) UnmarshalJSON(data []byte) error {
// rawEntry shadows Org and OrgID so we can inspect their raw JSON tokens
// before deciding how to decode them.
type entryAlias AuditEntry
var v entryAlias
if err := json.Unmarshal(data, &v); err != nil {
var raw struct {
entryAlias
Org json.RawMessage `json:"org,omitempty"`
OrgID json.RawMessage `json:"org_id,omitempty"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
v := raw.entryAlias

// Normalise "org": accept both "string" and ["string", ...].
if len(raw.Org) > 0 && string(raw.Org) != "null" {
org, err := unmarshalStringOrStringArray(raw.Org)
if err != nil {
return fmt.Errorf("AuditEntry.Org: %w", err)
}
v.Org = org
}

// Normalise "org_id": accept both integer and [integer, ...].
if len(raw.OrgID) > 0 && string(raw.OrgID) != "null" {
orgID, err := unmarshalInt64OrInt64Array(raw.OrgID)
if err != nil {
return fmt.Errorf("AuditEntry.OrgID: %w", err)
}
v.OrgID = orgID
}

rawDefinedFields, err := json.Marshal(v)
if err != nil {
Expand Down Expand Up @@ -90,6 +122,42 @@ func (a *AuditEntry) UnmarshalJSON(data []byte) error {
return nil
}

// unmarshalStringOrStringArray decodes a JSON value that is either a plain
// string or an array of strings. Arrays are joined with ", ".
func unmarshalStringOrStringArray(raw json.RawMessage) (*string, error) {
// Try scalar string first (the common case).
var s string
if err := json.Unmarshal(raw, &s); err == nil {
return &s, nil
}
// Fall back to array of strings.
var arr []string
if err := json.Unmarshal(raw, &arr); err != nil {
return nil, err
}
joined := strings.Join(arr, ", ")
return &joined, nil
}

// unmarshalInt64OrInt64Array decodes a JSON value that is either a plain
// integer or an array of integers. Arrays use the first element.
func unmarshalInt64OrInt64Array(raw json.RawMessage) (*int64, error) {
// Try scalar integer first (the common case).
var n int64
if err := json.Unmarshal(raw, &n); err == nil {
return &n, nil
}
// Fall back to array of integers; use the first element.
var arr []int64
if err := json.Unmarshal(raw, &arr); err != nil {
return nil, err
}
if len(arr) == 0 {
return nil, nil
}
return &arr[0], nil
}

// MarshalJSON implements the json.Marshaler interface.
func (a *AuditEntry) MarshalJSON() ([]byte, error) {
type entryAlias AuditEntry
Expand Down
83 changes: 83 additions & 0 deletions github/orgs_audit_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,86 @@
testJSONMarshalOnly(t, u, want)
// can't unmarshal AdditionalFields back into map[string]any, so skip testJSONUnmarshalOnly
}

// TestAuditEntry_UnmarshalJSON_OrgArray verifies that the GitHub Enterprise
// audit-log API's non-standard behaviour of returning "org" as a JSON array

Check failure on line 391 in github/orgs_audit_log_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`behaviour` is a misspelling of `behavior` (misspell)
// of strings (instead of a single string) is handled gracefully.
// See: https://github.com/google/go-github/issues/3488
func TestAuditEntry_UnmarshalJSON_OrgArray(t *testing.T) {
t.Parallel()
tests := []struct {
name string
payload string
wantOrg string
}{
{
name: "org_as_scalar_string",
payload: `{"action":"test","org":"myorg","org_id":42}`,
wantOrg: "myorg",
},
{
name: "org_as_single_element_array",
payload: `{"action":"test","org":["myorg"],"org_id":[42]}`,
wantOrg: "myorg",
},
{
name: "org_as_multi_element_array",
payload: `{"action":"test","org":["org1","org2","org3"],"org_id":[1,2,3]}`,
wantOrg: "org1, org2, org3",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var entry AuditEntry
if err := entry.UnmarshalJSON([]byte(tc.payload)); err != nil {
t.Fatalf("UnmarshalJSON(%q) returned unexpected error: %v", tc.payload, err)
}
if entry.Org == nil {
t.Fatal("AuditEntry.Org is nil; want non-nil")
}
if *entry.Org != tc.wantOrg {
t.Errorf("AuditEntry.Org = %q; want %q", *entry.Org, tc.wantOrg)
}
})
}
}

// TestAuditEntry_UnmarshalJSON_OrgIDArray verifies that the "org_id" field is
// correctly decoded when returned as a JSON array of integers.
func TestAuditEntry_UnmarshalJSON_OrgIDArray(t *testing.T) {
t.Parallel()
tests := []struct {
name string
payload string
wantOrgID int64
}{
{
name: "org_id_as_scalar",
payload: `{"action":"test","org_id":42}`,
wantOrgID: 42,
},
{
name: "org_id_as_array",
payload: `{"action":"test","org_id":[42,43,44]}`,
wantOrgID: 42, // first element
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var entry AuditEntry
if err := entry.UnmarshalJSON([]byte(tc.payload)); err != nil {
t.Fatalf("UnmarshalJSON(%q) returned unexpected error: %v", tc.payload, err)
}
if entry.OrgID == nil {
t.Fatal("AuditEntry.OrgID is nil; want non-nil")
}
if *entry.OrgID != tc.wantOrgID {
t.Errorf("AuditEntry.OrgID = %d; want %d", *entry.OrgID, tc.wantOrgID)

Check failure on line 467 in github/orgs_audit_log_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

use %v instead of %d (fmtpercentv)
}
})
}
}
Loading