Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: Add Integration and unit tests on Billing #9317

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

anamarn
Copy link
Contributor

@anamarn anamarn commented Jan 2, 2025

Solves [ https://github.com/twentyhq/private-issues/issues/214 ]

TLDR
Add unit and integration tests to Billing. First approach to run jest integration tests directly from VSCode.

In order to run the unit tests:
Run unit test using the CLI or with the jest extension directly from VSCode.

In order to run the integration tests:
Ensure that your database has the billingTables. If that's not the case, migrate the database with IS_BILLING_ENABLED set to true:
npx nx run twenty-server:test:integration test/integration/billing/suites/billing-controller.integration-spec.ts

Doing:

  • Unit test on transformSubscriptionEventToSubscriptionItem
  • More tests cases in billingController integration tests.

@anamarn anamarn requested a review from FelixMalfait January 2, 2025 12:53
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

This PR adds comprehensive test coverage for the billing functionality, focusing on Stripe webhook handling and data transformations.

  • Added extensive unit tests in /packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/ covering all Stripe data transformation utilities with thorough edge cases
  • Added integration tests in /packages/twenty-server/test/integration/billing/suites/billing-controller.integration-spec.ts for webhook handling of product, price, and subscription events
  • Modified nx.json and .eslintrc.cjs to properly configure integration test files with pattern *.integration-spec.ts
  • Added mock StripeService in create-app.ts for webhook testing, though signature validation could be more robust
  • Incomplete test coverage for entitlement handling as noted by commented out test case

14 file(s) reviewed, 20 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +7 to +11
it('should return true if metadata is empty', () => {
const metadata: Stripe.Metadata = {};

expect(isStripeValidProductMetadata(metadata)).toBe(true);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Empty metadata returning true but missing required keys returning false (line 49-55) seems inconsistent. Should validate this is the intended behavior.

Comment on lines +99 to +103
files: [
'*.spec.@(ts|tsx|js|jsx)',
'*.integration-spec.@(ts|tsx|js|jsx)',
'*.test.@(ts|tsx|js|jsx)',
],
Copy link
Contributor

Choose a reason for hiding this comment

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

style: test file patterns could be consolidated into a single pattern like '*.@(spec|integration-spec|test).@(ts|tsx|js|jsx)' for better maintainability

]);
});

it('should return the SSO key with false value,should only render the values that are listed in BillingEntitlementKeys', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Test description contains a comma splice. Consider splitting into two test cases for clarity: one for SSO false value and one for BillingEntitlementKeys filtering

default_aggregation: {
formula: 'sum',
},
event_time_window: null,
Copy link
Contributor

Choose a reason for hiding this comment

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

style: test should also verify behavior with event_time_window='hour' since that's another valid value

Comment on lines +38 to +39
mockData as any,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: avoid using 'any' type casting - should define proper type for mockData based on Stripe.PriceCreatedEvent.Data

await client
.post('/billing/webhooks')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.set('stripe-signature', 'correct-signature')
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Using 'correct-signature' as a static value doesn't properly test signature verification. Should use Stripe's signing secret to generate valid signatures for test payloads.

Comment on lines +34 to +44
constructEventFromPayload: (signature: string, payload: Buffer) => {
if (signature === 'correct-signature') {
const body = JSON.parse(payload.toString());

return {
type: body.type,
data: body.data,
};
}
throw new Error('Invalid signature');
},
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Mock implementation is overly simplified and may miss edge cases. Consider using Stripe's testing webhooks or a more robust signature verification process.

Comment on lines 57 to 59
verify: (req: any, res, buf) => {
req.rawBody = buf;
},
Copy link
Contributor

Choose a reason for hiding this comment

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

style: req is typed as 'any'. Consider creating a proper type extending Express.Request

describe('BillingController (integration)', () => {
const mockTimestamp = 1672531200;

//put this in utils?
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Mock data creation utilities should be moved to a shared test utils file for reuse across test suites

@@ -25,7 +28,21 @@ export const createApp = async (
): Promise<NestExpressApplication> => {
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
imports: [AppModule],
});
})
Copy link
Member

Choose a reason for hiding this comment

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

as discussed, let's initialize the app with the same parameters as the real app (i.e. cors true and rawbody true)

@@ -35,6 +52,14 @@ export const createApp = async (

const app = moduleFixture.createNestApplication<NestExpressApplication>();

app.use(
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this

const client = request(`http://localhost:${APP_PORT}`);

// TODO: Add more tests
// THIS TEST ONLY WORKS IF THE BILLING TABLES ARE CREATED, VERIFY IS_BILLING_ENABLED IS TRUE
Copy link
Member

Choose a reason for hiding this comment

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

check ci-server.yaml

.set('stripe-signature', 'correct-signature')
.set('Content-Type', 'application/json')
.send(JSON.stringify(payload))
.expect(200);
Copy link
Member

Choose a reason for hiding this comment

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

You're not testing much here when just checking the status code, could you check something else everytime you check a 200, for example modify the response so that it provides the id of the object we just inserted/modified, and then in the test you verify that the request returned a uuid as a response

@@ -43,6 +43,8 @@ export class BillingController {
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
let resultBody = {};
Copy link
Member

@Weiko Weiko Jan 3, 2025

Choose a reason for hiding this comment

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

Style notes:
You can instead create another method like handleStripeEvent that will return the body to simplify your code

const result = await this.handleStripeEvent(event);

return res.status(200).send(result).end();
private async handleStripeEvent(event: WebhookEvent) {
  switch (event.type) {
    case WebhookEvent.SETUP_INTENT_SUCCEEDED:
      return this.billingSubscriptionService.handleUnpaidInvoices(event.data);
    case WebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED:
    case WebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED:
    case WebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED:
    ......

@@ -54,7 +56,9 @@ export class BillingController {
);

if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename this WebhookEvent to BillingWebhookEvent?

Copy link
Member

@Weiko Weiko left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants