diff --git a/internal/webhooksync/service.go b/internal/webhooksync/service.go index 596dcaf7..db2b7cb2 100644 --- a/internal/webhooksync/service.go +++ b/internal/webhooksync/service.go @@ -255,18 +255,16 @@ func (s *Service) ProcessWebhook(ctx context.Context, secret string, r *http.Req slog.Warn("webhook sync: failed to upsert seen external user", "connection_id", conn.ID, "external_user_id", event.UserID, "error", err) } - profileID := conn.DefaultProfileID if mapping, err := s.repo.GetMappingByUser(ctx, conn.ID, event.UserID); err != nil { return s.failWebhook(ctx, conn.ID, result, err, "Failed to resolve profile mapping") - } else if mapping != nil && mapping.SiloProfileID != nil && *mapping.SiloProfileID != "" { - profileID = *mapping.SiloProfileID - } - result.ProfileID = profileID - if profileID == "" { + } else if profileID, ok := resolveWebhookProfileID(mapping); ok { + result.ProfileID = profileID + } else { result.Outcome = OutcomeSkipped - result.Summary = "Skipped because no default or user-specific profile is configured" + result.Summary = "Skipped because external user is not linked to a Silo profile" return result, nil } + profileID := result.ProfileID record := event.Record.toHistoryImportRecord() match, _, err := s.matcher.Match(ctx, record) @@ -432,6 +430,13 @@ func shouldSkipEvent(state *ItemState, event *CanonicalEvent) bool { return true } +func resolveWebhookProfileID(mapping *ProfileMapping) (string, bool) { + if mapping == nil || mapping.SiloProfileID == nil || strings.TrimSpace(*mapping.SiloProfileID) == "" { + return "", false + } + return *mapping.SiloProfileID, true +} + func buildWebhookURL(baseURL, secret string) string { if baseURL == "" { return webhookSyncPathPrefix + secret diff --git a/internal/webhooksync/service_test.go b/internal/webhooksync/service_test.go index e2e0ecf0..ff0ca492 100644 --- a/internal/webhooksync/service_test.go +++ b/internal/webhooksync/service_test.go @@ -42,6 +42,52 @@ func TestBuildWebhookURL(t *testing.T) { } } +func TestResolveWebhookProfileRequiresExplicitMapping(t *testing.T) { + t.Parallel() + + linkedProfileID := "linked-profile" + + cases := []struct { + name string + mapping *ProfileMapping + want string + wantOK bool + }{ + { + name: "missing mapping is skipped", + wantOK: false, + }, + { + name: "unmapped external user is skipped", + mapping: &ProfileMapping{}, + wantOK: false, + }, + { + name: "empty profile mapping is skipped", + mapping: &ProfileMapping{SiloProfileID: ptrString("")}, + wantOK: false, + }, + { + name: "explicit profile mapping is used", + mapping: &ProfileMapping{SiloProfileID: &linkedProfileID}, + want: linkedProfileID, + wantOK: true, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got, ok := resolveWebhookProfileID(tc.mapping) + if ok != tc.wantOK || got != tc.want { + t.Fatalf("resolveWebhookProfileID() = (%q, %v), want (%q, %v)", got, ok, tc.want, tc.wantOK) + } + }) + } +} + func TestFilterDiscoveredAccounts(t *testing.T) { t.Parallel() @@ -82,3 +128,7 @@ func TestFilterDiscoveredAccountsFallsBackWhenFlagsMissing(t *testing.T) { t.Fatalf("unexpected fallback accounts: %#v", filtered) } } + +func ptrString(value string) *string { + return &value +} diff --git a/web/src/pages/settings/WebhookSyncSettings.tsx b/web/src/pages/settings/WebhookSyncSettings.tsx index d85f2dab..b34fda51 100644 --- a/web/src/pages/settings/WebhookSyncSettings.tsx +++ b/web/src/pages/settings/WebhookSyncSettings.tsx @@ -678,7 +678,7 @@ export default function WebhookSyncSettings() {

- Activity from unmapped users goes to this profile. + The signed-in external user is linked to this profile when the connection is created.