Changeset c7a211d in Klonkt for test/paid-patron.test.js


Ignore:
Timestamp:
07/21/2026 02:57:49 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ec288dc
Parents:
9019a3e
git-author:
Robin <roboburr@…> (07/21/2026 02:57:46 AM)
git-committer:
Robin <roboburr@…> (07/21/2026 02:57:49 AM)
Message:

Fix: real supporters were rejected on a wrong campaign_id + add diagnostics

Robin is an active supporter but /paid/callback said "Nog geen supporter",
so he never reached the passkey step (that page only shows after a verified
patron). Root cause: pickCampaignMembership matched STRICTLY on the owner's
configured campaign_id, and a wrong/typo'd id in the admin locked out real
patrons.

Memberships returned via a creator's OWN OAuth client are already scoped to
that creator's campaign(s), so:

  • prefer an exact campaign_id match (unchanged for the common case),
  • but fall back to the sole membership when the configured id doesn't match,
  • refuse only when it's genuinely ambiguous (multiple memberships, none matching) so we never silently grant the wrong one.

Also log a one-line diagnostic in verifyPatron when a patron is NOT accepted:
the campaign_id configured vs the memberships Patreon actually returned
(campaign:status:cents). Stores nothing; it's a server log so the owner can
see whether their campaign_id is wrong or the pledge isn't active.

Changed files:
src/services/PaidPatreonService.js

  • pickCampaignMembership: exact match, else sole-membership fallback, else null; verifyPatron logs why a patron was rejected

test/paid-patron.test.js

  • exact match, sole-membership fallback, ambiguous→null, empty→null

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/paid-patron.test.js

    r9019a3e rc7a211d  
    2727});
    2828
    29 test('pickCampaignMembership finds the right campaign, ignores others', async () => {
     29test('pickCampaignMembership: exact campaign match wins', async () => {
    3030  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
    3131  const id = identityWith('42', 'active_patron', 500);
     
    3333  assert.equal(m.status, 'active_patron');
    3434  assert.equal(m.cents, 500);
    35   assert.equal(pickCampaignMembership(id, '999'), null);   // different campaign
     35});
     36
     37test('pickCampaignMembership: sole membership is used even if campaign_id is wrong (creator-scoped)', async () => {
     38  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
     39  const id = identityWith('42', 'active_patron', 500);
     40  // A typo'd campaign_id must not lock out a real patron: memberships from the
     41  // owner's own client are already their campaign, so fall back to the sole one.
     42  const m = pickCampaignMembership(id, '999');
     43  assert.equal(m.status, 'active_patron');
     44  assert.equal(m.cents, 500);
     45});
     46
     47test('pickCampaignMembership: multiple memberships + no match → null (no silent grant)', async () => {
     48  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
     49  const id = {
     50    data: { type: 'user', id: 'u', relationships: { memberships: { data: [{ type: 'member', id: 'm1' }, { type: 'member', id: 'm2' }] } } },
     51    included: [
     52      { type: 'member', id: 'm1', attributes: { patron_status: 'active_patron', currently_entitled_amount_cents: 300 }, relationships: { campaign: { data: { type: 'campaign', id: '42' } } } },
     53      { type: 'member', id: 'm2', attributes: { patron_status: 'active_patron', currently_entitled_amount_cents: 800 }, relationships: { campaign: { data: { type: 'campaign', id: '77' } } } },
     54    ],
     55  };
     56  assert.equal(pickCampaignMembership(id, '999'), null);      // ambiguous, refuse
     57  assert.equal(pickCampaignMembership(id, '77').cents, 800);  // exact still works
     58});
     59
     60test('pickCampaignMembership: no memberships → null', async () => {
     61  const { pickCampaignMembership } = await import('../src/services/PaidPatreonService.js');
     62  assert.equal(pickCampaignMembership({ data: {}, included: [] }, '42'), null);
    3663});
    3764
Note: See TracChangeset for help on using the changeset viewer.