feat(core-testing): R49 guard — block real Sentry SDK init in test processes

This commit is contained in:
2026-05-07 00:11:22 +02:00
parent 05e7bf6c57
commit 64b6eb79d4
7 changed files with 89 additions and 12 deletions

View File

@@ -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",

View File

@@ -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 }

View File

@@ -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[] = [];

View File

@@ -1,3 +1,4 @@
import "./no-sentry";
import "@testing-library/jest-dom/vitest";
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";

View 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();
});
});

View 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 })),
}));

View File

@@ -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 {};