Skip to content

Commit 15f0cb4

Browse files
fix: redirect unauthenticated users from subscriptions page (#4137)
Co-authored-by: Jean du Plessis <jeandp@gmail.com>
1 parent 4a4f9bf commit 15f0cb4

3 files changed

Lines changed: 45 additions & 8 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ReactNode } from 'react';
2+
import type { User } from '@kilocode/db/schema';
3+
import { defineTestUser } from '@/tests/helpers/user.helper';
4+
5+
const mockGetUserFromAuthOrRedirect = jest.fn<Promise<User>, []>();
6+
7+
jest.mock('@/lib/user/server', () => ({
8+
getUserFromAuthOrRedirect: () => mockGetUserFromAuthOrRedirect(),
9+
}));
10+
11+
describe('SubscriptionsLayout', () => {
12+
const redirectSentinel = new Error('NEXT_REDIRECT');
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('renders subscription pages when authenticated', async () => {
19+
mockGetUserFromAuthOrRedirect.mockResolvedValue(defineTestUser());
20+
21+
const { default: SubscriptionsLayout } = await import('@/app/(app)/subscriptions/layout');
22+
const children = 'subscriptions content' as ReactNode;
23+
24+
await expect(SubscriptionsLayout({ children })).resolves.toBe(children);
25+
expect(mockGetUserFromAuthOrRedirect).toHaveBeenCalledTimes(1);
26+
});
27+
28+
it('propagates a redirect sentinel when unauthenticated', async () => {
29+
mockGetUserFromAuthOrRedirect.mockRejectedValue(redirectSentinel);
30+
31+
const { default: SubscriptionsLayout } = await import('@/app/(app)/subscriptions/layout');
32+
const children = 'subscriptions content' as ReactNode;
33+
34+
await expect(SubscriptionsLayout({ children })).rejects.toBe(redirectSentinel);
35+
expect(mockGetUserFromAuthOrRedirect).toHaveBeenCalledTimes(1);
36+
});
37+
});
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ReactNode } from 'react';
2+
import { getUserFromAuthOrRedirect } from '@/lib/user/server';
23

3-
export default function SubscriptionsLayout({ children }: { children: ReactNode }) {
4+
export default async function SubscriptionsLayout({ children }: { children: ReactNode }) {
5+
await getUserFromAuthOrRedirect();
46
return children;
57
}

apps/web/src/lib/impact/kilo-pass-referrals.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -661,11 +661,9 @@ describe('Kilo Pass Impact referral conversions', () => {
661661
expect(await db.select().from(impact_referral_rewards)).toHaveLength(0);
662662
});
663663

664-
test('renewal, prior subscription, deleted tombstone, and self-referral do not grant rewards', async () => {
665-
const cases = ['renewal', 'prior_subscription', 'deleted_tombstone', 'self_referral'] as const;
666-
667-
for (const scenario of cases) {
668-
await cleanupDbForTest();
664+
test.each(['renewal', 'prior_subscription', 'deleted_tombstone', 'self_referral'] as const)(
665+
'%s does not grant referral rewards',
666+
async scenario => {
669667
const referrer = await insertTestUser({ created_at: '2025-12-01T00:00:00.000Z' });
670668
const referee =
671669
scenario === 'self_referral'
@@ -696,11 +694,11 @@ describe('Kilo Pass Impact referral conversions', () => {
696694

697695
const disposition = await processInvoice({ refereeId: referee.id, subscriptionId });
698696
const rewards = await db.select().from(impact_referral_rewards);
699-
expect({ scenario, rewardCount: rewards.length }).toEqual({ scenario, rewardCount: 0 });
697+
expect(rewards).toHaveLength(0);
700698
expect(disposition.winningTouchType).toBe(ImpactReferralWinningTouchType.Referral);
701699
expect(disposition.disqualificationReason).toMatch(/^referral_/);
702700
}
703-
});
701+
);
704702

705703
test('referrer cap limits only referrer reward and invoice retry is idempotent', async () => {
706704
const referrer = await insertTestUser({ created_at: '2025-12-01T00:00:00.000Z' });

0 commit comments

Comments
 (0)