push-worker.test.mjs (22934B)
1 import test from "node:test"; 2 import assert from "node:assert/strict"; 3 import { createHmac } from "node:crypto"; 4 import { PushRegistry } from "../../Workers/push-worker.js"; 5 import { StubStorage } from "./helpers.mjs"; 6 7 function makeRegistry(env = {}) { 8 const storage = new StubStorage(); 9 return { registry: new PushRegistry({ storage }, env), storage }; 10 } 11 12 function attestRequest(ip) { 13 return new Request("https://push.example/attest/register", { 14 method: "POST", 15 headers: { "CF-Connecting-IP": ip } 16 }); 17 } 18 19 const appAttestKeyBytes = Buffer.from([ 20 0xfb, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 21 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 22 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 23 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d 24 ]); 25 const standardAppAttestKeyID = appAttestKeyBytes.toString("base64"); 26 const urlSafeAppAttestKeyID = appAttestKeyBytes.toString("base64url"); 27 28 function challengeRequest(keyID, deviceID = "device-1") { 29 return new Request("https://push.example/attest/challenge", { 30 method: "POST", 31 headers: { 32 "Content-Type": "application/json", 33 "CF-Connecting-IP": "203.0.113.9" 34 }, 35 body: JSON.stringify({ deviceID, keyID }) 36 }); 37 } 38 39 test("challenge accepts standard and URL-safe App Attest key IDs", async () => { 40 assert.match(standardAppAttestKeyID, /[+/]/); 41 assert.match(standardAppAttestKeyID, /=+$/); 42 const standard = makeRegistry(); 43 const standardResponse = await standard.registry.fetch(challengeRequest(standardAppAttestKeyID)); 44 assert.equal(standardResponse.status, 200); 45 46 const urlSafe = makeRegistry(); 47 const urlSafeResponse = await urlSafe.registry.fetch(challengeRequest(urlSafeAppAttestKeyID)); 48 assert.equal(urlSafeResponse.status, 200); 49 }); 50 51 test("registration accepts both App Attest key-ID alphabets before attestation verification", async () => { 52 for (const keyID of [standardAppAttestKeyID, urlSafeAppAttestKeyID]) { 53 const { registry } = makeRegistry(); 54 const response = await registry.handleAttestationRegister( 55 attestRequest("203.0.113.9"), 56 JSON.stringify({ 57 deviceID: "device-1", 58 keyID, 59 challenge: "unknown-challenge", 60 attestationObject: "AA" 61 }) 62 ); 63 assert.equal(response.status, 401); 64 assert.equal(await response.text(), "Unknown App Attest challenge"); 65 } 66 }); 67 68 test("App Attest key-ID storage uses one URL-safe canonical component", () => { 69 const { registry } = makeRegistry(); 70 const expected = `appattest-key:device-1:${urlSafeAppAttestKeyID}`; 71 assert.equal(registry.appAttestRegistrationKey("device-1", standardAppAttestKeyID), expected); 72 assert.equal(registry.appAttestRegistrationKey("device-1", urlSafeAppAttestKeyID), expected); 73 }); 74 75 test("registration retry succeeds after its first response was lost", async () => { 76 const { registry, storage } = makeRegistry(); 77 const registrationKey = registry.appAttestRegistrationKey("device-1", standardAppAttestKeyID); 78 await storage.put(registrationKey, { publicKeySPKI: "already-verified" }); 79 80 const response = await registry.handleAttestationRegister( 81 attestRequest("203.0.113.9"), 82 JSON.stringify({ 83 deviceID: "device-1", 84 keyID: standardAppAttestKeyID, 85 challenge: "already-consumed", 86 attestationObject: "AA" 87 }) 88 ); 89 90 assert.equal(response.status, 204); 91 }); 92 93 test("pre-885 registration storage migrates to the canonical key on lookup", async () => { 94 const { registry, storage } = makeRegistry(); 95 const legacyKey = `appattest-key:device-1:${standardAppAttestKeyID}`; 96 const canonicalKey = `appattest-key:device-1:${urlSafeAppAttestKeyID}`; 97 const registration = { publicKeySPKI: "legacy-record" }; 98 await storage.put(legacyKey, registration); 99 100 const loaded = await registry.loadAppAttestRegistration( 101 "device-1", 102 standardAppAttestKeyID, 103 urlSafeAppAttestKeyID 104 ); 105 106 assert.deepEqual(loaded, registration); 107 assert.equal(storage.map.has(legacyKey), false); 108 assert.deepEqual(await storage.get(canonicalKey), registration); 109 }); 110 111 test("challenge rejects empty, oversized, malformed, and storage-smuggling key IDs", async () => { 112 const invalidKeyIDs = [ 113 "", 114 "A".repeat(257), 115 "not:base64", 116 "+_8=", 117 "AA=A", 118 "A", 119 `${standardAppAttestKeyID}:forged` 120 ]; 121 for (const keyID of invalidKeyIDs) { 122 const { registry } = makeRegistry(); 123 const response = await registry.fetch(challengeRequest(keyID)); 124 assert.equal(response.status, 400, `expected rejection for ${JSON.stringify(keyID.slice(0, 20))}`); 125 } 126 }); 127 128 test("rate limit config uses table defaults", () => { 129 const { registry } = makeRegistry(); 130 assert.deepEqual(registry.rateLimitConfig("PUBLISH_CRED"), { limit: 60, windowSeconds: 60 }); 131 assert.deepEqual(registry.rateLimitConfig("APP_ATTEST_REGISTER_IP"), { limit: 30, windowSeconds: 60 * 60 }); 132 }); 133 134 test("rate limit config honours env overrides and rejects junk", () => { 135 const { registry } = makeRegistry({ 136 PUBLISH_CRED_LIMIT: "5", 137 PUBLISH_CRED_WINDOW_SECONDS: "120", 138 PUBLISH_ADDRESS_LIMIT: "not-a-number" 139 }); 140 assert.deepEqual(registry.rateLimitConfig("PUBLISH_CRED"), { limit: 5, windowSeconds: 120 }); 141 // A malformed override falls back to the table default rather than NaN. 142 assert.equal(registry.rateLimitConfig("PUBLISH_ADDRESS").limit, 30); 143 }); 144 145 test("requests beyond the limit are rejected with a retry hint", async () => { 146 const { registry } = makeRegistry(); 147 const config = registry.rateLimitConfig("PUBLISH_CRED"); 148 for (let i = 0; i < config.limit; i++) { 149 const result = await registry.checkRateLimit("publish:cred", "cred-a", config); 150 assert.equal(result.ok, true, `call ${i + 1} should be allowed`); 151 } 152 const over = await registry.checkRateLimit("publish:cred", "cred-a", config); 153 assert.equal(over.ok, false); 154 assert.ok(over.retryAfterSeconds >= 1); 155 }); 156 157 test("the window slides: aged entries free quota again", async () => { 158 const { registry, storage } = makeRegistry({ PUBLISH_CRED_LIMIT: "2" }); 159 const config = registry.rateLimitConfig("PUBLISH_CRED"); 160 await registry.checkRateLimit("publish:cred", "cred-a", config); 161 await registry.checkRateLimit("publish:cred", "cred-a", config); 162 assert.equal((await registry.checkRateLimit("publish:cred", "cred-a", config)).ok, false); 163 164 storage.age(storage.keys("rate:")[0], config.windowSeconds * 1000 + 1000); 165 assert.equal((await registry.checkRateLimit("publish:cred", "cred-a", config)).ok, true); 166 }); 167 168 test("identities have independent buckets", async () => { 169 const { registry } = makeRegistry({ PUBLISH_CRED_LIMIT: "1" }); 170 const config = registry.rateLimitConfig("PUBLISH_CRED"); 171 assert.equal((await registry.checkRateLimit("publish:cred", "cred-a", config)).ok, true); 172 assert.equal((await registry.checkRateLimit("publish:cred", "cred-a", config)).ok, false); 173 assert.equal((await registry.checkRateLimit("publish:cred", "cred-b", config)).ok, true); 174 }); 175 176 test("attestation gate returns a 429 response once the device limit is hit", async () => { 177 const { registry } = makeRegistry({ APP_ATTEST_REGISTER_DEVICE_LIMIT: "2" }); 178 const request = attestRequest("203.0.113.9"); 179 assert.equal(await registry.checkAttestationRateLimit(request, "register", "device-1"), null); 180 assert.equal(await registry.checkAttestationRateLimit(request, "register", "device-1"), null); 181 const limited = await registry.checkAttestationRateLimit(request, "register", "device-1"); 182 assert.equal(limited.status, 429); 183 assert.ok(Number(limited.headers.get("Retry-After")) >= 1); 184 }); 185 186 test("first write arms the sweep alarm at the horizon; later writes leave it", async () => { 187 const { registry, storage } = makeRegistry(); 188 const config = registry.rateLimitConfig("PUBLISH_CRED"); 189 const before = Date.now(); 190 await registry.checkRateLimit("publish:cred", "cred-a", config); 191 const horizon = registry.rateSweepHorizonMillis(); 192 // The default horizon is the largest table window: the 1h attest buckets. 193 assert.equal(horizon, 60 * 60 * 1000); 194 assert.ok(storage.alarmAt >= before + horizon); 195 196 const armedAt = storage.alarmAt; 197 await registry.checkRateLimit("publish:cred", "cred-b", config); 198 assert.equal(storage.alarmAt, armedAt, "arm-if-unarmed must not reschedule"); 199 }); 200 201 test("an env window override raises the sweep horizon", () => { 202 const { registry } = makeRegistry({ APP_ATTEST_REGISTER_IP_WINDOW_SECONDS: "7200" }); 203 assert.equal(registry.rateSweepHorizonMillis(), 7200 * 1000); 204 }); 205 206 test("sweep prunes only stale rate keys and re-arms while live ones remain", async () => { 207 const { registry, storage } = makeRegistry(); 208 const config = registry.rateLimitConfig("PUBLISH_CRED"); 209 await registry.checkRateLimit("publish:cred", "cred-a", config); 210 await registry.checkRateLimit("publish:cred", "cred-b", config); 211 const [staleKey, freshKey] = storage.keys("rate:"); 212 storage.age(staleKey, registry.rateSweepHorizonMillis() + 60 * 1000); 213 await storage.put("gamecred:keep-me", { secret: "s" }); 214 215 await registry.alarm(); 216 assert.equal(storage.map.has(staleKey), false); 217 assert.equal(storage.map.has(freshKey), true); 218 assert.equal(storage.map.has("gamecred:keep-me"), true, "sweep must stay inside the rate: prefix"); 219 assert.ok(storage.alarmAt > Date.now(), "alarm re-arms while live keys remain"); 220 }); 221 222 test("sweep goes quiet once every rate key has expired", async () => { 223 const { registry, storage } = makeRegistry(); 224 const config = registry.rateLimitConfig("PUBLISH_CRED"); 225 await registry.checkRateLimit("publish:cred", "cred-a", config); 226 storage.age(storage.keys("rate:")[0], registry.rateSweepHorizonMillis() + 60 * 1000); 227 storage.alarmAt = null; 228 229 await registry.alarm(); 230 assert.deepEqual(storage.keys("rate:"), []); 231 assert.equal(storage.alarmAt, null, "no live keys, no re-arm"); 232 }); 233 234 // --- Game-binding registration (H4): a credID-scoped address registration 235 // must prove possession of that game's secret, exactly like a publish. 236 237 const gameSecret = Buffer.alloc(32, 7).toString("base64url"); 238 const otherSecret = Buffer.alloc(32, 9).toString("base64url"); 239 240 function gameSignature(secret, credID, { bodyHash = "hash", timestamp = "1000", nonce = "nonce" } = {}) { 241 const payload = ["crossmate-push-game-v1", credID, bodyHash, timestamp, nonce].join("\n"); 242 return createHmac("sha256", Buffer.from(secret, "base64url")).update(payload).digest("base64url"); 243 } 244 245 function registerRequest(signature) { 246 const headers = { 247 "X-Crossmate-Body-SHA256": "hash", 248 "X-Crossmate-Timestamp": "1000", 249 "X-Crossmate-Nonce": "nonce" 250 }; 251 if (signature) headers["X-Crossmate-Game-Signature"] = signature; 252 return new Request("https://push.example/register", { method: "POST", headers }); 253 } 254 255 function registerBody(addresses, deviceID = "device1") { 256 return JSON.stringify({ 257 deviceID, 258 token: "apns-token", 259 environment: "production", 260 addresses 261 }); 262 } 263 264 async function registerGameCred(registry, credID, secret = gameSecret) { 265 const response = await registry.handleGameRegister(credID, JSON.stringify({ secret })); 266 assert.ok(response.status === 201 || response.status === 204); 267 } 268 269 test("a correctly signed game binding registers under its credID", async () => { 270 const { registry, storage } = makeRegistry(); 271 await registerGameCred(registry, "cred-1"); 272 273 const response = await registry.handleRegister( 274 registerRequest(gameSignature(gameSecret, "cred-1")), 275 registerBody([{ address: "addr-a", credID: "cred-1" }]), 276 { deviceID: "device1" } 277 ); 278 279 assert.equal(response.status, 204); 280 assert.deepEqual(storage.keys("addr:"), ["addr:cred-1:addr-a:device1"]); 281 }); 282 283 test("an App-Attest-only game binding (no game signature) is not stored", async () => { 284 const { registry, storage } = makeRegistry(); 285 await registerGameCred(registry, "cred-1"); 286 287 // A departed participant (or anyone who learned the credID) has a valid 288 // App Attest enrollment of their own but not the game secret. 289 const response = await registry.handleRegister( 290 registerRequest(null), 291 registerBody([ 292 { address: "addr-account" }, 293 { address: "addr-a", credID: "cred-1" } 294 ]), 295 { deviceID: "device1" } 296 ); 297 298 // The account-scoped binding still lands (legacy batched clients), but the 299 // unproven game binding is dropped. 300 assert.equal(response.status, 204); 301 assert.deepEqual(storage.keys("addr:"), ["addr:addr-account:device1"]); 302 }); 303 304 test("a game binding signed with the wrong secret is not stored", async () => { 305 const { registry, storage } = makeRegistry(); 306 await registerGameCred(registry, "cred-1"); 307 308 const response = await registry.handleRegister( 309 registerRequest(gameSignature(otherSecret, "cred-1")), 310 registerBody([{ address: "addr-a", credID: "cred-1" }]), 311 { deviceID: "device1" } 312 ); 313 314 assert.equal(response.status, 204); 315 assert.deepEqual(storage.keys("addr:"), []); 316 }); 317 318 test("a game binding naming an unknown credID is not stored", async () => { 319 const { registry, storage } = makeRegistry(); 320 321 const response = await registry.handleRegister( 322 registerRequest(gameSignature(gameSecret, "cred-unregistered")), 323 registerBody([{ address: "addr-a", credID: "cred-unregistered" }]), 324 { deviceID: "device1" } 325 ); 326 327 assert.equal(response.status, 204); 328 assert.deepEqual(storage.keys("addr:"), []); 329 }); 330 331 test("mixed credIDs in one request drop every game binding", async () => { 332 const { registry, storage } = makeRegistry(); 333 await registerGameCred(registry, "cred-1"); 334 await registerGameCred(registry, "cred-2", otherSecret); 335 336 // One request carries one signature, so it can prove at most one credID; 337 // a batch that names two proves neither. 338 const response = await registry.handleRegister( 339 registerRequest(gameSignature(gameSecret, "cred-1")), 340 registerBody([ 341 { address: "addr-a", credID: "cred-1" }, 342 { address: "addr-b", credID: "cred-2" } 343 ]), 344 { deviceID: "device1" } 345 ); 346 347 assert.equal(response.status, 204); 348 assert.deepEqual(storage.keys("addr:"), []); 349 }); 350 351 test("after rotation, the old credential cannot bind under the new credID", async () => { 352 const { registry, storage } = makeRegistry(); 353 // The room before rotation… 354 await registerGameCred(registry, "cred-old"); 355 // …and the replacement a remaining participant registered after someone left. 356 await registerGameCred(registry, "cred-new", otherSecret); 357 358 // The departed participant holds the complete old credential but signs for 359 // the new credID with the old secret — the only credential it has. 360 const response = await registry.handleRegister( 361 registerRequest(gameSignature(gameSecret, "cred-new")), 362 registerBody([{ address: "addr-a", credID: "cred-new" }], "device-departed"), 363 { deviceID: "device-departed" } 364 ); 365 assert.equal(response.status, 204); 366 assert.deepEqual(storage.keys("addr:"), [], "old secret must not subscribe to the rotated room"); 367 368 // A current participant holding the new secret registers fine. 369 const ok = await registry.handleRegister( 370 registerRequest(gameSignature(otherSecret, "cred-new")), 371 registerBody([{ address: "addr-a", credID: "cred-new" }], "device-current"), 372 { deviceID: "device-current" } 373 ); 374 assert.equal(ok.status, 204); 375 assert.deepEqual(storage.keys("addr:"), ["addr:cred-new:addr-a:device-current"]); 376 }); 377 378 // --- Ingress and fanout bounds (M3): request bodies, lists, strings, and 379 // APNs payloads are all refused before the work they would otherwise buy. 380 381 test("an oversized request body is refused before auth", async () => { 382 const { registry } = makeRegistry({ MAX_BODY_BYTES: "1024" }); 383 const response = await registry.fetch(new Request("https://push.example/publish", { 384 method: "POST", 385 body: "x".repeat(2048) 386 })); 387 assert.equal(response.status, 413); 388 }); 389 390 test("a body exactly at the limit passes the size gate", async () => { 391 const { registry } = makeRegistry({ MAX_BODY_BYTES: "1024" }); 392 const response = await registry.fetch(new Request("https://push.example/publish", { 393 method: "POST", 394 body: "x".repeat(1024) 395 })); 396 // Past the size gate; refused by auth, not by size. 397 assert.equal(response.status, 401); 398 }); 399 400 test("the body cap also covers the pre-auth attestation routes", async () => { 401 const { registry } = makeRegistry({ MAX_BODY_BYTES: "1024" }); 402 const response = await registry.fetch(new Request("https://push.example/attest/challenge", { 403 method: "POST", 404 body: "x".repeat(2048) 405 })); 406 assert.equal(response.status, 413); 407 }); 408 409 test("register refuses oversized address and mutedKinds lists", async () => { 410 const { registry } = makeRegistry({ MAX_ADDRESS_COUNT: "2" }); 411 const tooMany = await registry.handleRegister( 412 registerRequest(null), 413 registerBody([{ address: "a1" }, { address: "a2" }, { address: "a3" }]), 414 { deviceID: "device1" } 415 ); 416 assert.equal(tooMany.status, 400); 417 418 const mutedOverflow = await registry.handleRegister( 419 registerRequest(null), 420 JSON.stringify({ 421 deviceID: "device1", 422 token: "apns-token", 423 environment: "production", 424 addresses: [{ address: "a1" }], 425 mutedKinds: Array.from({ length: 33 }, (_, i) => `kind${i}`) 426 }), 427 { deviceID: "device1" } 428 ); 429 assert.equal(mutedOverflow.status, 400); 430 }); 431 432 test("register refuses a malformed token and skips malformed addresses", async () => { 433 const { registry, storage } = makeRegistry(); 434 const badToken = await registry.handleRegister( 435 registerRequest(null), 436 JSON.stringify({ 437 deviceID: "device1", 438 token: "x".repeat(200), 439 environment: "production", 440 addresses: [{ address: "a1" }] 441 }), 442 { deviceID: "device1" } 443 ); 444 assert.equal(badToken.status, 400); 445 446 // An address that could forge a `:`-separated storage-key segment is 447 // dropped; the well-formed sibling in the same request still registers. 448 const mixed = await registry.handleRegister( 449 registerRequest(null), 450 registerBody([{ address: "addr:forged" }, { address: "addr-ok" }]), 451 { deviceID: "device1" } 452 ); 453 assert.equal(mixed.status, 204); 454 assert.deepEqual(storage.keys("addr:"), ["addr:addr-ok:device1"]); 455 }); 456 457 test("a game's registration set is capped at write time", async () => { 458 const { registry, storage } = makeRegistry({ MAX_REGISTRATIONS_PER_CRED: "2" }); 459 await registerGameCred(registry, "cred-1"); 460 for (const device of ["device1", "device2"]) { 461 const response = await registry.handleRegister( 462 registerRequest(gameSignature(gameSecret, "cred-1")), 463 registerBody([{ address: `addr-${device}`, credID: "cred-1" }], device), 464 { deviceID: device } 465 ); 466 assert.equal(response.status, 204); 467 } 468 469 const over = await registry.handleRegister( 470 registerRequest(gameSignature(gameSecret, "cred-1")), 471 registerBody([{ address: "addr-device3", credID: "cred-1" }], "device3"), 472 { deviceID: "device3" } 473 ); 474 assert.equal(over.status, 400); 475 assert.equal(storage.keys("addr:cred-1:").length, 2); 476 477 // Re-registering an existing binding is an overwrite, not growth. 478 const again = await registry.handleRegister( 479 registerRequest(gameSignature(gameSecret, "cred-1")), 480 registerBody([{ address: "addr-device1", credID: "cred-1" }], "device1"), 481 { deviceID: "device1" } 482 ); 483 assert.equal(again.status, 204); 484 }); 485 486 test("game credential registration bounds credID and secret", async () => { 487 const { registry } = makeRegistry(); 488 const badCredID = await registry.handleGameRegister( 489 "cred:forged", 490 JSON.stringify({ secret: gameSecret }) 491 ); 492 assert.equal(badCredID.status, 400); 493 494 const hugeSecret = await registry.handleGameRegister( 495 "cred-1", 496 JSON.stringify({ secret: "A".repeat(1000) }) 497 ); 498 assert.equal(hugeSecret.status, 400); 499 }); 500 501 function publishRequest() { 502 return new Request("https://push.example/publish", { method: "POST" }); 503 } 504 505 function publishBody(overrides = {}) { 506 return JSON.stringify({ 507 kind: "win", 508 addressees: [{ address: "addr-a" }], 509 ...overrides 510 }); 511 } 512 513 test("a bounded publish with no registered targets succeeds cheaply", async () => { 514 const { registry } = makeRegistry(); 515 const response = await registry.handlePublish( 516 publishRequest(), 517 publishBody(), 518 { deviceID: "device1" } 519 ); 520 assert.equal(response.status, 200); 521 assert.deepEqual(await response.json(), { delivered: 0, removed: 0, muted: 0, failed: 0 }); 522 }); 523 524 test("publish refuses oversized addressee lists and fields", async () => { 525 const { registry } = makeRegistry({ MAX_ADDRESS_COUNT: "2" }); 526 const tooMany = await registry.handlePublish( 527 publishRequest(), 528 publishBody({ addressees: [{ address: "a1" }, { address: "a2" }, { address: "a3" }] }), 529 { deviceID: "device1" } 530 ); 531 assert.equal(tooMany.status, 400); 532 533 const hugeEnc = await registry.handlePublish( 534 publishRequest(), 535 publishBody({ addressees: [{ address: "a1", enc: "A".repeat(5000) }] }), 536 { deviceID: "device1" } 537 ); 538 assert.equal(hugeEnc.status, 400); 539 540 const hugeBody = await registry.handlePublish( 541 publishRequest(), 542 publishBody({ alertBody: "x".repeat(1000) }), 543 { deviceID: "device1" } 544 ); 545 assert.equal(hugeBody.status, 400); 546 547 const badCollapse = await registry.handlePublish( 548 publishRequest(), 549 publishBody({ collapseID: "c".repeat(65) }), 550 { deviceID: "device1" } 551 ); 552 assert.equal(badCollapse.status, 400); 553 554 const badAddress = await registry.handlePublish( 555 publishRequest(), 556 publishBody({ addressees: [{ address: "addr:forged" }] }), 557 { deviceID: "device1" } 558 ); 559 assert.equal(badAddress.status, 400); 560 }); 561 562 test("a publish whose APNs payload cannot fit is refused before any fanout", async () => { 563 const { registry } = makeRegistry(); 564 // Each field is within its own cap, but the assembled APNs payload 565 // (aps + kind + title + enc as JSON) exceeds the 4 KB APNs ceiling. 566 const response = await registry.handlePublish( 567 publishRequest(), 568 publishBody({ 569 title: "t".repeat(400), 570 alertBody: "b".repeat(500), 571 addressees: [{ address: "a1", enc: "A".repeat(4000) }] 572 }), 573 { deviceID: "device1" } 574 ); 575 assert.equal(response.status, 413); 576 577 // The same shape with a small enc fits and proceeds. 578 const fits = await registry.handlePublish( 579 publishRequest(), 580 publishBody({ 581 title: "t".repeat(400), 582 alertBody: "b".repeat(500), 583 addressees: [{ address: "a1", enc: "A".repeat(500) }] 584 }), 585 { deviceID: "device1" } 586 ); 587 assert.equal(fits.status, 200); 588 }); 589 590 test("broadcast fanout is capped even for an over-cap room", async () => { 591 const { registry, storage } = makeRegistry({ MAX_BROADCAST_TARGETS: "2" }); 592 for (let index = 0; index < 4; index += 1) { 593 await storage.put(`addr:cred-1:addr-${index}:device-${index}`, { 594 token: "t", 595 environment: "production" 596 }); 597 } 598 const targets = await registry.resolveBroadcastTargets( 599 "cred-1", null, null, "body", undefined, undefined 600 ); 601 assert.equal(targets.length, 2); 602 });