Skip to content
Draft
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
53 changes: 34 additions & 19 deletions pkg/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type listenCmd struct {
forwardConnectURL string
eventsFrom string
events []string
allSnapshot bool
allThin bool
latestAPIVersion bool
livemode bool
useConfiguredWebhooks bool
Expand Down Expand Up @@ -73,12 +75,12 @@ local machine by connecting directly to Stripe's API. You can test the latest
API version, filter events, or even load your saved webhook endpoints from your
Stripe account.`,
Example: `stripe listen
stripe listen --events charge.captured,charge.updated \
stripe listen --all-snapshot --forward-to localhost:3000/events
stripe listen --all-thin --forward-to localhost:3000/events
stripe listen --events charge.captured,v1.billing.meter.no_meter_found \
--forward-to localhost:3000/events
stripe listen --events v1.billing.meter.no_meter_found \
--forward-to localhost:3000/events
stripe listen --events v2.core.account.created \
--events-from @accounts --forward-to localhost:3000/events`,
stripe listen --all-thin --events-from @accounts \
--forward-to localhost:3000/events`,
Annotations: map[string]string{
AIAgentHelpAnnotationKey: " Use `--forward-to` to specify where events are sent, e.g. localhost:4242/webhook.\n" +
" Use `--events` to filter to specific event types, e.g. `--events checkout.session.completed`.\n" +
Expand All @@ -89,7 +91,9 @@ Stripe account.`,
}

lc.cmd.Flags().StringSliceVar(&lc.forwardConnectHeaders, "connect-headers", []string{}, "A comma-separated list of custom headers to forward for Connect. Ex: \"Key1:Value1, Key2:Value2\"")
lc.cmd.Flags().StringSliceVarP(&lc.events, "events", "e", []string{"*"}, "A comma-separated list of specific events to listen for. Supports both snapshot events (e.g. charge.captured) and thin events (e.g. v1.billing.meter.no_meter_found)")
lc.cmd.Flags().StringSliceVarP(&lc.events, "events", "e", []string{}, "A comma-separated list of specific events to listen for. Supports both snapshot events (e.g. charge.captured) and thin events (e.g. v1.billing.meter.no_meter_found)")
lc.cmd.Flags().BoolVar(&lc.allSnapshot, "all-snapshot", false, "Subscribe to all snapshot events")
lc.cmd.Flags().BoolVar(&lc.allThin, "all-thin", false, "Subscribe to all thin events")
lc.cmd.Flags().StringVar(&lc.eventsFrom, "events-from", "all", "Event source filter: '@self' (your account only), '@accounts' (connected accounts only), or 'all' (default)")
lc.cmd.Flags().StringVarP(&lc.forwardURL, "forward-to", "f", "", "The URL to forward events to")
lc.cmd.Flags().StringSliceVarP(&lc.forwardHeaders, "headers", "H", []string{}, "A comma-separated list of custom headers to forward. Ex: \"Key1:Value1, Key2:Value2\"")
Expand Down Expand Up @@ -386,22 +390,23 @@ func (lc *listenCmd) createVisitor(logger *log.Logger, format string, printJSON
}

func (lc *listenCmd) getFeatures() []string {
needsSnapshot := false
needsThin := false
needsSnapshot := lc.allSnapshot
needsThin := lc.allThin

for _, e := range lc.events {
if e == "*" {
needsSnapshot = true
needsThin = true
break
}
if isThinEvent(e) {
needsThin = true
} else {
needsSnapshot = true
}
}

// bare "stripe listen" with no event flags opens both channels
if !needsSnapshot && !needsThin {
needsSnapshot = true
needsThin = true
}

features := []string{}
if needsSnapshot {
features = append(features, webhooksWebSocketFeature)
Expand All @@ -415,17 +420,27 @@ func (lc *listenCmd) getFeatures() []string {
// splitEventsByType separates the unified --events list into snapshot and thin
// event lists for the proxy, which still uses two separate channels internally.
func (lc *listenCmd) splitEventsByType() (snapshotEvents []string, thinEvents []string) {
if lc.allSnapshot {
snapshotEvents = append(snapshotEvents, "*")
}
if lc.allThin {
thinEvents = append(thinEvents, "*")
}

for _, e := range lc.events {
switch {
case e == "*":
snapshotEvents = append(snapshotEvents, "*")
thinEvents = append(thinEvents, "*")
case isThinEvent(e):
if isThinEvent(e) {
thinEvents = append(thinEvents, e)
default:
} else {
snapshotEvents = append(snapshotEvents, e)
}
}

// bare "stripe listen" with no event flags subscribes to everything
if len(snapshotEvents) == 0 && len(thinEvents) == 0 {
snapshotEvents = []string{"*"}
thinEvents = []string{"*"}
}

return
}

Expand Down
89 changes: 73 additions & 16 deletions pkg/cmd/listen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,51 @@ func TestSplitEventsByType(t *testing.T) {
tests := []struct {
name string
events []string
allSnapshot bool
allThin bool
wantSnapshot []string
wantThin []string
}{
{
name: "wildcard splits to both",
events: []string{"*"},
name: "bare listen with no flags subscribes to everything",
events: []string{},
wantSnapshot: []string{"*"},
wantThin: []string{"*"},
},
{
name: "all-snapshot adds wildcard for snapshot",
allSnapshot: true,
events: []string{},
wantSnapshot: []string{"*"},
wantThin: nil,
},
{
name: "all-thin adds wildcard for thin",
allThin: true,
events: []string{},
wantSnapshot: nil,
wantThin: []string{"*"},
},
{
name: "both all-snapshot and all-thin",
allSnapshot: true,
allThin: true,
events: []string{},
wantSnapshot: []string{"*"},
wantThin: []string{"*"},
},
{
name: "all-snapshot with specific thin events",
allSnapshot: true,
events: []string{"v1.billing.meter.no_meter_found"},
wantSnapshot: []string{"*"},
wantThin: []string{"v1.billing.meter.no_meter_found"},
},
{
name: "all-thin with specific snapshot events",
allThin: true,
events: []string{"charge.captured"},
wantSnapshot: []string{"charge.captured"},
wantThin: []string{"*"},
},
{
Expand All @@ -62,17 +100,15 @@ func TestSplitEventsByType(t *testing.T) {
wantSnapshot: []string{"charge.captured"},
wantThin: []string{"v1.billing.meter.no_meter_found"},
},
{
name: "empty events returns nil",
events: []string{},
wantSnapshot: nil,
wantThin: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lc := &listenCmd{events: tt.events}
lc := &listenCmd{
events: tt.events,
allSnapshot: tt.allSnapshot,
allThin: tt.allThin,
}
snapshot, thin := lc.splitEventsByType()
assert.Equal(t, tt.wantSnapshot, snapshot)
assert.Equal(t, tt.wantThin, thin)
Expand All @@ -82,14 +118,31 @@ func TestSplitEventsByType(t *testing.T) {

func TestGetFeatures(t *testing.T) {
tests := []struct {
name string
events []string
want []string
name string
events []string
allSnapshot bool
allThin bool
want []string
}{
{
name: "wildcard opens both channels",
events: []string{"*"},
want: []string{webhooksWebSocketFeature, destinationsWebSocketFeature},
name: "bare listen opens both channels",
want: []string{webhooksWebSocketFeature, destinationsWebSocketFeature},
},
{
name: "all-snapshot opens webhooks only",
allSnapshot: true,
want: []string{webhooksWebSocketFeature},
},
{
name: "all-thin opens v2_events only",
allThin: true,
want: []string{destinationsWebSocketFeature},
},
{
name: "both all flags open both channels",
allSnapshot: true,
allThin: true,
want: []string{webhooksWebSocketFeature, destinationsWebSocketFeature},
},
{
name: "snapshot events only opens webhooks",
Expand All @@ -110,7 +163,11 @@ func TestGetFeatures(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lc := &listenCmd{events: tt.events}
lc := &listenCmd{
events: tt.events,
allSnapshot: tt.allSnapshot,
allThin: tt.allThin,
}
got := lc.getFeatures()
assert.Equal(t, tt.want, got)
})
Expand Down
Loading