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
12 changes: 6 additions & 6 deletions platform-api/src/internal/service/websub_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ func mapWebSubAPIModelToAPI(m *model.WebSubAPI, apiUtil *utils.APIUtil) *api.Web
Transport: transport,
Context: m.Configuration.Context,
Upstream: mapUpstreamModelToAPI(&m.Configuration.Upstream),
Channels: *mapWebSubChannelsModelToAPI(m.Configuration.Channels),
Channels: mapWebSubChannelsModelToAPI(m.Configuration.Channels),
AllChannels: mapWebSubAllChannelPoliciesModelToAPI(m.Configuration.AllChannels),
SubscriptionPlans: subscriptionPlans,
CreatedAt: utils.TimePtr(m.CreatedAt),
Expand Down Expand Up @@ -465,10 +465,10 @@ func mapWebSubAllChannelPoliciesAPIToModel(in *api.WebSubAllChannelPolicies) *mo
}

// mapWebSubChannelsModelToAPI converts the model channel map to the API channel map.
func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string]api.WebSubChannel {
if len(in) == 0 {
return nil
}
// It always returns a non-nil map so callers that embed the result by value
// (api.WebSubAPI.Channels is a value-type map) do not panic when the input is
// empty or nil.
func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) map[string]api.WebSubChannel {
out := make(map[string]api.WebSubChannel, len(in))
for name, ch := range in {
out[name] = api.WebSubChannel{
Expand All @@ -478,7 +478,7 @@ func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string]
OnMessageDelivery: mapEventPoliciesModelToAPI(ch.OnMessageDelivery),
}
}
return &out
return out
}

// mapEventPoliciesModelToAPI converts model event policies to API.
Expand Down
58 changes: 58 additions & 0 deletions platform-api/src/internal/service/websub_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,61 @@ func TestWebSubAPI_NilPoliciesHandled(t *testing.T) {
t.Errorf("expected nil for nil input, got %v", got)
}
}

// TestWebSubAPI_MapModelToAPI_EmptyChannelsDoesNotPanic guards the nil-pointer
// dereference reported in #1995. Previously mapWebSubChannelsModelToAPI
// returned nil for empty/nil channel maps and the caller dereferenced that nil
// pointer when assigning to api.WebSubAPI.Channels (a value-type map). The Get,
// List, and Update return paths panicked for any WebSub API stored with an
// empty channels map.
func TestWebSubAPI_MapModelToAPI_EmptyChannelsDoesNotPanic(t *testing.T) {
tests := []struct {
name string
in map[string]model.WebSubChannel
}{
{name: "nil channel map", in: nil},
{name: "empty channel map", in: map[string]model.WebSubChannel{}},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("mapWebSubAPIModelToAPI panicked: %v", r)
}
}()

m := &model.WebSubAPI{
Handle: "test",
Name: "test",
Version: "v1",
Configuration: model.WebSubAPIConfiguration{
Channels: tc.in,
},
}

got := mapWebSubAPIModelToAPI(m, &utils.APIUtil{})
if got == nil {
t.Fatal("expected non-nil WebSubAPI result")
}
if got.Channels == nil {
t.Errorf("expected non-nil Channels map, got nil")
}
if len(got.Channels) != 0 {
t.Errorf("expected empty Channels map, got %d entries", len(got.Channels))
}
})
}
}

// TestWebSubAPI_MapChannelsModelToAPI_NeverReturnsNil ensures the helper itself
// always returns a usable (non-nil) map so callers do not need a nil guard
// before assigning the result to a value-type map field.
func TestWebSubAPI_MapChannelsModelToAPI_NeverReturnsNil(t *testing.T) {
if got := mapWebSubChannelsModelToAPI(nil); got == nil {
t.Errorf("expected non-nil map for nil input, got nil")
}
if got := mapWebSubChannelsModelToAPI(map[string]model.WebSubChannel{}); got == nil {
t.Errorf("expected non-nil map for empty input, got nil")
}
}