Skip to content

Commit ad884ac

Browse files
committed
fix: AuditEntry fields org and org_id can be array
1 parent 21154c5 commit ad884ac

2 files changed

Lines changed: 147 additions & 3 deletions

File tree

github/orgs_audit_log.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"encoding/json"
1111
"fmt"
12+
"strings"
1213
)
1314

1415
// GetAuditLogOptions sets up optional parameters to query audit-log endpoint.
@@ -57,12 +58,39 @@ type AuditEntry struct {
5758
}
5859

5960
// UnmarshalJSON implements the json.Unmarshaler interface.
61+
//
62+
// GitHub's audit-log API occasionally returns "org" as a JSON array of strings
63+
// and "org_id" as a JSON array of integers instead of the documented scalar types.
64+
// This implementation normalises both fields to their scalar forms
65+
// (joining multiple org names with a comma, and using the first org_id) so
66+
// callers always receive a consistent type regardless of the API response shape.
6067
func (a *AuditEntry) UnmarshalJSON(data []byte) error {
6168
type entryAlias AuditEntry
62-
var v entryAlias
63-
if err := json.Unmarshal(data, &v); err != nil {
69+
var raw struct {
70+
entryAlias
71+
Org json.RawMessage `json:"org,omitempty"`
72+
OrgID json.RawMessage `json:"org_id,omitempty"`
73+
}
74+
if err := json.Unmarshal(data, &raw); err != nil {
6475
return err
6576
}
77+
v := raw.entryAlias
78+
79+
if len(raw.Org) > 0 && string(raw.Org) != "null" {
80+
var err error
81+
v.Org, err = unmarshalStringOrArray(raw.Org)
82+
if err != nil {
83+
return fmt.Errorf("unmarshal org: %w", err)
84+
}
85+
}
86+
87+
if len(raw.OrgID) > 0 && string(raw.OrgID) != "null" {
88+
var err error
89+
v.OrgID, err = unmarshalIntOrArray(raw.OrgID)
90+
if err != nil {
91+
return fmt.Errorf("unmarshal org_id: %w", err)
92+
}
93+
}
6694

6795
rawDefinedFields, err := json.Marshal(v)
6896
if err != nil {
@@ -90,6 +118,37 @@ func (a *AuditEntry) UnmarshalJSON(data []byte) error {
90118
return nil
91119
}
92120

121+
// unmarshalStringOrArray decodes a JSON value that is either a plain string or an array of strings.
122+
// Arrays are joined with ", ".
123+
func unmarshalStringOrArray(raw json.RawMessage) (*string, error) {
124+
var s string
125+
if err := json.Unmarshal(raw, &s); err == nil {
126+
return &s, nil
127+
}
128+
var ss []string
129+
if err := json.Unmarshal(raw, &ss); err != nil {
130+
return nil, err
131+
}
132+
return Ptr(strings.Join(ss, ", ")), nil
133+
}
134+
135+
// unmarshalIntOrArray decodes a JSON value that is either a plain integer or an array of integers.
136+
// Arrays use the first element.
137+
func unmarshalIntOrArray(raw json.RawMessage) (*int64, error) {
138+
var n int64
139+
if err := json.Unmarshal(raw, &n); err == nil {
140+
return &n, nil
141+
}
142+
var ns []int64
143+
if err := json.Unmarshal(raw, &ns); err != nil {
144+
return nil, err
145+
}
146+
if len(ns) == 0 {
147+
return nil, nil
148+
}
149+
return &ns[0], nil
150+
}
151+
93152
// MarshalJSON implements the json.Marshaler interface.
94153
func (a AuditEntry) MarshalJSON() ([]byte, error) {
95154
type entryAlias AuditEntry

github/orgs_audit_log_test.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package github
77

88
import (
9+
"encoding/json"
910
"fmt"
1011
"net/http"
1112
"testing"
@@ -366,5 +367,89 @@ func TestAuditEntry_MarshalJSON(t *testing.T) {
366367

367368
testJSONMarshalOnly(t, u, want)
368369
testJSONMarshalOnly(t, &u, want)
369-
// can't unmarshal AdditionalFields back into map[string]any, so skip testJSONUnmarshalOnly
370+
}
371+
372+
func TestAuditEntry_UnmarshalJSON(t *testing.T) {
373+
t.Parallel()
374+
testJSONUnmarshalOnly(t, &AuditEntry{}, "{}")
375+
376+
tests := []struct {
377+
name string
378+
payload string
379+
want *AuditEntry
380+
}{
381+
{
382+
name: "org_as_scalar_string",
383+
payload: `{"action":"test","org":"myorg","org_id":42}`,
384+
want: &AuditEntry{Action: Ptr("test"), Org: Ptr("myorg"), OrgID: Ptr(int64(42))},
385+
},
386+
{
387+
name: "org_as_single_element_array",
388+
payload: `{"action":"test","org":["myorg"],"org_id":[42]}`,
389+
want: &AuditEntry{Action: Ptr("test"), Org: Ptr("myorg"), OrgID: Ptr(int64(42))},
390+
},
391+
{
392+
name: "org_as_multi_element_array",
393+
payload: `{"action":"test","org":["org1","org2","org3"],"org_id":[1,2,3]}`,
394+
want: &AuditEntry{Action: Ptr("test"), Org: Ptr("org1, org2, org3"), OrgID: Ptr(int64(1))},
395+
},
396+
{
397+
name: "org_id_as_scalar",
398+
payload: `{"action":"test","org_id":42}`,
399+
want: &AuditEntry{Action: Ptr("test"), OrgID: Ptr(int64(42))},
400+
},
401+
{
402+
name: "org_id_as_array",
403+
payload: `{"action":"test","org_id":[42,43,44]}`,
404+
want: &AuditEntry{Action: Ptr("test"), OrgID: Ptr(int64(42))}, // first element
405+
},
406+
{
407+
name: "org_id_as_empty_array",
408+
payload: `{"action":"test","org_id":[]}`,
409+
want: &AuditEntry{Action: Ptr("test"), AdditionalFields: map[string]any{"org_id": []any{}}},
410+
},
411+
{
412+
name: "null_org_fields",
413+
payload: `{"action":"test","org":null,"org_id":null}`,
414+
want: &AuditEntry{Action: Ptr("test")},
415+
},
416+
}
417+
418+
for _, tc := range tests {
419+
t.Run(tc.name, func(t *testing.T) {
420+
t.Parallel()
421+
testJSONUnmarshalOnly(t, tc.want, tc.payload)
422+
})
423+
}
424+
}
425+
426+
func TestAuditEntry_UnmarshalJSON_Errors(t *testing.T) {
427+
t.Parallel()
428+
tests := []struct {
429+
name string
430+
payload string
431+
}{
432+
{
433+
name: "invalid_org_type",
434+
payload: `{"action":"test","org":{"key":"value"}}`,
435+
},
436+
{
437+
name: "invalid_org_id_type",
438+
payload: `{"action":"test","org_id":{"key":"value"}}`,
439+
},
440+
{
441+
name: "invalid_org_id_type",
442+
payload: `{"action":"test","org_id":{"key":"value"}}`,
443+
},
444+
}
445+
446+
for _, tc := range tests {
447+
t.Run(tc.name, func(t *testing.T) {
448+
t.Parallel()
449+
var entry AuditEntry
450+
if err := json.Unmarshal([]byte(tc.payload), &entry); err == nil {
451+
t.Errorf("UnmarshalJSON(%q) = nil; want error", tc.payload)
452+
}
453+
})
454+
}
370455
}

0 commit comments

Comments
 (0)