feat(core-testing): R49 guard — block real Sentry SDK init in test processes
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.0",
|
||||
@@ -43,6 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@sentry/nextjs": "^10.51.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import type {
|
||||
ILogger,
|
||||
Breadcrumb,
|
||||
CaptureContext,
|
||||
} from "@repo/core-shared/instrumentation";
|
||||
// Local type aliases matching the contracts in @repo/core-shared/instrumentation.
|
||||
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
|
||||
type Breadcrumb = {
|
||||
category: string;
|
||||
message: string;
|
||||
level?: "info" | "warning" | "error";
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type CaptureContext = {
|
||||
tags?: Record<string, string>;
|
||||
extras?: Record<string, unknown>;
|
||||
fingerprint?: string[];
|
||||
};
|
||||
|
||||
interface ILogger {
|
||||
captureException(err: unknown, ctx?: CaptureContext): void;
|
||||
captureMessage(msg: string, level?: "info" | "warning" | "error", ctx?: CaptureContext): void;
|
||||
addBreadcrumb(b: Breadcrumb): void;
|
||||
setUser(user: { id: string } | null): void;
|
||||
}
|
||||
|
||||
export type RecordedCapture =
|
||||
| { kind: "exception"; err: unknown; ctx?: CaptureContext }
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import type {
|
||||
ITracer,
|
||||
ISpan,
|
||||
SpanOpts,
|
||||
AttributeValue,
|
||||
} from "@repo/core-shared/instrumentation";
|
||||
// Local type aliases matching the contracts in @repo/core-shared/instrumentation.
|
||||
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
|
||||
type AttributeValue = string | number | boolean | null;
|
||||
|
||||
type SpanOpts = {
|
||||
name: string;
|
||||
op?: string;
|
||||
attributes?: Record<string, AttributeValue>;
|
||||
};
|
||||
|
||||
interface ISpan {
|
||||
setAttribute(key: string, value: AttributeValue): void;
|
||||
setStatus(status: "ok" | "error", message?: string): void;
|
||||
}
|
||||
|
||||
interface ITracer {
|
||||
startSpan<T>(opts: SpanOpts, fn: (span: ISpan) => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export type RecordedSpan = {
|
||||
name: string;
|
||||
@@ -14,6 +26,7 @@ export type RecordedSpan = {
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
// Exported so callers can use it as a compatible ITracer via structural typing.
|
||||
export class RecordingTracer implements ITracer {
|
||||
spans: RecordedSpan[] = [];
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./no-sentry";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
|
||||
16
packages/core-testing/src/setup/no-sentry.test.ts
Normal file
16
packages/core-testing/src/setup/no-sentry.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
describe("setup/no-sentry guard (R49)", () => {
|
||||
it("Sentry.init is a vi.fn (mocked, not real)", () => {
|
||||
expect(vi.isMockFunction(Sentry.init)).toBe(true);
|
||||
});
|
||||
|
||||
it("Sentry.captureException is a vi.fn", () => {
|
||||
expect(vi.isMockFunction(Sentry.captureException)).toBe(true);
|
||||
});
|
||||
|
||||
it("calling Sentry.init does not throw or initialize", () => {
|
||||
expect(() => Sentry.init({ dsn: "https://x@y/1" } as Parameters<typeof Sentry.init>[0])).not.toThrow();
|
||||
});
|
||||
});
|
||||
29
packages/core-testing/src/setup/no-sentry.ts
Normal file
29
packages/core-testing/src/setup/no-sentry.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
/**
|
||||
* R49 — guard against real Sentry SDK initialization in test processes.
|
||||
*
|
||||
* Mocks @sentry/nextjs at the module level so any code that imports it
|
||||
* receives a no-op surface. Tests that need to assert Sentry behavior
|
||||
* still use vi.mock locally with their own implementation; this guard
|
||||
* just ensures *unintentional* imports don't cause real network/init.
|
||||
*/
|
||||
vi.mock("@sentry/nextjs", () => ({
|
||||
init: vi.fn(),
|
||||
startSpan: vi.fn((_opts: unknown, fn: (span: unknown) => unknown) =>
|
||||
fn({ setAttribute: vi.fn(), setStatus: vi.fn() }),
|
||||
),
|
||||
captureException: vi.fn(),
|
||||
captureMessage: vi.fn(),
|
||||
addBreadcrumb: vi.fn(),
|
||||
setUser: vi.fn(),
|
||||
setContext: vi.fn(),
|
||||
setTag: vi.fn(),
|
||||
setExtra: vi.fn(),
|
||||
withScope: vi.fn((fn: (scope: unknown) => unknown) =>
|
||||
fn({ setTag: vi.fn(), setExtra: vi.fn() }),
|
||||
),
|
||||
replayIntegration: vi.fn(() => ({ name: "Replay" })),
|
||||
getActiveSpan: vi.fn(() => undefined),
|
||||
getCurrentHub: vi.fn(() => ({ getClient: () => undefined })),
|
||||
}));
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./no-sentry";
|
||||
|
||||
// Reserved for future global node-env setup. Currently a no-op so that
|
||||
// vitest configs may reference @repo/core-testing/setup/node uniformly.
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user