test: share oauth fuzz utilities

This commit is contained in:
Peter Steinberger
2026-04-18 22:49:43 +01:00
parent 8bfa06e992
commit f48c91ac2f
3 changed files with 37 additions and 75 deletions

View File

@@ -6,6 +6,7 @@ import {
normalizeAuthIdentityToken,
shouldMirrorRefreshedOAuthCredential,
} from "./oauth-identity.js";
import { makeSeededRandom, maybe, randomAsciiString as randomString } from "./oauth-test-utils.js";
import type { AuthProfileCredential } from "./types.js";
// Direct unit + fuzz tests for the cross-agent credential-mirroring identity
@@ -177,30 +178,6 @@ describe("isSameOAuthIdentity", () => {
// Fuzz tests. Seeded Mulberry32 so the run is reproducible.
// ---------------------------------------------------------------------------
function makeSeededRandom(seed: number): () => number {
let t = seed >>> 0;
return () => {
t = (t + 0x6d2b79f5) >>> 0;
let r = t;
r = Math.imul(r ^ (r >>> 15), r | 1);
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
function randomString(rng: () => number, maxLen: number): string {
const len = Math.floor(rng() * maxLen);
const chars: string[] = [];
for (let i = 0; i < len; i += 1) {
chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95))); // printable ASCII
}
return chars.join("");
}
function maybe<T>(rng: () => number, value: T): T | undefined {
return rng() < 0.5 ? value : undefined;
}
describe("isSafeToCopyOAuthIdentity (unified copy gate, used for mirror and adopt)", () => {
describe("positive matches", () => {
it("accepts matching accountIds", () => {
@@ -479,30 +456,6 @@ describe("shouldMirrorRefreshedOAuthCredential", () => {
});
describe("isSafeToCopyOAuthIdentity fuzz", () => {
function makeSeededRandom(seed: number): () => number {
let t = seed >>> 0;
return () => {
t = (t + 0x6d2b79f5) >>> 0;
let r = t;
r = Math.imul(r ^ (r >>> 15), r | 1);
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
function randomString(rng: () => number, maxLen: number): string {
const len = Math.floor(rng() * maxLen);
const chars: string[] = [];
for (let i = 0; i < len; i += 1) {
chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95)));
}
return chars.join("");
}
function maybe<T>(rng: () => number, value: T): T | undefined {
return rng() < 0.5 ? value : undefined;
}
it("is reflexive: share(a, a) is always true", () => {
const rng = makeSeededRandom(0x0172_0417);
for (let i = 0; i < 1000; i += 1) {

View File

@@ -1,4 +1,9 @@
import { describe, expect, it } from "vitest";
import {
makeSeededRandom,
randomAsciiString as randomJunk,
randomlyCased,
} from "./oauth-test-utils.js";
import { isRefreshTokenReusedError } from "./oauth.js";
// Direct tests for the refresh_token_reused classifier. This is the gate that
@@ -94,33 +99,6 @@ describe("isRefreshTokenReusedError", () => {
});
describe("fuzz: random noisy messages", () => {
function makeSeededRandom(seed: number): () => number {
let t = seed >>> 0;
return () => {
t = (t + 0x6d2b79f5) >>> 0;
let r = t;
r = Math.imul(r ^ (r >>> 15), r | 1);
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
function randomJunk(rng: () => number, maxLen: number): string {
const len = Math.floor(rng() * maxLen);
const chars: string[] = [];
for (let i = 0; i < len; i += 1) {
chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95)));
}
return chars.join("");
}
function randomlyCased(s: string, rng: () => number): string {
return s
.split("")
.map((c) => (rng() < 0.5 ? c.toUpperCase() : c.toLowerCase()))
.join("");
}
it("always detects the marker when embedded at random positions with noise", () => {
const rng = makeSeededRandom(0xabad1dea);
const markers = [

View File

@@ -52,3 +52,34 @@ export function createExpiredOauthStore(params: {
},
};
}
export function makeSeededRandom(seed: number): () => number {
let state = seed >>> 0;
return () => {
state = (state + 0x6d2b79f5) >>> 0;
let value = state;
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
}
export function randomAsciiString(rng: () => number, maxLen: number): string {
const len = Math.floor(rng() * maxLen);
const chars: string[] = [];
for (let index = 0; index < len; index += 1) {
chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95)));
}
return chars.join("");
}
export function maybe<T>(rng: () => number, value: T): T | undefined {
return rng() < 0.5 ? value : undefined;
}
export function randomlyCased(value: string, rng: () => number): string {
return value
.split("")
.map((char) => (rng() < 0.5 ? char.toUpperCase() : char.toLowerCase()))
.join("");
}