mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail.
160 lines
7.1 KiB
TypeScript
160 lines
7.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
findOcrEncodedInputViolation,
|
|
OCR_MAX_ENCODED_INPUT_BYTES,
|
|
ocrUploadErrorStatus,
|
|
resolveOcrUploadLimits,
|
|
} from "../../../apps/api/src/lib/ocr-limits.js";
|
|
|
|
// Mutation-hardening tests for ocr-limits.ts. Each case pins the exact verdict on
|
|
// both sides of a comparison so the boundary (`<` vs `<=`, `>` vs `>=`), logical
|
|
// (`&&` vs `||`), and condition-forcing mutants each become observable.
|
|
|
|
const MIB = 1024 * 1024;
|
|
|
|
describe("findOcrEncodedInputViolation per-file boundary (line 43)", () => {
|
|
it("returns null for a file exactly at the per-file limit", () => {
|
|
// `bytes > fileBytes` is false at equality. The `>=` mutant would flip this
|
|
// to a file violation, and the whole-condition `false` mutant would let the
|
|
// loop fall through and (here) still return null - so this alone does not
|
|
// separate `false`; the over-limit case below does.
|
|
const { fileBytes } = resolveOcrUploadLimits(100);
|
|
expect(fileBytes).toBe(100 * MIB);
|
|
expect(findOcrEncodedInputViolation([fileBytes], 100)).toBeNull();
|
|
});
|
|
|
|
it("flags a file one byte over the per-file limit as a file violation", () => {
|
|
// Original: file violation. Kills the whole-condition `false` mutant (which
|
|
// would let 101 MiB fall through to a 101 MiB aggregate, under the 512 MiB
|
|
// ceiling, and return null) and the `>=`/`>` boundary mutants.
|
|
const overLimit = 100 * MIB + 1;
|
|
expect(findOcrEncodedInputViolation([overLimit], 100)).toEqual({
|
|
scope: "file",
|
|
limitBytes: 100 * MIB,
|
|
});
|
|
});
|
|
|
|
it("treats zero bytes as a valid file (not a violation)", () => {
|
|
// `bytes < 0` is false at zero. The `bytes <= 0` mutant would classify a
|
|
// 0-byte input as a file violation; the original returns null.
|
|
expect(findOcrEncodedInputViolation([0], 100)).toBeNull();
|
|
});
|
|
|
|
it("flags a negative byte count as a file violation via the bytes < 0 arm", () => {
|
|
// -1 is a safe integer and its magnitude is under the limit, so only the
|
|
// `bytes < 0` sub-clause triggers. Kills the sub-clause `false` mutant
|
|
// (which would return null) and the `<`/`<=` mutant is separated by the
|
|
// zero case above.
|
|
expect(findOcrEncodedInputViolation([-1], 100)).toEqual({
|
|
scope: "file",
|
|
limitBytes: 100 * MIB,
|
|
});
|
|
});
|
|
|
|
it("flags a non-safe-integer (fractional) size as a file violation", () => {
|
|
// 1.5 is positive, small, but not a safe integer, so only the
|
|
// `!Number.isSafeInteger(bytes)` arm triggers. Kills the LogicalOperator
|
|
// mutants that turn the leading `||` into `&&` (which would require
|
|
// bytes < 0 too and thus miss 1.5).
|
|
expect(findOcrEncodedInputViolation([1.5], 100)).toEqual({
|
|
scope: "file",
|
|
limitBytes: 100 * MIB,
|
|
});
|
|
});
|
|
|
|
it("flags a non-safe huge integer (above MAX_SAFE_INTEGER) as a file violation", () => {
|
|
expect(findOcrEncodedInputViolation([Number.MAX_SAFE_INTEGER + 2], 100)).toEqual({
|
|
scope: "file",
|
|
limitBytes: 100 * MIB,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("findOcrEncodedInputViolation aggregate boundary (line 47)", () => {
|
|
it("returns null when the running total exactly equals the aggregate ceiling", () => {
|
|
// With maxUploadSizeMb=0 the per-file limit equals the hard ceiling, so a
|
|
// single 512 MiB input passes the per-file check and lands the aggregate
|
|
// exactly at the ceiling. `aggregateBytes > ceiling` is false at equality;
|
|
// the `>=` mutant would report an aggregate violation here.
|
|
const { fileBytes, aggregateBytes } = resolveOcrUploadLimits(0);
|
|
expect(fileBytes).toBe(OCR_MAX_ENCODED_INPUT_BYTES);
|
|
expect(aggregateBytes).toBe(OCR_MAX_ENCODED_INPUT_BYTES);
|
|
expect(findOcrEncodedInputViolation([OCR_MAX_ENCODED_INPUT_BYTES], 0)).toBeNull();
|
|
});
|
|
|
|
it("flags an aggregate one byte over the ceiling", () => {
|
|
// Two files that each pass the per-file check but sum to ceiling + 1.
|
|
const half = OCR_MAX_ENCODED_INPUT_BYTES / 2;
|
|
const inputs = [half, half + 1];
|
|
expect(findOcrEncodedInputViolation(inputs, 0)).toEqual({
|
|
scope: "aggregate",
|
|
limitBytes: OCR_MAX_ENCODED_INPUT_BYTES,
|
|
});
|
|
});
|
|
|
|
it("returns null for several inputs whose sum stays under the aggregate ceiling", () => {
|
|
expect(findOcrEncodedInputViolation([60 * MIB, 60 * MIB, 60 * MIB], 100)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("ocrUploadErrorStatus 503 branch (lines 57-58)", () => {
|
|
it("returns 503 only for an object carrying statusCode === 503", () => {
|
|
expect(ocrUploadErrorStatus({ statusCode: 503 })).toBe(503);
|
|
});
|
|
|
|
it("does not return 503 for an object with a different statusCode", () => {
|
|
// Kills the whole-condition `true` mutant on the 503 block (which would make
|
|
// every input 503) and the equality/logical mutants on that block.
|
|
expect(ocrUploadErrorStatus({ statusCode: 500 })).toBe(400);
|
|
});
|
|
|
|
it("does not return 503 for null", () => {
|
|
// `error !== null` guards a null deref. Forcing that sub-condition true (or
|
|
// dropping the &&) would try `null.statusCode` / `"in" null` and throw or
|
|
// misroute; the original cleanly returns 400.
|
|
expect(ocrUploadErrorStatus(null)).toBe(400);
|
|
expect(ocrUploadErrorStatus(undefined)).toBe(400);
|
|
});
|
|
|
|
it("does not return 503 for a non-object (string) error", () => {
|
|
// A string has no statusCode. Any logical mutant that reorders the guards so
|
|
// `"statusCode" in error` runs against a string throws a TypeError; the
|
|
// original returns 400. Asserting exactly 400 kills those.
|
|
expect(ocrUploadErrorStatus("some string error")).toBe(400);
|
|
expect(ocrUploadErrorStatus(42)).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe("ocrUploadErrorStatus 413 branch (lines 65-68)", () => {
|
|
it("returns 413 for an object with statusCode === 413", () => {
|
|
expect(ocrUploadErrorStatus({ statusCode: 413 })).toBe(413);
|
|
});
|
|
|
|
it("returns 413 for an Error whose message matches the size-exceeded pattern", () => {
|
|
expect(ocrUploadErrorStatus(new Error("File too large"))).toBe(413);
|
|
expect(ocrUploadErrorStatus(new Error("Upload exceeds the maximum allowed size"))).toBe(413);
|
|
expect(ocrUploadErrorStatus(new Error("upload exceeds the configured limit"))).toBe(413);
|
|
});
|
|
|
|
it("returns 400 for an object whose statusCode is neither 413 nor 503", () => {
|
|
// Object path, no regex match: kills the `error.statusCode === 413`
|
|
// condition-forcing mutant (which would report 413 for any object).
|
|
expect(ocrUploadErrorStatus({ statusCode: 418 })).toBe(400);
|
|
});
|
|
|
|
it("returns 400 for an Error whose message does not match the pattern", () => {
|
|
// Error path, no size keyword: kills the regex-branch `true` mutant.
|
|
expect(ocrUploadErrorStatus(new Error("malformed multipart body"))).toBe(400);
|
|
expect(ocrUploadErrorStatus(new Error("upload exceeds"))).toBe(400);
|
|
});
|
|
|
|
it("returns 400 for a non-Error, non-object value with no status", () => {
|
|
expect(ocrUploadErrorStatus("plain string")).toBe(400);
|
|
});
|
|
|
|
it("prefers 503 over 413 when both would nominally apply", () => {
|
|
// A 503 object never reaches the 413 block; confirms ordering.
|
|
expect(ocrUploadErrorStatus({ statusCode: 503, message: "file too large" })).toBe(503);
|
|
});
|
|
});
|