first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import type { IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
|
||||
|
||||
import { stripeApiRequest } from '../helpers';
|
||||
import { StripeTrigger } from '../StripeTrigger.node';
|
||||
import { verifySignature } from '../StripeTriggerHelpers';
|
||||
|
||||
jest.mock('../helpers', () => ({
|
||||
stripeApiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../StripeTriggerHelpers', () => ({
|
||||
verifySignature: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
const mockedStripeApiRequest = jest.mocked(stripeApiRequest);
|
||||
const mockedVerifySignature = jest.mocked(verifySignature);
|
||||
|
||||
describe('Stripe Trigger Node', () => {
|
||||
let node: StripeTrigger;
|
||||
let mockNodeFunctions: IHookFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
node = new StripeTrigger();
|
||||
|
||||
mockNodeFunctions = {
|
||||
getNodeWebhookUrl: jest.fn().mockReturnValue('https://webhook.url/test'),
|
||||
getWorkflow: jest.fn().mockReturnValue({ id: 'test-workflow-id' }),
|
||||
getNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
getWorkflowStaticData: jest.fn().mockReturnValue({}),
|
||||
getNode: jest.fn().mockReturnValue({ name: 'StripeTrigger' }),
|
||||
getWebhookName: jest.fn().mockReturnValue('default'),
|
||||
getContext: jest.fn(),
|
||||
getActivationMode: jest.fn(),
|
||||
getMode: jest.fn(),
|
||||
getNodeExecutionData: jest.fn(),
|
||||
getRestApiUrl: jest.fn(),
|
||||
getTimezone: jest.fn(),
|
||||
helpers: {} as any,
|
||||
} as unknown as IHookFunctions;
|
||||
|
||||
// (mockNodeFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
// secretKey: 'sk_test_123',
|
||||
// });
|
||||
|
||||
mockedStripeApiRequest.mockResolvedValue({
|
||||
id: 'we_test123',
|
||||
secret: 'whsec_test123',
|
||||
status: 'enabled',
|
||||
enabled_events: ['*'],
|
||||
});
|
||||
|
||||
mockedStripeApiRequest.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not send API version in body if not specified', async () => {
|
||||
(mockNodeFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
||||
if (param === 'events') return ['*'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const expectedRequestBody = {
|
||||
url: 'https://webhook.url/test',
|
||||
description: 'Created by n8n for workflow ID: test-workflow-id',
|
||||
enabled_events: ['*'],
|
||||
};
|
||||
|
||||
const endpoint = '/webhook_endpoints';
|
||||
|
||||
await node.webhookMethods.default.create.call(mockNodeFunctions);
|
||||
expect(mockedStripeApiRequest).toHaveBeenCalledWith('POST', endpoint, expectedRequestBody);
|
||||
|
||||
const callArgs = mockedStripeApiRequest.mock.calls[0];
|
||||
const requestBody = callArgs[2];
|
||||
expect(requestBody).not.toHaveProperty('api_version');
|
||||
});
|
||||
|
||||
it('should send API version in body if specified in node parameters', async () => {
|
||||
(mockNodeFunctions.getNodeParameter as jest.Mock).mockImplementation((param) => {
|
||||
if (param === 'apiVersion') return '2025-05-28.basil';
|
||||
if (param === 'events') return ['*'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const expectedRequestBody = {
|
||||
url: 'https://webhook.url/test',
|
||||
description: 'Created by n8n for workflow ID: test-workflow-id',
|
||||
enabled_events: ['*'],
|
||||
api_version: '2025-05-28.basil',
|
||||
};
|
||||
|
||||
const endpoint = '/webhook_endpoints';
|
||||
|
||||
await node.webhookMethods.default.create.call(mockNodeFunctions);
|
||||
expect(mockedStripeApiRequest).toHaveBeenCalledWith('POST', endpoint, expectedRequestBody);
|
||||
|
||||
const callArgs = mockedStripeApiRequest.mock.calls[0];
|
||||
const requestBody = callArgs[2];
|
||||
expect(requestBody).toHaveProperty('api_version', '2025-05-28.basil');
|
||||
});
|
||||
|
||||
describe('webhook signature verification', () => {
|
||||
let mockWebhookFunctions: IWebhookFunctions;
|
||||
const testBody = { type: 'charge.succeeded', id: 'ch_123' };
|
||||
const rawBody = JSON.stringify(testBody);
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebhookFunctions = {
|
||||
getBodyData: jest.fn().mockReturnValue(testBody),
|
||||
getRequestObject: jest.fn().mockReturnValue({
|
||||
rawBody: Buffer.from(rawBody),
|
||||
body: testBody,
|
||||
}),
|
||||
getResponseObject: jest.fn().mockReturnValue({
|
||||
status: jest.fn().mockReturnThis(),
|
||||
send: jest.fn().mockReturnThis(),
|
||||
end: jest.fn(),
|
||||
}),
|
||||
getNodeParameter: jest.fn().mockReturnValue(['*']),
|
||||
helpers: {
|
||||
returnJsonArray: jest.fn().mockImplementation((data) => [data]),
|
||||
},
|
||||
} as unknown as IWebhookFunctions;
|
||||
|
||||
// Reset the verifySignature mock to return true by default
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('should process webhook with valid signature', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
workflowData: [[testBody]],
|
||||
});
|
||||
expect(mockedVerifySignature).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should reject webhook with invalid signature', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(false);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
noWebhookResponse: true,
|
||||
});
|
||||
expect(mockedVerifySignature).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should handle events filtering correctly', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue([
|
||||
'payment_intent.succeeded',
|
||||
]);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should process webhook when event type matches filter', async () => {
|
||||
mockedVerifySignature.mockResolvedValue(true);
|
||||
(mockWebhookFunctions.getNodeParameter as jest.Mock).mockReturnValue(['charge.succeeded']);
|
||||
|
||||
const result = await node.webhook.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
workflowData: [[testBody]],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import type { IWebhookFunctions } from 'n8n-workflow';
|
||||
|
||||
import { verifySignature } from '../StripeTriggerHelpers';
|
||||
|
||||
describe('StripeTriggerHelpers', () => {
|
||||
describe('verifySignature', () => {
|
||||
let mockWebhookFunctions: IWebhookFunctions;
|
||||
const webhookSecret = 'whsec_test123456789';
|
||||
const getCurrentTimestamp = () => Math.floor(Date.now() / 1000).toString();
|
||||
const testBody = { type: 'charge.succeeded', id: 'ch_123' };
|
||||
const rawBody = JSON.stringify(testBody);
|
||||
|
||||
function generateValidSignature(timestamp: string, body: string, secret: string): string {
|
||||
const signedPayload = `${timestamp}.${body}`;
|
||||
const signature = createHmac('sha256', secret).update(signedPayload).digest('hex');
|
||||
return `t=${timestamp},v1=${signature}`;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebhookFunctions = {
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
signatureSecret: webhookSecret,
|
||||
}),
|
||||
getRequestObject: jest.fn().mockReturnValue({
|
||||
header: jest.fn(),
|
||||
rawBody: Buffer.from(rawBody),
|
||||
}),
|
||||
} as unknown as IWebhookFunctions;
|
||||
});
|
||||
|
||||
it('should return true when no signature secret is provided', async () => {
|
||||
(mockWebhookFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when stripe-signature header is missing', async () => {
|
||||
const mockHeader = jest.fn().mockReturnValue(undefined);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockHeader).toHaveBeenCalledWith('stripe-signature');
|
||||
});
|
||||
|
||||
it('should return false when signature format is invalid', async () => {
|
||||
const mockHeader = jest.fn().mockReturnValue('invalid-format');
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const signature = createHmac('sha256', webhookSecret)
|
||||
.update(`${timestamp}.${rawBody}`)
|
||||
.digest('hex');
|
||||
const mockHeader = jest.fn().mockReturnValue(`v1=${signature}`);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when v1 signature is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const mockHeader = jest.fn().mockReturnValue(`t=${timestamp}`);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when signature is valid', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when signature is invalid', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const wrongSecret = 'wrong_secret';
|
||||
const invalidSignature = generateValidSignature(timestamp, rawBody, wrongSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(invalidSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex signature header with multiple elements', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const signature = createHmac('sha256', webhookSecret)
|
||||
.update(`${timestamp}.${rawBody}`)
|
||||
.digest('hex');
|
||||
const complexHeader = `t=${timestamp},v1=${signature},v0=old_signature`;
|
||||
const mockHeader = jest.fn().mockReturnValue(complexHeader);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle string rawBody', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody, // String instead of Buffer
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when rawBody is missing', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: null,
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when signatureSecret is not a string', async () => {
|
||||
const timestamp = getCurrentTimestamp();
|
||||
const validSignature = generateValidSignature(timestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
(mockWebhookFunctions.getCredentials as jest.Mock).mockResolvedValue({
|
||||
secretKey: 'sk_test_123',
|
||||
signatureSecret: 123, // Not a string
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is older than 5 minutes', async () => {
|
||||
// Create timestamp that's 6 minutes (360 seconds) old
|
||||
const oldTimestamp = (Math.floor(Date.now() / 1000) - 360).toString();
|
||||
const validSignature = generateValidSignature(oldTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when timestamp is from the future beyond tolerance', async () => {
|
||||
// Create timestamp that's 6 minutes (360 seconds) in the future
|
||||
const futureTimestamp = (Math.floor(Date.now() / 1000) + 360).toString();
|
||||
const validSignature = generateValidSignature(futureTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when timestamp is within tolerance', async () => {
|
||||
// Create timestamp that's 4 minutes (240 seconds) old - within 5 minute tolerance
|
||||
const recentTimestamp = (Math.floor(Date.now() / 1000) - 240).toString();
|
||||
const validSignature = generateValidSignature(recentTimestamp, rawBody, webhookSecret);
|
||||
const mockHeader = jest.fn().mockReturnValue(validSignature);
|
||||
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
|
||||
header: mockHeader,
|
||||
rawBody: Buffer.from(rawBody),
|
||||
});
|
||||
|
||||
const result = await verifySignature.call(mockWebhookFunctions);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as helpers from '../helpers';
|
||||
|
||||
describe('adjustMetadata', () => {
|
||||
it('it should adjust multiple metadata values', async () => {
|
||||
const additionalFieldsValues = {
|
||||
metadata: {
|
||||
metadataProperties: [
|
||||
{
|
||||
key: 'keyA',
|
||||
value: 'valueA',
|
||||
},
|
||||
{
|
||||
key: 'keyB',
|
||||
value: 'valueB',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const adjustedMetadata = helpers.adjustMetadata(additionalFieldsValues);
|
||||
|
||||
const expectedAdjustedMetadata = {
|
||||
metadata: {
|
||||
keyA: 'valueA',
|
||||
keyB: 'valueB',
|
||||
},
|
||||
};
|
||||
expect(adjustedMetadata).toStrictEqual(expectedAdjustedMetadata);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
const baseUrl = 'https://api.stripe.com/v1';
|
||||
|
||||
const meterEventResponse = {
|
||||
id: 'evt_test_123',
|
||||
object: 'billing.meter_event',
|
||||
event_name: 'api_request',
|
||||
created: 1705320600,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
livemode: false,
|
||||
};
|
||||
|
||||
describe('Stripe - Meter Event Workflows', () => {
|
||||
const credentials = {
|
||||
stripeApi: {
|
||||
secretKey: 'sk_test_fake_key',
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// Basic meter event creation
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
})
|
||||
.reply(200, meterEventResponse);
|
||||
|
||||
// Meter event with identifier
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
identifier: 'unique_event_id_123',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
identifier: 'unique_event_id_123',
|
||||
});
|
||||
|
||||
// Meter event with custom payload properties
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
endpoint: '/api/v1/users',
|
||||
method: 'GET',
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: 100,
|
||||
endpoint: '/api/v1/users',
|
||||
method: 'GET',
|
||||
},
|
||||
});
|
||||
|
||||
// Negative value support
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.post('/billing/meter_events', {
|
||||
event_name: 'api_request',
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: -50,
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
...meterEventResponse,
|
||||
payload: {
|
||||
stripe_customer_id: 'cus_test123',
|
||||
value: -50,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// NodeTestHarness will discover and run all workflow JSON files in this directory
|
||||
new NodeTestHarness().setupTests({ credentials });
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Basic Create",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - With Custom Payload",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "endpoint",
|
||||
"value": "/api/v1/users"
|
||||
},
|
||||
{
|
||||
"key": "method",
|
||||
"value": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100,
|
||||
"endpoint": "/api/v1/users",
|
||||
"method": "GET"
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Guard Customer ID",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "stripe_customer_id",
|
||||
"value": "cus_malicious_override"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Guard Value",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"customPayload": {
|
||||
"properties": [
|
||||
{
|
||||
"key": "value",
|
||||
"value": "999"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - With Identifier",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": 100,
|
||||
"additionalFields": {
|
||||
"identifier": "unique_event_id_123"
|
||||
}
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"identifier": "unique_event_id_123",
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": 100
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Stripe Meter Event - Negative Value",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "meterEvent",
|
||||
"operation": "create",
|
||||
"eventName": "api_request",
|
||||
"customerId": "cus_test123",
|
||||
"value": -50
|
||||
},
|
||||
"id": "stripe-meter-event",
|
||||
"name": "Stripe",
|
||||
"type": "n8n-nodes-base.stripe",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 0],
|
||||
"credentials": {
|
||||
"stripeApi": {
|
||||
"id": "1",
|
||||
"name": "Stripe API"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stripe",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Stripe": [
|
||||
{
|
||||
"json": {
|
||||
"id": "evt_test_123",
|
||||
"object": "billing.meter_event",
|
||||
"event_name": "api_request",
|
||||
"created": 1705320600,
|
||||
"payload": {
|
||||
"stripe_customer_id": "cus_test123",
|
||||
"value": -50
|
||||
},
|
||||
"livemode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user