Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
All notable changes to this project are documented here. This project follows
[Semantic Versioning](https://semver.org/).

## [1.1.1]

### Fixed

- **expectJsonSchema actually validates now.** It was calling the validator with
its arguments swapped, so schema checks silently passed for most bodies. It now
validates the response body against the schema.
- **Deep equality no longer reports false matches for Date / RegExp / Map / Set.**
`matches()` (used by `expectJson`, `expectBody`, `expectHeader`, `expectCookie`)
now delegates to the correct `isEqual`, so `expectJson(path, new Date(...))` and
similar comparisons are meaningful instead of always passing.
- **The JSON-schema validator rejects `NaN`** for `type: "number"` (and therefore
for `minimum`/`maximum`), since JSON has no `NaN`.

## [1.1.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "two-go",
"version": "1.1.0",
"version": "1.1.1",
"description": "Zero-dependency fluent service/API testing library: chainable HTTP requests, Jest-style + HTTP assertions, and a lodash-inspired utility belt. Works standalone or inside node:test, Jest, Vitest, and Mocha.",
"type": "module",
"main": "src/index.js",
Expand Down
33 changes: 8 additions & 25 deletions src/assertions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Core assertion primitives shared by GoResponse and RequestBuilder.
// These have no dependencies and are safe to use standalone.
// Deep equality is delegated to isEqual from the utility belt so there is a
// single, correct source of truth (it handles Date, RegExp, Map, Set, NaN).
import { isEqual } from "./utils/lang.js";

// Error thrown by every failing assertion. Carries the expected/actual values
// so test runners (and humans) can inspect the mismatch.
Expand Down Expand Up @@ -37,7 +39,7 @@ export function resolvePath(obj, path) {
// Flexible value matcher used by expectJson / expectHeader / expectBody.
// - RegExp: test against String(actual)
// - function: treat as predicate, truthy return means match
// - object/array: structural deepEqual
// - object/array: deep equality (Date/RegExp/Map/Set aware via isEqual)
// - primitive: strict ===
export function matches(actual, expected) {
if (expected instanceof RegExp) {
Expand All @@ -47,30 +49,11 @@ export function matches(actual, expected) {
return Boolean(expected(actual));
}
if (expected !== null && typeof expected === "object") {
return deepEqual(actual, expected);
return isEqual(actual, expected);
}
return actual === expected;
}

// Recursive structural equality. Compares own enumerable keys.
export function deepEqual(a, b) {
if (a === b) return true;

if (a === null || b === null) return false;
if (typeof a !== "object" || typeof b !== "object") return false;

const aIsArray = Array.isArray(a);
const bIsArray = Array.isArray(b);
if (aIsArray !== bIsArray) return false;

const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;

for (const key of aKeys) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!deepEqual(a[key], b[key])) return false;
}

return true;
}
// Deep structural equality. Kept as a named export for back-compat; it is the
// same correct implementation used everywhere else.
export { isEqual as deepEqual };
2 changes: 1 addition & 1 deletion src/http-assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ const methods = {

// Validate the parsed body against a schema, listing every error on failure.
expectJsonSchema(schema) {
const result = validate(schema, this.body);
const result = validate(this.body, schema);

// Support either a { valid, errors } result or a bare errors array.
let valid;
Expand Down
3 changes: 2 additions & 1 deletion src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
// Map of supported "type" keyword values to their predicate.
const TYPE_CHECKS = {
string: isString,
number: isNumber,
// JSON has no NaN/Infinity, so a "number" must be finite (rejects NaN).
number: (v) => isNumber(v) && Number.isFinite(v),
integer: isInteger,
boolean: isBoolean,
object: isPlainObject,
Expand Down
28 changes: 28 additions & 0 deletions test/unit/assertion-correctness.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Regression tests for the Tier 0 false-pass fixes.
import { test } from "node:test";
import assert from "node:assert/strict";
import { matches, deepEqual } from "../../src/assertions.js";
import { validate } from "../../src/schema.js";

test("matches no longer treats different Dates as equal", () => {
assert.equal(matches(new Date(0), new Date(1000)), false);
assert.equal(matches(new Date(1000), new Date(1000)), true);
});

test("matches distinguishes Maps, Sets, and RegExps", () => {
assert.equal(matches(new Map([["a", 1]]), new Map([["a", 2]])), false);
assert.equal(matches(new Set([1, 2]), new Set([1, 3])), false);
assert.equal(matches(/a/i, /b/i), false);
assert.equal(deepEqual(new Set([1, 2]), new Set([1, 2])), true);
});

test("plain object deep equality still works", () => {
assert.equal(matches({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] }), true);
assert.equal(matches({ a: 1 }, { a: 2 }), false);
});

test("schema validator rejects NaN for type number and range", () => {
assert.equal(validate(NaN, { type: "number" }).valid, false);
assert.equal(validate(NaN, { type: "number", minimum: 0, maximum: 10 }).valid, false);
assert.equal(validate(5, { type: "number", minimum: 0, maximum: 10 }).valid, true);
});
31 changes: 21 additions & 10 deletions test/unit/http-assertions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -238,18 +238,29 @@ test("expectValue returns an expect() wrapper for chaining matchers", () => {
);
});

test("expectJsonSchema passes when validation yields no errors", () => {
// validate(schema, body) is invoked internally; when the effective schema
// (the body) is not a plain object, validation short-circuits to valid.
const res = makeResponse({ body: "plain-text-body" });
assert.ok(res.expectJsonSchema({ type: "string" }));
test("expectJsonSchema passes when the body matches the schema", () => {
const res = makeResponse({ body: { id: 1, name: "Ada" } });
assert.ok(
res.expectJsonSchema({
type: "object",
required: ["id"],
properties: { id: { type: "integer" }, name: { type: "string" } },
})
);
});

test("expectJsonSchema fails and lists errors when validation fails", () => {
// Here the body acts as the effective schema and the passed argument as the
// value, so a non-matching pair produces validation errors.
const res = makeResponse({ body: { type: "number" } });
assertFails(() => res.expectJsonSchema("not-a-number"), "GET", "/x");
test("expectJsonSchema fails and lists errors when the body violates the schema", () => {
const res = makeResponse({ body: { id: "not-a-number" } });
assertFails(
() =>
res.expectJsonSchema({
type: "object",
required: ["id"],
properties: { id: { type: "integer" } },
}),
"GET",
"/x"
);
});

test("methods are installed as non-enumerable on the prototype and chain", () => {
Expand Down
Loading