Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Vercel CLI 41.6.2

See [Securing cron jobs](https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs) for more information.

5. On your Vercel project, visit the Storage tab (Vercel Dashboard > (Your Project) > Storage tab) and create a new Upstash Redis database. You should be prompted to connect your new store to your project, if not, connect it manually. Once connected, you should see the `KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN` environment variables in your project. This database is used to store state for your example marketplace integration.
5. On your Vercel project, visit the Storage tab (Vercel Dashboard > (Your Project) > Storage tab) and create a new key-value database. You should be prompted to connect your new store to your project, if not, connect it manually. Once connected, you should see the `KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN` environment variables in your project. This integration uses the `KV_URL` environment variable to connect directly to your key-value database using ioredis. The database is used to store state for your example marketplace integration.

![](/docs/assets/storage-upstash-redis.png)

Expand Down
110 changes: 110 additions & 0 deletions __tests__/partner-simple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Mock the Redis module first
jest.mock('../lib/redis', () => ({
kv: {
get: jest.fn(),
set: jest.fn(),
del: jest.fn(),
lrange: jest.fn(),
lpush: jest.fn(),
lrem: jest.fn(),
ltrim: jest.fn(),
incrby: jest.fn(),
pipeline: jest.fn(() => ({
get: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
del: jest.fn().mockReturnThis(),
lrem: jest.fn().mockReturnThis(),
lpush: jest.fn().mockReturnThis(),
ltrim: jest.fn().mockReturnThis(),
exec: jest.fn(),
})),
},
}));

// Mock external API calls
jest.mock('../lib/vercel/marketplace-api', () => ({
getInvoice: jest.fn(),
importResource: jest.fn(),
}));

import {
listInstallations,
getInstallation,
getResource,
} from '../lib/partner';
import { kv } from '../lib/redis';

const mockKv = kv as jest.Mocked<typeof kv>;

describe('Partner Integration - Key Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('Installation Management', () => {
it('should list installations', async () => {
const installationIds = ['install1', 'install2', 'install3'];
mockKv.lrange.mockResolvedValue(installationIds);

const result = await listInstallations();

expect(mockKv.lrange).toHaveBeenCalledWith('installations', 0, -1);
expect(result).toEqual(installationIds);
});

it('should get installation details', async () => {
const installation = {
type: 'marketplace',
installationId: 'test-installation',
userId: 'test-user',
teamId: 'test-team',
billingPlanId: 'default',
};
mockKv.get.mockResolvedValue(installation);

const result = await getInstallation('test-installation');

expect(mockKv.get).toHaveBeenCalledWith('test-installation');
expect(result).toEqual(installation);
});

it('should throw error for missing installation', async () => {
mockKv.get.mockResolvedValue(null);

await expect(getInstallation('missing-installation'))
.rejects.toThrow("Installation 'missing-installation' not found");
});
});

describe('Resource Management', () => {
it('should get a resource', async () => {
const serializedResource = {
id: 'resource-123',
name: 'Test Resource',
status: 'ready',
productId: 'test-product',
metadata: {},
billingPlan: 'default',
};
mockKv.get.mockResolvedValue(serializedResource);

const result = await getResource('test-installation', 'resource-123');

expect(mockKv.get).toHaveBeenCalledWith('test-installation:resource:resource-123');
expect(result).toMatchObject({
id: 'resource-123',
name: 'Test Resource',
status: 'ready',
billingPlan: expect.objectContaining({ id: 'default' }),
});
});

it('should return null for missing resource', async () => {
mockKv.get.mockResolvedValue(null);

const result = await getResource('test-installation', 'missing-resource');

expect(result).toBeNull();
});
});
});
29 changes: 29 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const nextJest = require('next/jest')

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files
dir: './',
})

// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
testEnvironment: 'jest-environment-node',
testMatch: [
'**/__tests__/**/*.(test|spec).(js|jsx|ts|tsx)',
'**/*.(test|spec).(js|jsx|ts|tsx)'
],
modulePathIgnorePatterns: [
'<rootDir>/.next/',
'<rootDir>/node_modules/',
],
transformIgnorePatterns: [
'node_modules/(?!(nanoid)/)',
],
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
13 changes: 13 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`

// Set up environment variables for testing
process.env.KV_URL = 'redis://localhost:6379';
process.env.INTEGRATION_CLIENT_ID = 'test-client-id';
process.env.INTEGRATION_CLIENT_SECRET = 'test-client-secret';
process.env.CRON_SECRET = 'test-cron-secret';

// Mock nanoid to avoid ES module issues
jest.mock('nanoid', () => ({
nanoid: () => 'test-id-123',
}));
Loading