Skip to content
Merged
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
42 changes: 42 additions & 0 deletions packages/app/control/src/app/api/v1/user/referral/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,54 @@ import { z } from 'zod';
import { appIdSchema } from '@/services/db/apps/lib/schemas';
import { authRoute } from '../../../../../lib/api/auth-route';
import { setAppMembershipReferrer } from '@/services/db/apps/membership';
import {
getUserAppReferralCode,
createAppReferralCode,
} from '@/services/db/apps/referral-code';

const getUserReferralCodeSchema = z.object({
echoAppId: appIdSchema,
});

const setUserReferrerForAppSchema = z.object({
echoAppId: appIdSchema,
code: z.string(),
});

export const GET = authRoute
.query(getUserReferralCodeSchema)
.handler(async (_, context) => {
const { echoAppId } = context.query;
const userId = context.ctx.userId;

let referralCode = await getUserAppReferralCode(userId, echoAppId);

if (!referralCode) {
referralCode = await createAppReferralCode(userId, {
appId: echoAppId,
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
});

if (!referralCode) {
return NextResponse.json(
{
success: false,
message: 'Failed to create referral code',
},
{ status: 500 }
);
}
}

return NextResponse.json({
success: true,
message: 'Referral code retrieved successfully',
code: referralCode.code,
referralLinkUrl: referralCode.referralLinkUrl,
expiresAt: referralCode.expiresAt,
});
});

export const POST = authRoute
.body(setUserReferrerForAppSchema)
.handler(async (_, context) => {
Expand Down
17 changes: 13 additions & 4 deletions packages/app/control/src/services/db/apps/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,30 @@ export async function setAppMembershipReferrer(
echoAppId: string,
code: string
): Promise<boolean> {
const appMembership = await db.appMembership.findUnique({
// Get or create the app membership
const appMembership = await db.appMembership.upsert({
where: {
userId_echoAppId: {
userId,
echoAppId,
},
referrerId: null,
},
create: {
userId,
echoAppId,
role: AppRole.CUSTOMER,
status: MembershipStatus.ACTIVE,
totalSpent: 0,
},
update: {},
});

if (appMembership) {
// If the user already has a referrer, return false
// Check if user already has a referrer
if (appMembership.referrerId) {
return false;
}

// Validate the referral code exists
Copy link
Contributor

Choose a reason for hiding this comment

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

The setAppMembershipReferrer function doesn't validate whether a referral code has expired, allowing expired codes to be applied to memberships.

View Details
📝 Patch Details
diff --git a/packages/app/control/src/services/db/apps/membership.ts b/packages/app/control/src/services/db/apps/membership.ts
index 192c1992..2db42c68 100644
--- a/packages/app/control/src/services/db/apps/membership.ts
+++ b/packages/app/control/src/services/db/apps/membership.ts
@@ -186,14 +186,14 @@ export async function setAppMembershipReferrer(
     return false;
   }
 
-  // Validate the referral code exists
+  // Validate the referral code exists and hasn't expired
   const referralCode = await db.referralCode.findUnique({
     where: {
       code,
     },
   });
 
-  if (!referralCode) {
+  if (!referralCode || referralCode.expiresAt < new Date()) {
     return false;
   }
 

Analysis

Missing expiration validation in setAppMembershipReferrer allows expired referral codes to be applied

What fails: The setAppMembershipReferrer() function in packages/app/control/src/services/db/apps/membership.ts does not validate whether a referral code has expired before applying it to a membership.

How to reproduce:

  1. Create a referral code with an expiresAt date in the past (code schema supports this via optional expiresAt parameter, defaulting to 1 year in future)
  2. Call setAppMembershipReferrer(userId, echoAppId, expiredCode)
  3. The function returns true and applies the expired code to the membership

Result: Expired referral codes are accepted and applied. The function succeeds even when referralCode.expiresAt < new Date().

Expected: Function should return false for expired codes, matching the pattern used in other similar functions and the error message which states codes "may be invalid, expired, or you may already have a referrer for this app"

Verification: The same expiration validation pattern is correctly implemented in:

  • getCreditGrantCode() in packages/app/control/src/services/db/credits/grant.ts - uses expiresAt: { gt: new Date() } in WHERE clause
  • findRefreshToken() in packages/app/control/src/services/db/auth/refresh.ts - uses expiresAt: { gt: new Date() } in WHERE clause

The fix adds the missing expiration check: if (!referralCode || referralCode.expiresAt < new Date())

const referralCode = await db.referralCode.findUnique({
where: {
code,
Expand Down
83 changes: 82 additions & 1 deletion packages/tests/integration/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,82 @@
# Integration test trigger
# Integration Tests

This package contains integration tests for the Echo platform.

## Setup

1. Set up the test environment:
```bash
pnpm env:setup
```

2. Seed the test database:
```bash
pnpm db:seed
```

## Running Tests

Run all integration tests:
```bash
pnpm test:watch
```

Run specific test suites:
```bash
# Echo Data Server tests
pnpm test:echo-data-server

# OAuth Protocol tests
pnpm test:oauth-protocol
```

## Test Suites

### Echo Data Server Tests
Located in `tests/echo-data-server/`:
- `api-key.client.test.ts` - API key authentication and usage
- `402-auth.client.test.ts` - Payment required (402) authentication flow
- `echo-access-jwt.client.test.ts` - JWT token validation
- `free-tier.client.test.ts` - Free tier functionality
- `referral-code.client.test.ts` - Referral code creation and application
- `in-flight-requests.test.ts` - Concurrent request handling

### OAuth Protocol Tests
Located in `tests/oauth-protocol/`:
- OAuth authorization flow
- Token refresh and lifecycle
- PKCE security
- CSRF vulnerability testing

## Referral Code Tests

The referral code integration tests (`referral-code.client.test.ts`) cover:

1. **GET endpoint** - Retrieval of referral codes:
- Retrieving an existing referral code for a user
- Auto-creating a referral code for users who don't have one
- Ensuring consistency across multiple requests

2. **POST endpoint** - Application of referral codes:
- Successfully applying another user's referral code
- Rejecting invalid referral codes
- Preventing users from applying codes when they already have a referrer

### Test Data

Referral code test data is defined in `config/test-data.ts`:
- Primary user has referral code: `TEST-REFERRAL-CODE-PRIMARY`
- Secondary user has referral code: `TEST-REFERRAL-CODE-SECONDARY`
- Tertiary user has no referral code (created during tests)

## Database Management

Reset and reseed the database:
```bash
pnpm db:reset-and-seed
```

Reset only:
```bash
pnpm db:reset
```
31 changes: 31 additions & 0 deletions packages/tests/integration/config/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ export const TEST_DATA = {
},
},

// Referral code configurations
referralCodes: {
primaryUserCode: {
id: '88888888-8888-4888-8888-888888888888',
code: 'TEST-REFERRAL-CODE-PRIMARY',
userId: '11111111-1111-4111-8111-111111111111', // Primary test user
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year from now
isArchived: false,
usedAt: null,
},
secondaryUserCode: {
id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
code: 'TEST-REFERRAL-CODE-SECONDARY',
userId: '33333333-3333-4333-8333-333333333333', // Secondary test user
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year from now
isArchived: false,
usedAt: null,
},
},

// Test timeouts and delays
timeouts: {
default: 30000,
Expand Down Expand Up @@ -234,6 +254,16 @@ export const TEST_SPEND_POOL_IDS = {
primary: TEST_DATA.spendPools.primary.id,
};

export const TEST_REFERRAL_CODE_IDS = {
primary: TEST_DATA.referralCodes.primaryUserCode.id,
secondary: TEST_DATA.referralCodes.secondaryUserCode.id,
};

export const TEST_REFERRAL_CODES = {
primary: TEST_DATA.referralCodes.primaryUserCode.code,
secondary: TEST_DATA.referralCodes.secondaryUserCode.code,
};

// Type definitions for test data
export type TestData = typeof TEST_DATA;
export type TestUser = typeof TEST_DATA.users.primary;
Expand All @@ -242,3 +272,4 @@ export type TestApiKey = typeof TEST_DATA.apiKeys.primary;
export type TestSpendPool = typeof TEST_DATA.spendPools.primary;
export type TestUserSpendPoolUsage =
typeof TEST_DATA.userSpendPoolUsage.tertiaryUserPrimaryPool;
export type TestReferralCode = typeof TEST_DATA.referralCodes.primaryUserCode;
13 changes: 13 additions & 0 deletions packages/tests/integration/scripts/seed-integration-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function seedIntegrationDatabase() {
await prisma.spendPool.deleteMany();
await prisma.apiKey.deleteMany();
await prisma.appMembership.deleteMany();
await prisma.referralCode.deleteMany();
await prisma.echoApp.deleteMany();
await prisma.user.deleteMany();

Expand Down Expand Up @@ -171,6 +172,17 @@ export async function seedIntegrationDatabase() {

console.log('🤖 Created test LLM transaction');

// Create test referral codes
await prisma.referralCode.create({
data: TEST_DATA.referralCodes.primaryUserCode,
});

await prisma.referralCode.create({
data: TEST_DATA.referralCodes.secondaryUserCode,
});

console.log('🎟️ Created test referral codes');

console.log('✅ Integration test database seeded successfully');
console.log('\n📊 Summary:');
console.log(` - Users: 3`);
Expand All @@ -181,6 +193,7 @@ export async function seedIntegrationDatabase() {
console.log(` - User Spend Pool Usage: 1`);
console.log(` - Payments: 1`);
console.log(` - LLM Transactions: 1`);
console.log(` - Referral Codes: 2`);
} catch (error) {
console.error('❌ Error seeding integration test database:', error);
throw error;
Expand Down
Loading
Loading