Skip to content

Commit 7b38af9

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

2 files changed

Lines changed: 151 additions & 3 deletions

File tree

github/orgs_audit_log.go

Lines changed: 64 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,40 @@ 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+
if len(ss) == 0 {
133+
return nil, nil
134+
}
135+
return Ptr(strings.Join(ss, ", ")), nil
136+
}
137+
138+
// unmarshalIntOrArray decodes a JSON value that is either a plain integer or an array of integers.
139+
// Arrays use the first element.
140+
func unmarshalIntOrArray(raw json.RawMessage) (*int64, error) {
141+
var n int64
142+
if err := json.Unmarshal(raw, &n); err == nil {
143+
return &n, nil
144+
}
145+
var ns []int64
146+
if err := json.Unmarshal(raw, &ns); err != nil {
147+
return nil, err
148+
}
149+
if len(ns) == 0 {
150+
return nil, nil
151+
}
152+
return &ns[0], nil
153+
}
154+
93155
// MarshalJSON implements the json.Marshaler interface.
94156
func (a AuditEntry) MarshalJSON() ([]byte, error) {
95157
type entryAlias AuditEntry

github/orgs_audit_log_test.go

Lines changed: 87 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,90 @@ 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 is scalar",
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 is 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 is 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 is scalar",
398+
payload: `{"action":"test","org_id":42}`,
399+
want: &AuditEntry{Action: Ptr("test"), OrgID: Ptr(int64(42))},
400+
},
401+
{
402+
name: "org_id is 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 is empty array",
408+
payload: `{"action":"test","org":[]}`,
409+
want: &AuditEntry{Action: Ptr("test"), AdditionalFields: map[string]any{"org": []any{}}},
410+
},
411+
{
412+
name: "org_id is empty array",
413+
payload: `{"action":"test","org_id":[]}`,
414+
want: &AuditEntry{Action: Ptr("test"), AdditionalFields: map[string]any{"org_id": []any{}}},
415+
},
416+
{
417+
name: "org and org_id are null",
418+
payload: `{"action":"test","org":null,"org_id":null}`,
419+
want: &AuditEntry{Action: Ptr("test")},
420+
},
421+
}
422+
423+
for _, tc := range tests {
424+
t.Run(tc.name, func(t *testing.T) {
425+
t.Parallel()
426+
testJSONUnmarshalOnly(t, tc.want, tc.payload)
427+
})
428+
}
429+
}
430+
431+
func TestAuditEntry_UnmarshalJSON_Errors(t *testing.T) {
432+
t.Parallel()
433+
tests := []struct {
434+
name string
435+
payload string
436+
}{
437+
{
438+
name: "invalid org type",
439+
payload: `{"action":"test","org":{"key":"value"}}`,
440+
},
441+
{
442+
name: "invalid org_id type",
443+
payload: `{"action":"test","org_id":{"key":"value"}}`,
444+
},
445+
}
446+
447+
for _, tc := range tests {
448+
t.Run(tc.name, func(t *testing.T) {
449+
t.Parallel()
450+
var entry AuditEntry
451+
if err := json.Unmarshal([]byte(tc.payload), &entry); err == nil {
452+
t.Errorf("AuditEntry.UnmarshalJSON(%q) = nil, but want error", tc.payload)
453+
}
454+
})
455+
}
370456
}

0 commit comments

Comments
 (0)