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"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/core-shared": "workspace:*",
|
|
||||||
"@testing-library/jest-dom": "^6.5.0",
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.0",
|
"@testing-library/user-event": "^14.5.0",
|
||||||
@@ -43,6 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/core-eslint": "workspace:*",
|
"@repo/core-eslint": "workspace:*",
|
||||||
"@repo/core-typescript": "workspace:*",
|
"@repo/core-typescript": "workspace:*",
|
||||||
|
"@sentry/nextjs": "^10.51.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.0",
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
import type {
|
// Local type aliases matching the contracts in @repo/core-shared/instrumentation.
|
||||||
ILogger,
|
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
|
||||||
Breadcrumb,
|
type Breadcrumb = {
|
||||||
CaptureContext,
|
category: string;
|
||||||
} from "@repo/core-shared/instrumentation";
|
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 =
|
export type RecordedCapture =
|
||||||
| { kind: "exception"; err: unknown; ctx?: CaptureContext }
|
| { kind: "exception"; err: unknown; ctx?: CaptureContext }
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import type {
|
// Local type aliases matching the contracts in @repo/core-shared/instrumentation.
|
||||||
ITracer,
|
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
|
||||||
ISpan,
|
type AttributeValue = string | number | boolean | null;
|
||||||
SpanOpts,
|
|
||||||
AttributeValue,
|
type SpanOpts = {
|
||||||
} from "@repo/core-shared/instrumentation";
|
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 = {
|
export type RecordedSpan = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,6 +26,7 @@ export type RecordedSpan = {
|
|||||||
durationMs: number;
|
durationMs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Exported so callers can use it as a compatible ITracer via structural typing.
|
||||||
export class RecordingTracer implements ITracer {
|
export class RecordingTracer implements ITracer {
|
||||||
spans: RecordedSpan[] = [];
|
spans: RecordedSpan[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "./no-sentry";
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { afterEach } from "vitest";
|
import { afterEach } from "vitest";
|
||||||
import { cleanup } from "@testing-library/react";
|
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
|
// Reserved for future global node-env setup. Currently a no-op so that
|
||||||
// vitest configs may reference @repo/core-testing/setup/node uniformly.
|
// vitest configs may reference @repo/core-testing/setup/node uniformly.
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user