mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
464 lines
12 KiB
Markdown
464 lines
12 KiB
Markdown
# Third-Party Service Mocking
|
|
|
|
## Table of Contents
|
|
|
|
1. [OAuth/SSO Mocking](#oauthsso-mocking)
|
|
2. [Payment Gateway Mocking](#payment-gateway-mocking)
|
|
3. [Email Verification](#email-verification)
|
|
4. [SMS Verification](#sms-verification)
|
|
5. [Analytics & Tracking](#analytics--tracking)
|
|
|
|
## OAuth/SSO Mocking
|
|
|
|
### Mock Google OAuth
|
|
|
|
```typescript
|
|
test("Google OAuth login", async ({ page }) => {
|
|
// Mock the OAuth callback
|
|
await page.route("**/auth/google/callback**", (route) => {
|
|
const url = new URL(route.request().url());
|
|
// Simulate successful OAuth by redirecting with token
|
|
route.fulfill({
|
|
status: 302,
|
|
headers: {
|
|
Location: "/dashboard?token=mock-jwt-token",
|
|
},
|
|
});
|
|
});
|
|
|
|
// Mock the token verification endpoint
|
|
await page.route("**/api/auth/verify", (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
valid: true,
|
|
user: {
|
|
id: "123",
|
|
email: "test@gmail.com",
|
|
name: "Test User",
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await page.getByRole("button", { name: "Sign in with Google" }).click();
|
|
|
|
await expect(page.getByText("Welcome, Test User")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
### OAuth Fixture
|
|
|
|
```typescript
|
|
// fixtures/oauth.fixture.ts
|
|
type OAuthProvider = "google" | "github" | "microsoft";
|
|
|
|
type OAuthUser = {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
avatar?: string;
|
|
};
|
|
|
|
type OAuthFixtures = {
|
|
mockOAuth: (provider: OAuthProvider, user: OAuthUser) => Promise<void>;
|
|
};
|
|
|
|
export const test = base.extend<OAuthFixtures>({
|
|
mockOAuth: async ({ page }, use) => {
|
|
await use(async (provider, user) => {
|
|
// Mock callback redirect
|
|
await page.route(`**/auth/${provider}/callback**`, (route) =>
|
|
route.fulfill({
|
|
status: 302,
|
|
headers: { Location: `/auth/success?provider=${provider}` },
|
|
}),
|
|
);
|
|
|
|
// Mock session/user endpoint
|
|
await page.route("**/api/auth/session", (route) =>
|
|
route.fulfill({
|
|
json: { user, provider, authenticated: true },
|
|
}),
|
|
);
|
|
|
|
// Mock user info endpoint
|
|
await page.route("**/api/me", (route) => route.fulfill({ json: user }));
|
|
});
|
|
},
|
|
});
|
|
|
|
// Usage
|
|
test("login with GitHub", async ({ page, mockOAuth }) => {
|
|
await mockOAuth("github", {
|
|
id: "gh-123",
|
|
email: "dev@github.com",
|
|
name: "GitHub User",
|
|
});
|
|
|
|
await page.goto("/login");
|
|
await page.getByRole("button", { name: "Sign in with GitHub" }).click();
|
|
|
|
await expect(page.getByText("Welcome, GitHub User")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
### Mock SAML SSO
|
|
|
|
```typescript
|
|
test("SAML SSO login", async ({ page }) => {
|
|
// Mock SAML assertion consumer service
|
|
await page.route("**/saml/acs", async (route) => {
|
|
route.fulfill({
|
|
status: 302,
|
|
headers: {
|
|
Location: "/dashboard",
|
|
"Set-Cookie": "session=mock-saml-session; Path=/; HttpOnly",
|
|
},
|
|
});
|
|
});
|
|
|
|
// Mock session validation
|
|
await page.route("**/api/session", (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
user: { email: "user@company.com", name: "SSO User" },
|
|
provider: "saml",
|
|
},
|
|
}),
|
|
);
|
|
|
|
await page.goto("/login");
|
|
await page.getByRole("button", { name: "SSO Login" }).click();
|
|
|
|
await expect(page).toHaveURL("/dashboard");
|
|
});
|
|
```
|
|
|
|
## Payment Gateway Mocking
|
|
|
|
### Mock Stripe
|
|
|
|
```typescript
|
|
test("Stripe checkout", async ({ page }) => {
|
|
// Mock Stripe.js
|
|
await page.addInitScript(() => {
|
|
(window as any).Stripe = () => ({
|
|
elements: () => ({
|
|
create: () => ({
|
|
mount: () => {},
|
|
on: () => {},
|
|
destroy: () => {},
|
|
}),
|
|
}),
|
|
confirmCardPayment: async () => ({
|
|
paymentIntent: { status: "succeeded", id: "pi_mock_123" },
|
|
}),
|
|
createPaymentMethod: async () => ({
|
|
paymentMethod: { id: "pm_mock_123" },
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Mock backend payment endpoint
|
|
await page.route("**/api/create-payment-intent", (route) =>
|
|
route.fulfill({
|
|
json: { clientSecret: "pi_mock_123_secret_mock" },
|
|
}),
|
|
);
|
|
|
|
await page.route("**/api/confirm-payment", (route) =>
|
|
route.fulfill({
|
|
json: { success: true, orderId: "order-123" },
|
|
}),
|
|
);
|
|
|
|
await page.goto("/checkout");
|
|
await page.getByRole("button", { name: "Pay $99.99" }).click();
|
|
|
|
await expect(page.getByText("Payment successful")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
### Mock PayPal
|
|
|
|
```typescript
|
|
test("PayPal checkout", async ({ page }) => {
|
|
// Mock PayPal SDK
|
|
await page.addInitScript(() => {
|
|
(window as any).paypal = {
|
|
Buttons: () => ({
|
|
render: () => Promise.resolve(),
|
|
isEligible: () => true,
|
|
}),
|
|
FUNDING: { PAYPAL: "paypal", CARD: "card" },
|
|
};
|
|
});
|
|
|
|
// Mock PayPal order creation
|
|
await page.route("**/api/paypal/create-order", (route) =>
|
|
route.fulfill({
|
|
json: { orderId: "PAYPAL-ORDER-123" },
|
|
}),
|
|
);
|
|
|
|
// Mock PayPal capture
|
|
await page.route("**/api/paypal/capture", (route) =>
|
|
route.fulfill({
|
|
json: { success: true, transactionId: "TXN-123" },
|
|
}),
|
|
);
|
|
|
|
await page.goto("/checkout");
|
|
|
|
// Simulate PayPal approval callback
|
|
await page.evaluate(() => {
|
|
(window as any).onPayPalApprove?.({ orderID: "PAYPAL-ORDER-123" });
|
|
});
|
|
|
|
await expect(page.getByText("Order confirmed")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
### Payment Fixture
|
|
|
|
```typescript
|
|
// fixtures/payment.fixture.ts
|
|
type PaymentFixtures = {
|
|
mockStripe: (options?: { failPayment?: boolean }) => Promise<void>;
|
|
};
|
|
|
|
export const test = base.extend<PaymentFixtures>({
|
|
mockStripe: async ({ page }, use) => {
|
|
await use(async (options = {}) => {
|
|
await page.addInitScript(
|
|
([shouldFail]) => {
|
|
(window as any).Stripe = () => ({
|
|
elements: () => ({
|
|
create: () => ({
|
|
mount: () => {},
|
|
on: (event: string, handler: Function) => {
|
|
if (event === "ready") setTimeout(handler, 100);
|
|
},
|
|
destroy: () => {},
|
|
}),
|
|
}),
|
|
confirmCardPayment: async () => {
|
|
if (shouldFail) {
|
|
return { error: { message: "Card declined" } };
|
|
}
|
|
return { paymentIntent: { status: "succeeded" } };
|
|
},
|
|
});
|
|
},
|
|
[options.failPayment],
|
|
);
|
|
});
|
|
},
|
|
});
|
|
|
|
// Usage
|
|
test("handles declined card", async ({ page, mockStripe }) => {
|
|
await mockStripe({ failPayment: true });
|
|
|
|
await page.goto("/checkout");
|
|
await page.getByRole("button", { name: "Pay" }).click();
|
|
|
|
await expect(page.getByText("Card declined")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
## Email Verification
|
|
|
|
### Mock Email API
|
|
|
|
```typescript
|
|
test("email verification flow", async ({ page, request }) => {
|
|
let verificationToken: string;
|
|
|
|
// Capture the verification email
|
|
await page.route("**/api/send-verification", async (route) => {
|
|
const body = route.request().postDataJSON();
|
|
verificationToken = `mock-token-${Date.now()}`;
|
|
|
|
// Don't actually send email, just store token
|
|
route.fulfill({
|
|
json: { sent: true, messageId: "msg-123" },
|
|
});
|
|
});
|
|
|
|
// Mock token verification
|
|
await page.route("**/api/verify-email**", (route) => {
|
|
const url = new URL(route.request().url());
|
|
const token = url.searchParams.get("token");
|
|
|
|
if (token === verificationToken) {
|
|
route.fulfill({ json: { verified: true } });
|
|
} else {
|
|
route.fulfill({ status: 400, json: { error: "Invalid token" } });
|
|
}
|
|
});
|
|
|
|
await page.goto("/signup");
|
|
await page.getByLabel("Email").fill("test@example.com");
|
|
await page.getByRole("button", { name: "Sign Up" }).click();
|
|
|
|
await expect(page.getByText("Check your email")).toBeVisible();
|
|
|
|
// Simulate clicking email link
|
|
await page.goto(`/verify?token=${verificationToken}`);
|
|
|
|
await expect(page.getByText("Email verified")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
### Use Mailinator/Temp Mail
|
|
|
|
```typescript
|
|
// fixtures/email.fixture.ts
|
|
type EmailFixtures = {
|
|
getVerificationEmail: (inbox: string) => Promise<{ link: string }>;
|
|
};
|
|
|
|
export const test = base.extend<EmailFixtures>({
|
|
getVerificationEmail: async ({ request }, use) => {
|
|
await use(async (inbox) => {
|
|
// Poll Mailinator API for new email
|
|
const response = await request.get(
|
|
`https://api.mailinator.com/v2/domains/public/inboxes/${inbox}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.MAILINATOR_API_KEY}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
const messages = await response.json();
|
|
const latest = messages.msgs[0];
|
|
|
|
// Get full message
|
|
const msgResponse = await request.get(
|
|
`https://api.mailinator.com/v2/domains/public/inboxes/${inbox}/messages/${latest.id}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.MAILINATOR_API_KEY}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
const message = await msgResponse.json();
|
|
|
|
// Extract verification link from HTML
|
|
const linkMatch = message.parts[0].body.match(
|
|
/href="([^"]*verify[^"]*)"/,
|
|
);
|
|
return { link: linkMatch?.[1] || "" };
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
## SMS Verification
|
|
|
|
### Mock SMS API
|
|
|
|
```typescript
|
|
test("SMS verification", async ({ page }) => {
|
|
let smsCode: string;
|
|
|
|
// Capture SMS send
|
|
await page.route("**/api/send-sms", (route) => {
|
|
smsCode = Math.random().toString().slice(2, 8); // 6-digit code
|
|
|
|
route.fulfill({
|
|
json: { sent: true, messageId: "sms-123" },
|
|
});
|
|
});
|
|
|
|
// Mock code verification
|
|
await page.route("**/api/verify-sms", (route) => {
|
|
const body = route.request().postDataJSON();
|
|
|
|
if (body.code === smsCode) {
|
|
route.fulfill({ json: { verified: true } });
|
|
} else {
|
|
route.fulfill({ status: 400, json: { error: "Invalid code" } });
|
|
}
|
|
});
|
|
|
|
await page.goto("/verify-phone");
|
|
await page.getByLabel("Phone").fill("+1234567890");
|
|
await page.getByRole("button", { name: "Send Code" }).click();
|
|
|
|
// Enter the code
|
|
await page.getByLabel("Verification Code").fill(smsCode);
|
|
await page.getByRole("button", { name: "Verify" }).click();
|
|
|
|
await expect(page.getByText("Phone verified")).toBeVisible();
|
|
});
|
|
```
|
|
|
|
## Analytics & Tracking
|
|
|
|
### Block Analytics in Tests
|
|
|
|
```typescript
|
|
test.beforeEach(async ({ page }) => {
|
|
// Block all analytics/tracking
|
|
await page.route(
|
|
/google-analytics|googletagmanager|facebook|hotjar|segment|mixpanel|amplitude/,
|
|
(route) => route.abort(),
|
|
);
|
|
});
|
|
```
|
|
|
|
### Mock Analytics for Verification
|
|
|
|
```typescript
|
|
test("tracks purchase event", async ({ page }) => {
|
|
const analyticsEvents: any[] = [];
|
|
|
|
// Capture analytics calls
|
|
await page.route("**/api/analytics/**", (route) => {
|
|
analyticsEvents.push(route.request().postDataJSON());
|
|
route.fulfill({ status: 200 });
|
|
});
|
|
|
|
// Mock analytics SDK
|
|
await page.addInitScript(() => {
|
|
(window as any).analytics = {
|
|
track: (event: string, props: any) => {
|
|
fetch("/api/analytics/track", {
|
|
method: "POST",
|
|
body: JSON.stringify({ event, props }),
|
|
});
|
|
},
|
|
};
|
|
});
|
|
|
|
await page.goto("/checkout");
|
|
await page.getByRole("button", { name: "Complete Purchase" }).click();
|
|
|
|
// Verify analytics event was sent
|
|
expect(analyticsEvents).toContainEqual(
|
|
expect.objectContaining({
|
|
event: "Purchase Completed",
|
|
props: expect.objectContaining({ amount: expect.any(Number) }),
|
|
}),
|
|
);
|
|
});
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
| Anti-Pattern | Problem | Solution |
|
|
| ------------------------- | ------------------------------ | ----------------------- |
|
|
| Using real OAuth in tests | Slow, needs credentials, flaky | Mock OAuth endpoints |
|
|
| Real payment processing | Charges real money, slow | Use test mode or mock |
|
|
| Waiting for real emails | Very slow, unreliable | Mock email API |
|
|
| Not mocking analytics | Pollutes analytics data | Block or mock analytics |
|
|
|
|
## Related References
|
|
|
|
- **Network Mocking**: See [network-advanced.md](network-advanced.md) for route patterns
|
|
- **Authentication**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for auth patterns
|