Skip to content

fix(core): Resolve customer user via relation instead of email lookup#4468

Open
grolmus wants to merge 3 commits intovendurehq:masterfrom
grolmus:fix/oss-321-verified-customer-wrong
Open

fix(core): Resolve customer user via relation instead of email lookup#4468
grolmus wants to merge 3 commits intovendurehq:masterfrom
grolmus:fix/oss-321-verified-customer-wrong

Conversation

@grolmus
Copy link
Copy Markdown
Collaborator

@grolmus grolmus commented Mar 5, 2026

Summary

  • Fix the user resolver on CustomerEntityResolver which incorrectly fell back to getUserByEmailAddress() — this caused guest customers to appear "verified" when a different registered customer shared the same email address
  • Replace the email-based lookup with customerService.findOne() which loads the actual user relation via the foreign key
  • Remove unused UserService dependency from the resolver

Fixes #4026

Test plan

  • Added e2e regression test: guest customer with same email as registered customer should not show as verified
  • All 47 customer e2e tests pass
  • All 48 shop-auth e2e tests pass

The customer entity resolver was falling back to looking up the User by email
address when the user relation was not eagerly loaded. This caused guest
customers to incorrectly appear as "verified" when a different registered
customer shared the same email address.

Now loads the customer's actual user relation via CustomerService.findOne()
instead.

Fixes vendurehq#4026

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment Mar 26, 2026 0:35am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43f5b6e8-c5d0-4fb6-bcf5-cac954471d04

📥 Commits

Reviewing files that changed from the base of the PR and between 4f86276 and 6db10b7.

📒 Files selected for processing (2)
  • packages/core/e2e/guest-checkout-strategy.e2e-spec.ts
  • packages/core/src/api/resolvers/entity/customer-entity.resolver.ts

📝 Walkthrough

Walkthrough

The changes fix an issue where a guest customer could incorrectly appear verified in the Admin Dashboard due to email-based user lookup returning an unrelated user account. The user field resolver in CustomerEntityResolver was refactored to remove UserService dependency and instead reload the customer entity to retrieve the directly-associated user relationship, ensuring only customers with their own user accounts are marked as verified. An E2E test was added to verify that guest customers created during email conflicts correctly return null for the user field.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the primary change: fixing customer user resolution to use relation-based lookup instead of email lookup.
Description check ✅ Passed The description includes a clear summary, test plan with passing tests, and links to the related issue, mostly following the template.
Linked Issues check ✅ Passed The PR directly addresses issue #4026 by replacing email-based user lookup with relation-based lookup and includes regression test to prevent recurrence.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the customer user resolver: the resolver implementation fix, removal of UserService dependency, and related e2e test are in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/e2e/customer.e2e-spec.ts`:
- Around line 706-718: The test is not verifying that the GraphQL mutation
succeeded before comparing customers; update the block that calls
shopClient.query for SET_CUSTOMER (and the returned SetCustomerForOrderMutation)
to first assert the mutation succeeded (e.g.,
expect(data.setCustomerForOrder).toBeDefined() or
expect(result.errors).toBeUndefined()), then read the customer id from the
strongly-typed response instead of using the cast (remove the "as any" and use
the SetCustomerForOrderMutation shape to access
data.setCustomerForOrder.customer.id), and only then compare guestCustomerId to
firstCustomer.id.

In `@packages/core/src/api/resolvers/entity/customer-entity.resolver.ts`:
- Around line 52-59: The fallback path in user(`@Ctx`() ctx: RequestContext,
`@Parent`() customer: Customer) calls this.customerService.findOne(ctx,
customer.id) without requesting relations, so loaded?.user stays undefined;
update the call to explicitly request the user relation (pass ['user'] as the
third parameter to this.customerService.findOne) so the returned Customer
includes the user relation and the method can return loaded.user correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1105030-3ebf-4d43-a49b-68d20a6d5475

📥 Commits

Reviewing files that changed from the base of the PR and between a635eb2 and 4f86276.

📒 Files selected for processing (2)
  • packages/core/e2e/customer.e2e-spec.ts
  • packages/core/src/api/resolvers/entity/customer-entity.resolver.ts

Comment on lines +706 to +718
const { setCustomerForOrder } = await shopClient.query<
SetCustomerForOrderMutation,
SetCustomerForOrderMutationVariables
>(SET_CUSTOMER, {
input: {
firstName: 'Guest',
lastName: 'Shared Email',
emailAddress: firstCustomer.emailAddress,
},
});

const guestCustomerId = (setCustomerForOrder as any).customer?.id;
if (guestCustomerId && guestCustomerId !== firstCustomer.id) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard setCustomerForOrder success before asserting customer linkage.

This test can produce a false positive: if setCustomerForOrder returns an error result, guestCustomerId is undefined and the else branch still passes against firstCustomer. Assert success first and remove the as any cast.

Proposed fix
+            const orderErrorGuard: ErrorResultGuard<ActiveOrderCustomerFragment> = createErrorResultGuard(
+                input => !!input.lines,
+            );
             const { setCustomerForOrder } = await shopClient.query<
                 SetCustomerForOrderMutation,
                 SetCustomerForOrderMutationVariables
             >(SET_CUSTOMER, {
                 input: {
                     firstName: 'Guest',
                     lastName: 'Shared Email',
                     emailAddress: firstCustomer.emailAddress,
                 },
             });

-            const guestCustomerId = (setCustomerForOrder as any).customer?.id;
+            orderErrorGuard.assertSuccess(setCustomerForOrder);
+            const guestCustomerId = setCustomerForOrder.customer?.id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { setCustomerForOrder } = await shopClient.query<
SetCustomerForOrderMutation,
SetCustomerForOrderMutationVariables
>(SET_CUSTOMER, {
input: {
firstName: 'Guest',
lastName: 'Shared Email',
emailAddress: firstCustomer.emailAddress,
},
});
const guestCustomerId = (setCustomerForOrder as any).customer?.id;
if (guestCustomerId && guestCustomerId !== firstCustomer.id) {
const orderErrorGuard: ErrorResultGuard<ActiveOrderCustomerFragment> = createErrorResultGuard(
input => !!input.lines,
);
const { setCustomerForOrder } = await shopClient.query<
SetCustomerForOrderMutation,
SetCustomerForOrderMutationVariables
>(SET_CUSTOMER, {
input: {
firstName: 'Guest',
lastName: 'Shared Email',
emailAddress: firstCustomer.emailAddress,
},
});
orderErrorGuard.assertSuccess(setCustomerForOrder);
const guestCustomerId = setCustomerForOrder.customer?.id;
if (guestCustomerId && guestCustomerId !== firstCustomer.id) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/e2e/customer.e2e-spec.ts` around lines 706 - 718, The test is
not verifying that the GraphQL mutation succeeded before comparing customers;
update the block that calls shopClient.query for SET_CUSTOMER (and the returned
SetCustomerForOrderMutation) to first assert the mutation succeeded (e.g.,
expect(data.setCustomerForOrder).toBeDefined() or
expect(result.errors).toBeUndefined()), then read the customer id from the
strongly-typed response instead of using the cast (remove the "as any" and use
the SetCustomerForOrderMutation shape to access
data.setCustomerForOrder.customer.id), and only then compare guestCustomerId to
firstCustomer.id.

Copy link
Copy Markdown
Collaborator

@gabriellbui gabriellbui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

The fix is conceptually correct — replacing email-based user lookup with relation-based lookup properly isolates the guest customer's user field from a registered customer sharing the same email. However there's a blocking issue in the implementation.

🔴 MUST CHANGE

packages/core/src/api/resolvers/entity/customer-entity.resolver.ts:52 — Missing ['user'] relation

// Current
const loaded = await this.customerService.findOne(ctx, customer.id);
return loaded?.user ?? null;

// Fix
const loaded = await this.customerService.findOne(ctx, customer.id, ['user']);
return loaded?.user ?? null;

findOne signature is findOne(ctx, id, relations: RelationPaths<Customer> = []). Without explicitly passing ['user'], the relation is never loaded and loaded?.user is always undefined. The fallback path would return null for all customers whose user isn't eagerly pre-loaded on the parent entity — including registered customers. This could be a silent regression worse than the original bug. (CodeRabbit flagged this same issue.)


🟡 SHOULD CHANGE

packages/core/e2e/customer.e2e-spec.ts:718 — Test conditional logic allows the bug scenario to go untested

const guestCustomerId = (setCustomerForOrder as any).customer?.id;
if (guestCustomerId && guestCustomerId !== firstCustomer.id) {
    // actual assertion
} else {
    // fallback that doesn't test the bug
}

If guestCustomerId is undefined (e.g. mutation returned an error type) or equals firstCustomer.id, the test silently passes without exercising the regression scenario. The as any cast should use the typed SetCustomerForOrderMutation shape, and the test should be structured to guarantee the guest customer is created and assert unconditionally.


🔵 CONSIDER

packages/core/e2e/customer.e2e-spec.ts:706 — Validate mutation result before use

Add a guard like expect(setCustomerForOrder.__typename).toBe('Order') before accessing .customer so an unexpected error type doesn't silently short-circuit the test through the else branch.


Once the ['user'] relation arg is added, this is a clean and well-scoped fix for a real bug. Good find on the root cause.

Copy link
Copy Markdown
Member

@michaelbromley michaelbromley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gabirel and CodeRabbit are spot on. This PR is dangerous because it introduces a new bug and has a test that does not test what it should be.

- Add missing ['user'] relation to customerService.findOne() call so the
  user relation is actually loaded in the fallback path
- Move regression test to guest-checkout-strategy.e2e-spec.ts where the
  TestGuestCheckoutStrategy can create the conflict scenario (default
  strategy rejects guest checkout with registered emails)
- Rewrite test to use assertSuccess and unconditional assertions
- Rename misleading describe('Order taxes') to describe('GuestCheckoutStrategy')

Fixes vendurehq#4026
@grolmus
Copy link
Copy Markdown
Collaborator Author

grolmus commented Mar 23, 2026

Addressed review feedback

Thanks @gabriellbui and @michaelbromley for the thorough review. Both issues are now fixed:

1. Missing ['user'] relation (blocking)

customerService.findOne(ctx, customer.id) was called without requesting the user relation, so loaded?.user was always undefined. Fixed by passing ['user'] as the third argument:

const loaded = await this.customerService.findOne(ctx, customer.id, ['user']);
return loaded?.user ?? null;

2. Regression test rewrite

The original test was placed in customer.e2e-spec.ts, but the default GuestCheckoutStrategy rejects guest checkout when a registered customer shares the same email (EMAIL_ADDRESS_CONFLICT_ERROR). This meant the test could never actually exercise the bug scenario.

Moved the test to guest-checkout-strategy.e2e-spec.ts where TestGuestCheckoutStrategy with createNewCustomerOnEmailAddressConflict = true can create the exact conflict scenario: a separate guest Customer entity sharing an email with a registered Customer.

The rewritten test:

  • Uses orderResultGuard.assertSuccess() instead of as any casts
  • Asserts unconditionally (no if/else branches that silently skip)
  • Queries activeOrder.customer.user via the shop API to verify the resolver returns null for the guest customer

Also renamed the misleading describe('Order taxes') block to describe('GuestCheckoutStrategy').

Test results

  • 46/46 customer e2e tests pass
  • 7/7 guest-checkout-strategy e2e tests pass
  • 48/48 shop-auth e2e tests pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verified customer wrong

3 participants