Skip to content

Commit 3d2a45b

Browse files
authored
Create webhook when config is created (#1873)
* validate rk in trpc * test * webhook manager * form errors handling * Build webhook url * Create webhook on configuration saving * create add webhook id * update tests * fix url building * Webhook manager tests * tests * fix test * change supported events * fix tests
1 parent 3ff5d8a commit 3d2a45b

21 files changed

+656
-24
lines changed

apps/stripe/src/__tests__/mocks/mock-stripe-config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const mockedStripeConfig = StripeConfig.create({
1010
publishableKey: mockedStripePublishableKey,
1111
restrictedKey: mockedStripeRestrictedKey,
1212
webhookSecret: mockStripeWebhookSecret,
13+
webhookId: "wh_123456789",
1314
})._unsafeUnwrap();

apps/stripe/src/app/api/stripe/webhook/webhook-params.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,10 @@ export class WebhookParams {
7878
}
7979
}
8080

81-
static createFromParams() {
82-
// todo
83-
}
84-
85-
createWebhookUrl() {
86-
// todo
81+
static createFromParams(params: { saleorApiUrl: SaleorApiUrl; configurationId: string }) {
82+
return new WebhookParams({
83+
saleorApiUrl: params.saleorApiUrl,
84+
configurationId: params.configurationId,
85+
});
8786
}
8887
}

apps/stripe/src/modules/app-config/file-app-config-repo.ts

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class FileAppConfigRepo implements AppConfigRepo {
3535
restrictedKey: z.string(),
3636
publishableKey: z.string(),
3737
webhookSecret: z.string(),
38+
webhookId: z.string(),
3839
}),
3940
),
4041
channelMapping: z.record(z.string(), z.string()),
@@ -47,6 +48,7 @@ export class FileAppConfigRepo implements AppConfigRepo {
4748
restrictedKey: config.restrictedKey,
4849
publishableKey: config.publishableKey,
4950
webhookSecret: config.webhookSecret,
51+
webhookId: config.webhookId,
5052
};
5153
}
5254

@@ -175,6 +177,7 @@ export class FileAppConfigRepo implements AppConfigRepo {
175177
restrictedKey: restrictedKey,
176178
publishableKey: publishableKey,
177179
webhookSecret: whSecret,
180+
webhookId: resolvedConfig.webhookId,
178181
})._unsafeUnwrap();
179182

180183
return ok(stripeConfig);
@@ -226,6 +229,7 @@ export class FileAppConfigRepo implements AppConfigRepo {
226229
publishableKey: createStripePublishableKey(configJson.publishableKey)._unsafeUnwrap(),
227230
restrictedKey: createStripeRestrictedKey(configJson.restrictedKey)._unsafeUnwrap(),
228231
webhookSecret: createStripeWebhookSecret(configJson.webhookSecret)._unsafeUnwrap(),
232+
webhookId: configJson.webhookId,
229233
})._unsafeUnwrap();
230234

231235
return acc;

apps/stripe/src/modules/app-config/stripe-config.test.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("StripeConfig", () => {
2222
publishableKey: mockedStripePublishableKey,
2323
restrictedKey: mockedStripeRestrictedKey,
2424
webhookSecret: mockStripeWebhookSecret,
25+
webhookId: mockStripeWebhookSecret,
2526
});
2627

2728
expect(result.isOk()).toBe(true);
@@ -38,6 +39,7 @@ describe("StripeConfig", () => {
3839
publishableKey: mockedStripePublishableKey,
3940
restrictedKey: mockedStripeRestrictedKey,
4041
webhookSecret: mockStripeWebhookSecret,
42+
webhookId: mockStripeWebhookSecret,
4143
});
4244

4345
expect(result.isErr()).toBe(true);
@@ -52,6 +54,7 @@ describe("StripeConfig", () => {
5254
publishableKey: mockedStripePublishableKey,
5355
restrictedKey: mockedStripeRestrictedKey,
5456
webhookSecret: mockStripeWebhookSecret,
57+
webhookId: mockStripeWebhookSecret,
5558
});
5659

5760
expect(result.isErr()).toBe(true);
@@ -66,6 +69,7 @@ describe("StripeConfig", () => {
6669
publishableKey: mockedStripePublishableKeyTest,
6770
restrictedKey: mockedStripeRestrictedKey,
6871
webhookSecret: mockStripeWebhookSecret,
72+
webhookId: mockStripeWebhookSecret,
6973
})._unsafeUnwrapErr();
7074

7175
const instance2 = StripeConfig.create({
@@ -74,11 +78,16 @@ describe("StripeConfig", () => {
7478
publishableKey: mockedStripePublishableKey,
7579
restrictedKey: mockedStripeRestrictedKeyTest,
7680
webhookSecret: mockStripeWebhookSecret,
81+
webhookId: mockStripeWebhookSecret,
7782
})._unsafeUnwrapErr();
7883

79-
expect(instance1).toMatchInlineSnapshot(`[ValidationError: Publishable key and restricted key must be of the same environment - TEST or LIVE]`);
84+
expect(instance1).toMatchInlineSnapshot(
85+
`[ValidationError: Publishable key and restricted key must be of the same environment - TEST or LIVE]`,
86+
);
8087

81-
expect(instance2).toMatchInlineSnapshot(`[ValidationError: Publishable key and restricted key must be of the same environment - TEST or LIVE]`);
88+
expect(instance2).toMatchInlineSnapshot(
89+
`[ValidationError: Publishable key and restricted key must be of the same environment - TEST or LIVE]`,
90+
);
8291
});
8392
});
8493

apps/stripe/src/modules/app-config/stripe-config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class StripeConfig {
1111
readonly restrictedKey: StripeRestrictedKey;
1212
readonly publishableKey: StripePublishableKey;
1313
readonly webhookSecret: StripeWebhookSecret;
14+
readonly webhookId: string;
1415

1516
static ValidationError = BaseError.subclass("ValidationError", {
1617
props: {
@@ -24,18 +25,21 @@ export class StripeConfig {
2425
restrictedKey: StripeRestrictedKey;
2526
publishableKey: StripePublishableKey;
2627
webhookSecret: StripeWebhookSecret;
28+
webhookId: string;
2729
}) {
2830
this.name = props.name;
2931
this.id = props.id;
3032
this.restrictedKey = props.restrictedKey;
3133
this.publishableKey = props.publishableKey;
3234
this.webhookSecret = props.webhookSecret;
35+
this.webhookId = props.webhookId;
3336
}
3437

3538
static create(args: {
3639
name: string;
3740
id: string;
3841
webhookSecret: StripeWebhookSecret;
42+
webhookId: string;
3943
restrictedKey: StripeRestrictedKey;
4044
publishableKey: StripePublishableKey;
4145
}): Result<StripeConfig, InstanceType<typeof StripeConfig.ValidationError>> {
@@ -72,6 +76,7 @@ export class StripeConfig {
7276
restrictedKey: args.restrictedKey,
7377
publishableKey: args.publishableKey,
7478
webhookSecret: args.webhookSecret,
79+
webhookId: args.webhookId,
7580
}),
7681
);
7782
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { GetStripeConfigTrpcHandler } from "@/modules/app-config/trpc-handlers/get-stripe-config-trpc-handler";
22
import { GetStripeConfigsListTrpcHandler } from "@/modules/app-config/trpc-handlers/get-stripe-configs-list-trpc-handler";
33
import { NewStripeConfigTrpcHandler } from "@/modules/app-config/trpc-handlers/new-stripe-config-trpc-handler";
4+
import { StripeWebhookManager } from "@/modules/stripe/stripe-webhook-manager";
45
import { router } from "@/modules/trpc/trpc-server";
56

67
/**
78
* TODO Figure out end-to-end router testing (must somehow check valid jwt token)
89
*/
910
export const appConfigRouter = router({
1011
getStripeConfig: new GetStripeConfigTrpcHandler().getTrpcProcedure(),
11-
saveNewStripeConfig: new NewStripeConfigTrpcHandler().getTrpcProcedure(),
12+
saveNewStripeConfig: new NewStripeConfigTrpcHandler({
13+
webhookManager: new StripeWebhookManager(),
14+
}).getTrpcProcedure(),
1215
getStripeConfigsList: new GetStripeConfigsListTrpcHandler().getTrpcProcedure(),
1316
});

apps/stripe/src/modules/app-config/trpc-handlers/get-stripe-config-trpc-handler.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const getTestCaller = () => {
3232
token: mockedAppToken,
3333
configRepo: mockedAppConfigRepo,
3434
apiClient: mockedGraphqlClient,
35+
appUrl: "https://localhost:3000",
3536
}),
3637
};
3738
};

apps/stripe/src/modules/app-config/trpc-handlers/new-stripe-config-input-schema.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ export const newStripeConfigInputSchema = z.object({
3838
);
3939
}),
4040
});
41+
42+
export type NewStripeConfigInput = z.infer<typeof newStripeConfigInputSchema>;

apps/stripe/src/modules/app-config/trpc-handlers/new-stripe-config-trpc-handler.test.ts

+55-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { err, ok } from "neverthrow";
2+
import Stripe from "stripe";
23
import { beforeEach, describe, expect, it, vi } from "vitest";
34

45
import { mockedAppConfigRepo } from "@/__tests__/mocks/app-config-repo";
@@ -7,16 +8,23 @@ import { mockedGraphqlClient } from "@/__tests__/mocks/graphql-client";
78
import { mockedStripePublishableKey } from "@/__tests__/mocks/mocked-stripe-publishable-key";
89
import { mockedStripeRestrictedKey } from "@/__tests__/mocks/mocked-stripe-restricted-key";
910
import { mockedSaleorApiUrl } from "@/__tests__/mocks/saleor-api-url";
11+
import { mockStripeWebhookSecret } from "@/__tests__/mocks/stripe-webhook-secret";
1012
import { TEST_Procedure } from "@/__tests__/trpc-testing-procedure";
1113
import { BaseError } from "@/lib/errors";
1214
import { NewStripeConfigTrpcHandler } from "@/modules/app-config/trpc-handlers/new-stripe-config-trpc-handler";
15+
import { StripeClient } from "@/modules/stripe/stripe-client";
16+
import { StripeWebhookManager } from "@/modules/stripe/stripe-webhook-manager";
1317
import { router } from "@/modules/trpc/trpc-server";
1418

19+
const webhookCreator = new StripeWebhookManager();
20+
1521
/**
1622
* TODO: Probably create some test abstraction to bootstrap trpc handler for testing
1723
*/
1824
const getTestCaller = () => {
19-
const instance = new NewStripeConfigTrpcHandler();
25+
const instance = new NewStripeConfigTrpcHandler({
26+
webhookManager: webhookCreator,
27+
});
2028

2129
// @ts-expect-error - context doesnt match but its applied in test
2230
instance.baseProcedure = TEST_Procedure;
@@ -27,19 +35,38 @@ const getTestCaller = () => {
2735

2836
return {
2937
mockedAppConfigRepo,
38+
webhookCreator,
3039
caller: testRouter.createCaller({
3140
appId: mockedSaleorAppId,
3241
saleorApiUrl: mockedSaleorApiUrl,
3342
token: mockedAppToken,
3443
configRepo: mockedAppConfigRepo,
3544
apiClient: mockedGraphqlClient,
45+
appUrl: "https://localhost:3000",
3646
}),
3747
};
3848
};
3949

4050
describe("NewStripeConfigTrpcHandler", () => {
51+
const stripe = new Stripe("key");
52+
4153
beforeEach(() => {
4254
vi.resetAllMocks();
55+
56+
vi.spyOn(stripe.paymentIntents, "list").mockImplementation(() => {
57+
return Promise.resolve({}) as Stripe.ApiListPromise<Stripe.PaymentIntent>;
58+
});
59+
vi.spyOn(StripeClient, "createFromRestrictedKey").mockImplementation(() => {
60+
return {
61+
nativeClient: stripe,
62+
};
63+
});
64+
vi.spyOn(webhookCreator, "createWebhook").mockImplementation(async () =>
65+
ok({
66+
id: "whid_1234",
67+
secret: mockStripeWebhookSecret,
68+
}),
69+
);
4370
});
4471

4572
it("Returns error 500 if repository fails to save config", async () => {
@@ -109,20 +136,42 @@ describe("NewStripeConfigTrpcHandler", () => {
109136
config: {
110137
id: expect.any(String),
111138
},
112-
},
113-
`
139+
}, `
114140
{
115141
"appId": "saleor-app-id",
116142
"config": {
117143
"id": Any<String>,
118144
"name": "Test config",
119145
"publishableKey": "pk_live_1",
120146
"restrictedKey": "rk_live_AAAAABBBBCCCCCEEEEEEEFFFFFGGGGG",
121-
"webhookSecret": "whsec_TODO",
147+
"webhookId": "whid_1234",
148+
"webhookSecret": "whsec_XYZ",
122149
},
123150
"saleorApiUrl": "https://foo.bar.saleor.cloud/graphql/",
124151
}
125-
`,
126-
);
152+
`);
153+
});
154+
155+
describe("Stripe Auth", () => {
156+
it("Calls auth service and returns error if Stripe RK is invalid", () => {
157+
// @ts-expect-error - mocking stripe client
158+
vi.spyOn(stripe.paymentIntents, "list").mockImplementationOnce(async () => {
159+
throw new Error("Invalid key");
160+
});
161+
162+
const { caller } = getTestCaller();
163+
164+
return expect(() =>
165+
caller.testProcedure({
166+
name: "Test config",
167+
publishableKey: mockedStripePublishableKey,
168+
restrictedKey: mockedStripeRestrictedKey,
169+
}),
170+
).rejects.toThrowErrorMatchingInlineSnapshot(
171+
`[TRPCError: Failed to create Stripe configuration. Restricted key is invalid]`,
172+
);
173+
174+
expect(stripe.paymentIntents.list).toHaveBeenCalledOnce();
175+
});
127176
});
128177
});

0 commit comments

Comments
 (0)