@orieljs/framework 0.1.0-alpha.3 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3653 +0,0 @@
1
- import {
2
- Arr,
3
- ClassRegistry as ClassRegistry2,
4
- Container,
5
- ContainerNotAvailableError,
6
- Str,
7
- currentContainer,
8
- facade
9
- } from "./chunk-IRHK7GH7.js";
10
- import {
11
- Application,
12
- AuthFactory,
13
- BusDispatcher,
14
- ClassRegistry,
15
- ConfigRepository,
16
- ConsoleKernel,
17
- DatabaseManager,
18
- Encrypter,
19
- EventDispatcher,
20
- ExceptionCallbacks,
21
- ExceptionHandler,
22
- FilesystemManager,
23
- HttpKernel,
24
- HttpRequest,
25
- MailManager,
26
- MiddlewareCallbacks,
27
- NotificationDispatcher,
28
- QueueManager,
29
- RoutingPaths,
30
- Session,
31
- UrlGenerator,
32
- ViewFactory
33
- } from "./chunk-3ZRPBRAE.js";
34
-
35
- // ../Config/dist/index.js
36
- import { existsSync, readFileSync } from "fs";
37
- import { join } from "path";
38
- import { parse } from "dotenv";
39
- import { existsSync as existsSync2, readdirSync } from "fs";
40
- import { join as join2 } from "path";
41
- import { pathToFileURL } from "url";
42
- var Repository = class {
43
- constructor(items = {}) {
44
- this.items = items;
45
- }
46
- items;
47
- has(key) {
48
- return Arr.has(this.items, key);
49
- }
50
- get(key, defaultValue) {
51
- return Arr.get(this.items, key, defaultValue);
52
- }
53
- set(keyOrValues, value) {
54
- const entries = typeof keyOrValues === "string" ? [[keyOrValues, value]] : Object.entries(keyOrValues);
55
- for (const [k, v] of entries) Arr.set(this.items, k, v);
56
- }
57
- push(key, value) {
58
- const current = this.get(key, []);
59
- this.set(key, [...Array.isArray(current) ? current : [current], value]);
60
- }
61
- all() {
62
- return this.items;
63
- }
64
- };
65
- var CASTS = {
66
- "true": true,
67
- "(true)": true,
68
- "false": false,
69
- "(false)": false,
70
- "null": null,
71
- "(null)": null,
72
- "empty": "",
73
- "(empty)": ""
74
- };
75
- function env(key, defaultValue) {
76
- const raw = process.env[key];
77
- if (raw === void 0) return typeof defaultValue === "function" ? defaultValue() : defaultValue;
78
- const lower = raw.toLowerCase();
79
- if (Object.hasOwn(CASTS, lower)) return CASTS[lower];
80
- const quoted = /^(["'])(.*)\1$/s.exec(raw);
81
- return quoted ? quoted[2] : raw;
82
- }
83
- function loadEnvironment(basePath) {
84
- const appEnv = process.env.APP_ENV;
85
- const specific = appEnv ? join(basePath, `.env.${appEnv}`) : void 0;
86
- const file = specific && existsSync(specific) ? specific : join(basePath, ".env");
87
- if (!existsSync(file)) return void 0;
88
- for (const [key, value] of Object.entries(parse(readFileSync(file)))) {
89
- if (process.env[key] === void 0) process.env[key] = value;
90
- }
91
- return file;
92
- }
93
- var CONFIG_FILE = /^(?!.*\.d\.ts$)([A-Za-z0-9_-]+)\.(ts|js|mjs)$/;
94
- function clonePlain(value) {
95
- if (Array.isArray(value)) return value.map((item) => clonePlain(item));
96
- if (typeof value === "object" && value !== null) {
97
- const proto = Object.getPrototypeOf(value);
98
- if (proto !== Object.prototype && proto !== null) return value;
99
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clonePlain(item)]));
100
- }
101
- return value;
102
- }
103
- async function loadConfiguration(configPath) {
104
- if (!existsSync2(configPath)) return {};
105
- const files = readdirSync(configPath).filter((f) => CONFIG_FILE.test(f)).sort();
106
- const config = {};
107
- for (const file of files) {
108
- const name = CONFIG_FILE.exec(file)[1];
109
- const mod = await import(pathToFileURL(join2(configPath, file)).href);
110
- config[name] = clonePlain(mod.default ?? {});
111
- }
112
- return config;
113
- }
114
-
115
- // ../Foundation/dist/index.js
116
- import { join as join3 } from "path";
117
- import { isAbsolute, join as join4 } from "path";
118
- import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "fs";
119
- import { createHash, randomBytes } from "crypto";
120
- import { join as join22 } from "path";
121
- var ApplicationBuilder = class {
122
- constructor(app) {
123
- this.app = app;
124
- }
125
- app;
126
- providers = [];
127
- middleware = [];
128
- exceptions = [];
129
- routing;
130
- withProviders(providers = []) {
131
- this.providers.push(...providers);
132
- return this;
133
- }
134
- withRouting(options) {
135
- const abs = (p) => p === void 0 ? void 0 : isAbsolute(p) ? p : join4(this.app.basePath(), p);
136
- const paths = { apiPrefix: options.apiPrefix ?? "api" };
137
- if (options.web !== void 0) paths.web = abs(options.web);
138
- if (options.api !== void 0) paths.api = abs(options.api);
139
- if (options.console !== void 0) paths.console = abs(options.console);
140
- if (options.health !== void 0) paths.health = options.health;
141
- this.routing = paths;
142
- return this;
143
- }
144
- withMiddleware(callback) {
145
- this.middleware.push(callback);
146
- return this;
147
- }
148
- withExceptions(callback) {
149
- this.exceptions.push(callback);
150
- return this;
151
- }
152
- booting(callback) {
153
- this.app.booting(callback);
154
- return this;
155
- }
156
- booted(callback) {
157
- this.app.booted(callback);
158
- return this;
159
- }
160
- async create() {
161
- loadEnvironment(this.app.basePath());
162
- this.app.instance(ConfigRepository, new Repository(await loadConfiguration(this.app.configPath())));
163
- if (this.routing) this.app.instance(RoutingPaths, this.routing);
164
- this.app.instance(MiddlewareCallbacks, this.middleware);
165
- this.app.instance(ExceptionCallbacks, this.exceptions);
166
- for (const provider of this.providers) await this.app.register(provider);
167
- return this.app;
168
- }
169
- };
170
- var ProviderBootError = class extends Error {
171
- constructor(provider, cause) {
172
- super(`Service provider [${provider}] failed to boot: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
173
- this.provider = provider;
174
- }
175
- provider;
176
- name = "ProviderBootError";
177
- };
178
- var ProviderRegisterError = class extends Error {
179
- constructor(provider, cause) {
180
- super(`Service provider [${provider}] failed to register: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
181
- this.provider = provider;
182
- }
183
- provider;
184
- name = "ProviderRegisterError";
185
- };
186
- var ViteError = class extends Error {
187
- kind = "Server";
188
- status = 500;
189
- constructor(message) {
190
- super(message);
191
- this.name = "ViteError";
192
- }
193
- };
194
- var ViteHtml = class {
195
- constructor(html) {
196
- this.html = html;
197
- }
198
- html;
199
- toHtml() {
200
- return this.html;
201
- }
202
- toString() {
203
- return this.html;
204
- }
205
- };
206
- var CSS_PATH = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)(\?[^.]*)?$/;
207
- var manifests = /* @__PURE__ */ new Map();
208
- var escapeAttribute = (value) => value.replace(/&/g, "&").replace(/"/g, """);
209
- var Vite = class {
210
- constructor(app) {
211
- this.app = app;
212
- }
213
- app;
214
- #hotFile;
215
- #buildDirectory = "build";
216
- #manifestFilename = "manifest.json";
217
- #nonce;
218
- #integrityKey = "integrity";
219
- #entryPoints = [];
220
- #scriptResolvers = [];
221
- #styleResolvers = [];
222
- #preloadResolvers = [];
223
- #assetPathResolver;
224
- #preloaded = {};
225
- // Configuration --------------------------------------------------------------------------------------------
226
- useCspNonce(nonce) {
227
- this.#nonce = nonce ?? randomBytes(30).toString("base64url").slice(0, 40);
228
- return this.#nonce;
229
- }
230
- cspNonce() {
231
- return this.#nonce;
232
- }
233
- useIntegrityKey(key) {
234
- this.#integrityKey = key;
235
- return this;
236
- }
237
- withEntryPoints(entryPoints) {
238
- this.#entryPoints = entryPoints;
239
- return this;
240
- }
241
- useHotFile(path) {
242
- this.#hotFile = path;
243
- return this;
244
- }
245
- useBuildDirectory(path) {
246
- this.#buildDirectory = path;
247
- return this;
248
- }
249
- useManifestFilename(filename) {
250
- this.#manifestFilename = filename;
251
- return this;
252
- }
253
- useScriptTagAttributes(attributes) {
254
- this.#scriptResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
255
- return this;
256
- }
257
- useStyleTagAttributes(attributes) {
258
- this.#styleResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
259
- return this;
260
- }
261
- /** Extra preload attributes; a resolver returning false drops that preload. */
262
- usePreloadTagAttributes(attributes) {
263
- this.#preloadResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
264
- return this;
265
- }
266
- createAssetPathsUsing(resolver) {
267
- this.#assetPathResolver = resolver;
268
- return this;
269
- }
270
- // Dev server -----------------------------------------------------------------------------------------------
271
- hotFile() {
272
- return this.#hotFile ?? this.app.publicPath("hot");
273
- }
274
- isRunningHot() {
275
- return existsSync3(this.hotFile());
276
- }
277
- /** The dev server URL written by @orieljs/vite, or undefined when it isn't running. */
278
- devServerUrl() {
279
- return this.isRunningHot() ? readFileSync2(this.hotFile(), "utf8").trimEnd() : void 0;
280
- }
281
- // Tags -----------------------------------------------------------------------------------------------------
282
- /** Laravel's @vite([...]): preloads (styles first), then stylesheets, then scripts. */
283
- tags(entryPoints, buildDirectory = this.#buildDirectory) {
284
- const entries = [entryPoints].flat();
285
- if (this.isRunningHot()) {
286
- return new ViteHtml(["@vite/client", ...entries].map((entry) => this.#tagForChunk(entry, this.#hotAsset(entry), void 0, void 0)).join(""));
287
- }
288
- const manifest = this.manifest(buildDirectory);
289
- const tags = [];
290
- const preloads = [];
291
- const cssChunk = (css) => {
292
- const found = Object.entries(manifest).find(([, chunk2]) => chunk2.file === css);
293
- return found === void 0 ? [css, void 0] : [found[0], found[1]];
294
- };
295
- for (const entry of entries) {
296
- const chunk2 = this.#chunk(manifest, entry);
297
- preloads.push([chunk2.src ?? entry, this.#assetPath(`${buildDirectory}/${chunk2.file}`), chunk2]);
298
- for (const name of this.#resolveImports(manifest, chunk2)) {
299
- const imported = manifest[name];
300
- preloads.push([name, this.#assetPath(`${buildDirectory}/${imported.file}`), imported]);
301
- for (const css of imported.css ?? []) {
302
- const [key, partial] = cssChunk(css);
303
- preloads.push([key, this.#assetPath(`${buildDirectory}/${css}`), partial]);
304
- tags.push(this.#tagForChunk(key, this.#assetPath(`${buildDirectory}/${css}`), partial, manifest));
305
- }
306
- }
307
- tags.push(this.#tagForChunk(entry, this.#assetPath(`${buildDirectory}/${chunk2.file}`), chunk2, manifest));
308
- for (const css of chunk2.css ?? []) {
309
- const [key, partial] = cssChunk(css);
310
- preloads.push([key, this.#assetPath(`${buildDirectory}/${css}`), partial]);
311
- tags.push(this.#tagForChunk(key, this.#assetPath(`${buildDirectory}/${css}`), partial, manifest));
312
- }
313
- }
314
- const uniqueTags = [...new Set(tags)];
315
- const stylesheets = uniqueTags.filter((tag) => tag.startsWith("<link"));
316
- const scripts = uniqueTags.filter((tag) => !tag.startsWith("<link"));
317
- const seen = /* @__PURE__ */ new Set();
318
- const uniquePreloads = preloads.filter(([, url]) => seen.has(url) ? false : (seen.add(url), true));
319
- const sorted = [...uniquePreloads.filter(([, url]) => CSS_PATH.test(url)), ...uniquePreloads.filter(([, url]) => !CSS_PATH.test(url))];
320
- const preloadTags = sorted.map(([src, url, chunk2]) => this.#preloadTag(src, url, chunk2, manifest));
321
- return new ViteHtml(preloadTags.join("") + stylesheets.join("") + scripts.join(""));
322
- }
323
- /** Laravel's @viteReactRefresh: the React Refresh preamble while the dev server runs; nothing otherwise. */
324
- reactRefresh() {
325
- if (!this.isRunningHot()) return void 0;
326
- const attributes = this.#parseAttributes({ nonce: this.#nonce ?? false });
327
- return new ViteHtml(
328
- [
329
- `<script type="module" ${attributes.join(" ")}>`,
330
- ` import RefreshRuntime from '${this.#hotAsset("@react-refresh")}'`,
331
- " RefreshRuntime.injectIntoGlobalHook(window)",
332
- " window.$RefreshReg$ = () => {}",
333
- " window.$RefreshSig$ = () => (type) => type",
334
- " window.__vite_plugin_react_preamble_installed__ = true",
335
- "</script>"
336
- ].join("\n")
337
- );
338
- }
339
- /** The URL of a processed asset (Vite::asset('resources/images/logo.svg')). */
340
- asset(path, buildDirectory = this.#buildDirectory) {
341
- if (this.isRunningHot()) return this.#hotAsset(path);
342
- return this.#assetPath(`${buildDirectory}/${this.#chunk(this.manifest(buildDirectory), path).file}`);
343
- }
344
- /** The built file's contents (Vite::content()). */
345
- content(path, buildDirectory = this.#buildDirectory) {
346
- const chunk2 = this.#chunk(this.manifest(buildDirectory), path);
347
- const file = this.app.publicPath(join22(buildDirectory, chunk2.file));
348
- if (!existsSync3(file)) throw new ViteError(`Unable to locate file from Vite manifest: ${file}.`);
349
- return readFileSync2(file, "utf8");
350
- }
351
- /** md5 of the manifest (a cache-busting asset version); undefined while hot or without a build. */
352
- manifestHash(buildDirectory = this.#buildDirectory) {
353
- if (this.isRunningHot()) return void 0;
354
- const path = this.manifestPath(buildDirectory);
355
- return existsSync3(path) ? createHash("md5").update(readFileSync2(path)).digest("hex") : void 0;
356
- }
357
- manifestPath(buildDirectory = this.#buildDirectory) {
358
- return this.app.publicPath(join22(buildDirectory, this.#manifestFilename));
359
- }
360
- manifest(buildDirectory = this.#buildDirectory) {
361
- const path = this.manifestPath(buildDirectory);
362
- if (!existsSync3(path)) throw new ViteError(`Vite manifest not found at: ${path}`);
363
- const { mtimeMs } = statSync(path);
364
- const cached = manifests.get(path);
365
- if (cached !== void 0 && cached.mtimeMs === mtimeMs) return cached.manifest;
366
- const manifest = JSON.parse(readFileSync2(path, "utf8"));
367
- manifests.set(path, { mtimeMs, manifest });
368
- return manifest;
369
- }
370
- /** URL → the attributes of the preload tags rendered so far (without href), for Link headers. */
371
- preloadedAssets() {
372
- return { ...this.#preloaded };
373
- }
374
- /** Renders withEntryPoints() (Vite is Htmlable, like Laravel's). */
375
- toHtml() {
376
- return this.tags(this.#entryPoints).toHtml();
377
- }
378
- // Internals ------------------------------------------------------------------------------------------------
379
- #hotAsset(path) {
380
- return `${this.devServerUrl() ?? ""}/${path}`;
381
- }
382
- #assetPath(path) {
383
- if (this.#assetPathResolver !== void 0) return this.#assetPathResolver(path);
384
- if (this.app.bound(UrlGenerator)) return this.app.make(UrlGenerator).asset(path);
385
- const base = this.app.bound(ConfigRepository) ? this.app.make(ConfigRepository).get("app.asset_url", null) : null;
386
- return `${(base ?? "").replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
387
- }
388
- #chunk(manifest, file) {
389
- const chunk2 = manifest[file];
390
- if (chunk2 === void 0) throw new ViteError(`Unable to locate file in Vite manifest: ${file}.`);
391
- return chunk2;
392
- }
393
- /** Every static import, depth first, each once. */
394
- #resolveImports(manifest, chunk2, seen = /* @__PURE__ */ new Set()) {
395
- const imports = [];
396
- for (const name of chunk2.imports ?? []) {
397
- if (seen.has(name)) continue;
398
- seen.add(name);
399
- imports.push(name);
400
- const imported = manifest[name];
401
- if (imported !== void 0) imports.push(...this.#resolveImports(manifest, imported, seen));
402
- }
403
- return imports;
404
- }
405
- #tagForChunk(src, url, chunk2, manifest) {
406
- if (CSS_PATH.test(url)) {
407
- const attributes2 = this.#parseAttributes({ rel: "stylesheet", href: url, nonce: this.#nonce ?? false, ...this.#styleAttributes(src, url, chunk2, manifest) });
408
- return `<link ${attributes2.join(" ")} />`;
409
- }
410
- const attributes = this.#parseAttributes({ type: "module", src: url, nonce: this.#nonce ?? false, ...this.#scriptAttributes(src, url, chunk2, manifest) });
411
- return `<script ${attributes.join(" ")}></script>`;
412
- }
413
- #preloadTag(src, url, chunk2, manifest) {
414
- const css = CSS_PATH.test(url);
415
- let attributes = {
416
- rel: css ? "preload" : "modulepreload",
417
- as: css ? "style" : "script",
418
- href: url,
419
- nonce: this.#nonce ?? false,
420
- crossorigin: (css ? this.#styleAttributes(src, url, chunk2, manifest) : this.#scriptAttributes(src, url, chunk2, manifest)).crossorigin ?? false
421
- };
422
- if (this.#integrityKey !== false) attributes.integrity = chunk2?.[this.#integrityKey] ?? false;
423
- for (const resolver of this.#preloadResolvers) {
424
- const resolved = resolver(src, url, chunk2, manifest);
425
- if (resolved === false) return "";
426
- attributes = { ...attributes, ...resolved };
427
- }
428
- const { href: _href, ...rest } = attributes;
429
- this.#preloaded[url] = this.#parseAttributes(rest);
430
- return `<link ${this.#parseAttributes(attributes).join(" ")} />`;
431
- }
432
- #scriptAttributes(src, url, chunk2, manifest) {
433
- let attributes = this.#integrityKey !== false ? { integrity: chunk2?.[this.#integrityKey] ?? false } : {};
434
- for (const resolver of this.#scriptResolvers) attributes = { ...attributes, ...resolver(src, url, chunk2, manifest) };
435
- return attributes;
436
- }
437
- #styleAttributes(src, url, chunk2, manifest) {
438
- let attributes = this.#integrityKey !== false ? { integrity: chunk2?.[this.#integrityKey] ?? false } : {};
439
- for (const resolver of this.#styleResolvers) attributes = { ...attributes, ...resolver(src, url, chunk2, manifest) };
440
- return attributes;
441
- }
442
- /** false/null/undefined are dropped, true is a bare attribute, anything else name="value". */
443
- #parseAttributes(attributes) {
444
- return Object.entries(attributes).filter(([, value]) => value !== false && value !== null && value !== void 0).map(([name, value]) => value === true ? name : `${name}="${escapeAttribute(String(value))}"`);
445
- }
446
- };
447
- function currentVite() {
448
- const container = currentContainer();
449
- if (container === void 0) throw new ContainerNotAvailableError("Vite");
450
- if (container.bound(HttpRequest)) return container.make(Vite);
451
- return new Vite(container.make(Application));
452
- }
453
- function vite(entryPoints, buildDirectory) {
454
- const instance = currentVite();
455
- return entryPoints === void 0 ? instance : instance.tags(entryPoints, buildDirectory);
456
- }
457
- function viteReactRefresh() {
458
- return currentVite().reactRefresh();
459
- }
460
- var Application2 = class _Application extends Container {
461
- constructor(base) {
462
- super();
463
- this.base = base;
464
- Container.setInstance(this);
465
- this.instance(Application, this);
466
- this.instance(_Application, this);
467
- this.singleton(ClassRegistry, ClassRegistry2);
468
- this.scoped(Vite, () => new Vite(this));
469
- }
470
- base;
471
- version = "0.1.0-alpha.1";
472
- providers = [];
473
- bootingCallbacks = [];
474
- bootedCallbacks = [];
475
- terminatingCallbacks = [];
476
- bootPromise;
477
- hasBooted = false;
478
- inConsole = false;
479
- /** Entry point used by bootstrap/app.ts. */
480
- static configure(options) {
481
- return new ApplicationBuilder(new _Application(options.basePath));
482
- }
483
- basePath(path = "") {
484
- return join3(this.base, path);
485
- }
486
- appPath(path = "") {
487
- return this.basePath(join3("app", path));
488
- }
489
- bootstrapPath(path = "") {
490
- return this.basePath(join3("bootstrap", path));
491
- }
492
- configPath(path = "") {
493
- return this.basePath(join3("config", path));
494
- }
495
- databasePath(path = "") {
496
- return this.basePath(join3("database", path));
497
- }
498
- publicPath(path = "") {
499
- return this.basePath(join3("public", path));
500
- }
501
- resourcePath(path = "") {
502
- return this.basePath(join3("resources", path));
503
- }
504
- storagePath(path = "") {
505
- return this.basePath(join3("storage", path));
506
- }
507
- environment(...environments) {
508
- const fallback = process.env.APP_ENV ?? "production";
509
- const current = this.bound(ConfigRepository) ? String(this.make(ConfigRepository).get("app.env", fallback)) : fallback;
510
- return environments.length === 0 ? current : environments.some((pattern) => Str.is(pattern, current));
511
- }
512
- isProduction() {
513
- return this.environment("production");
514
- }
515
- isLocal() {
516
- return this.environment("local");
517
- }
518
- runningUnitTests() {
519
- return this.environment("testing");
520
- }
521
- runningInConsole() {
522
- return this.inConsole;
523
- }
524
- setRunningInConsole(value = true) {
525
- this.inConsole = value;
526
- }
527
- async register(provider) {
528
- const cls = typeof provider === "function" ? provider : provider.constructor;
529
- const existing = this.providers.find((p) => p.constructor === cls);
530
- if (existing) return existing;
531
- const instance = typeof provider === "function" ? new provider(this) : provider;
532
- try {
533
- instance.register?.();
534
- } catch (error) {
535
- throw new ProviderRegisterError(instance.constructor.name, error);
536
- }
537
- this.providers.push(instance);
538
- if (this.hasBooted) await this.bootProvider(instance);
539
- return instance;
540
- }
541
- getProviders() {
542
- return [...this.providers];
543
- }
544
- boot() {
545
- return this.bootPromise ??= this.runBoot();
546
- }
547
- isBooted() {
548
- return this.hasBooted;
549
- }
550
- booting(callback) {
551
- this.bootingCallbacks.push(callback);
552
- }
553
- /** If already booted, the callback runs immediately (a rejection surfaces as an unhandled rejection). */
554
- booted(callback) {
555
- this.bootedCallbacks.push(callback);
556
- if (this.hasBooted) void callback(this);
557
- }
558
- terminating(callback) {
559
- this.terminatingCallbacks.push(callback);
560
- }
561
- async handle(request) {
562
- await this.boot();
563
- const scope = this.createScope();
564
- return scope.run(() => scope.make(HttpKernel).handle(request));
565
- }
566
- async terminate() {
567
- for (const callback of this.terminatingCallbacks) await callback();
568
- }
569
- async runBoot() {
570
- for (const callback of this.bootingCallbacks) await callback(this);
571
- for (let i = 0; i < this.providers.length; i++) await this.bootProvider(this.providers[i]);
572
- this.hasBooted = true;
573
- for (const callback of this.bootedCallbacks) await callback(this);
574
- }
575
- async bootProvider(provider) {
576
- try {
577
- await provider.boot?.();
578
- } catch (error) {
579
- throw new ProviderBootError(provider.constructor.name, error);
580
- }
581
- }
582
- };
583
- var ClosureKernel = class {
584
- routes = /* @__PURE__ */ new Map();
585
- on(method, path, handler) {
586
- this.routes.set(`${method.toUpperCase()} ${path}`, handler);
587
- return this;
588
- }
589
- async handle(request) {
590
- const handler = this.routes.get(`${request.method} ${new URL(request.url).pathname}`);
591
- if (!handler) return new Response("Not Found", { status: 404, headers: { "content-type": "text/plain; charset=utf-8" } });
592
- return toResponse(await handler(request));
593
- }
594
- };
595
- function toResponse(output) {
596
- if (output instanceof Response) return output;
597
- if (output === void 0 || output === null) return new Response(null, { status: 204 });
598
- if (typeof output === "string") return new Response(output, { headers: { "content-type": "text/html; charset=utf-8" } });
599
- return Response.json(output);
600
- }
601
-
602
- // ../Testing/dist/index.js
603
- import { AssertionError } from "assert";
604
- import { isDeepStrictEqual } from "util";
605
- import { existsSync as existsSync4 } from "fs";
606
- import { join as join5 } from "path";
607
- import { createHmac } from "crypto";
608
- import { randomUUID } from "crypto";
609
- import { randomUUID as randomUUID2 } from "crypto";
610
- import { randomUUID as randomUUID3 } from "crypto";
611
- import { randomBytes as randomBytes2 } from "crypto";
612
- import { deflateSync } from "zlib";
613
- function fail(message, details = {}) {
614
- const error = new AssertionError({ message, actual: details.actual, expected: details.expected, operator: "fail" });
615
- delete error.diff;
616
- Error.captureStackTrace?.(error, fail);
617
- throw error;
618
- }
619
- function assert(condition, message, details) {
620
- if (!condition) fail(typeof message === "function" ? message() : message, details);
621
- }
622
- function format(value) {
623
- if (typeof value === "string") return JSON.stringify(value);
624
- if (typeof value === "function") return value.name === "" ? "[Function]" : `[Function ${value.name}]`;
625
- try {
626
- return JSON.stringify(value, (_key, item) => typeof item === "bigint" ? `${item}n` : item) ?? String(value);
627
- } catch {
628
- return String(value);
629
- }
630
- }
631
- function escapeHtml(value) {
632
- return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" })[char]);
633
- }
634
- function stripTags(html) {
635
- return html.replace(/<[^>]*>/g, "");
636
- }
637
- function equals(a, b) {
638
- return isDeepStrictEqual(canonical(a), canonical(b));
639
- }
640
- function canonical(value) {
641
- if (Array.isArray(value)) return value.map(canonical);
642
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
643
- return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
644
- }
645
- return value;
646
- }
647
- function isSubset(subset, actual) {
648
- if (typeof subset !== "object" || subset === null) return equals(subset, actual);
649
- if (typeof actual !== "object" || actual === null) return false;
650
- if (Array.isArray(subset) !== Array.isArray(actual)) return false;
651
- return Object.entries(subset).every(([key, value]) => Object.hasOwn(actual, key) && isSubset(value, actual[key]));
652
- }
653
- function* objectsIn(value) {
654
- if (typeof value !== "object" || value === null) return;
655
- if (!Array.isArray(value)) yield value;
656
- for (const child of Object.values(value)) yield* objectsIn(child);
657
- }
658
- function containsPair(tree, key, expected) {
659
- for (const object of objectsIn(tree)) if (Object.hasOwn(object, key) && isSubset(expected, object[key])) return true;
660
- return false;
661
- }
662
- function assertStructure(structure, data, path = "") {
663
- const at = (key) => path === "" ? key : `${path}.${key}`;
664
- const hasKey = (key) => typeof data === "object" && data !== null && Object.hasOwn(data, key);
665
- if (Array.isArray(structure)) {
666
- for (const item of structure) {
667
- if (typeof item === "string") assert(hasKey(item), `Failed asserting that the JSON has the key [${at(item)}].`, { actual: data });
668
- else assertStructure(item, data, path);
669
- }
670
- return;
671
- }
672
- for (const [key, nested] of Object.entries(structure)) {
673
- if (key === "*") {
674
- assert(Array.isArray(data) || typeof data === "object" && data !== null, `Failed asserting that [${path || "the JSON"}] is a list.`, { actual: data });
675
- for (const [index, element] of Object.entries(data)) assertStructure(nested, element, at(index));
676
- continue;
677
- }
678
- assert(hasKey(key), `Failed asserting that the JSON has the key [${at(key)}].`, { actual: data });
679
- assertStructure(nested, data[key], at(key));
680
- }
681
- }
682
- var PendingCommand = class {
683
- constructor(app, command, parameters = {}) {
684
- this.app = app;
685
- this.command = command;
686
- this.parameters = parameters;
687
- }
688
- app;
689
- command;
690
- parameters;
691
- checks = [];
692
- result;
693
- /** A line of output equals `line` exactly; with no argument, the command printed something. */
694
- expectsOutput(line) {
695
- this.checks.push((output, lines) => {
696
- if (line === void 0) assert(output !== "", "The command was expected to print output, but printed nothing.");
697
- else assert(lines.includes(line), `Output "${line}" was not printed.
698
-
699
- Output:
700
- ${output}`, { actual: output, expected: line });
701
- });
702
- return this;
703
- }
704
- /** No line equals `line`; with no argument, the command printed nothing at all. */
705
- doesntExpectOutput(line) {
706
- this.checks.push((output, lines) => {
707
- if (line === void 0) assert(output === "", `The command printed output unexpectedly:
708
- ${output}`, { actual: output, expected: "" });
709
- else assert(!lines.includes(line), `Output "${line}" was printed.`);
710
- });
711
- return this;
712
- }
713
- expectsOutputToContain(text) {
714
- this.checks.push((output) => assert(output.includes(text), `Output does not contain "${text}".
715
-
716
- Output:
717
- ${output}`, { actual: output, expected: text }));
718
- return this;
719
- }
720
- doesntExpectOutputToContain(text) {
721
- this.checks.push((output) => assert(!output.includes(text), `Output "${text}" was printed.`));
722
- return this;
723
- }
724
- assertExitCode(code) {
725
- this.checks.push(
726
- (_output, _lines, exitCode) => assert(exitCode === code, `Expected status code ${code} but received ${exitCode}.`, { actual: exitCode, expected: code })
727
- );
728
- return this;
729
- }
730
- assertNotExitCode(code) {
731
- this.checks.push((_output, _lines, exitCode) => assert(exitCode !== code, `Unexpected status code ${code} was received.`));
732
- return this;
733
- }
734
- assertSuccessful() {
735
- return this.assertExitCode(0);
736
- }
737
- assertOk() {
738
- return this.assertSuccessful();
739
- }
740
- assertFailed() {
741
- return this.assertNotExitCode(0);
742
- }
743
- /** Runs the command once and checks the expectations; resolves to the exit code. */
744
- run() {
745
- return this.result ??= this.execute();
746
- }
747
- then(onfulfilled, onrejected) {
748
- return this.run().then(onfulfilled, onrejected);
749
- }
750
- async execute() {
751
- if (!this.app.bound(ConsoleKernel)) throw new Error(`Cannot run [${this.command}]: the Console component (ConsoleKernel) is not registered.`);
752
- const kernel = this.app.make(ConsoleKernel);
753
- const exitCode = await kernel.call(this.command, this.parameters);
754
- const output = kernel.output();
755
- const lines = output.split("\n").map((line) => line.replace(/\r$/, ""));
756
- for (const check of this.checks) check(output, lines, exitCode);
757
- return exitCode;
758
- }
759
- };
760
- var SHOW = 3;
761
- function target(db, table, connection) {
762
- if (typeof table === "string") return { table, connection: db.connection(connection) };
763
- return { table: table.getTable(), connection: db.connection(connection ?? table.getConnectionName?.()) };
764
- }
765
- function constrain(query, data) {
766
- for (const [column, value] of Object.entries(data)) {
767
- if (value === null) query.whereNull(column);
768
- else query.where(column, value);
769
- }
770
- return query;
771
- }
772
- async function similar(connection, table, data) {
773
- const query = connection.table(table);
774
- const columns = Object.keys(data);
775
- if (columns.length > 0) query.select(...columns);
776
- const rows = (await query.limit(SHOW).get()).all();
777
- if (rows.length === 0) return "The table is empty.";
778
- return `Found similar results: ${JSON.stringify(rows, null, 2)}`;
779
- }
780
- async function assertDatabaseHas(db, table, data, connection) {
781
- const t = target(db, table, connection);
782
- if (await constrain(t.connection.table(t.table), data).count() > 0) return;
783
- fail(`Failed asserting that a row in the table [${t.table}] matches the attributes ${JSON.stringify(data, null, 2)}.
784
-
785
- ${await similar(t.connection, t.table, data)}`);
786
- }
787
- async function assertDatabaseMissing(db, table, data, connection) {
788
- const t = target(db, table, connection);
789
- const count = await constrain(t.connection.table(t.table), data).count();
790
- assert(count === 0, `Failed asserting that a row in the table [${t.table}] does not match the attributes ${JSON.stringify(data, null, 2)}.`);
791
- }
792
- async function assertDatabaseCount(db, table, expected, connection) {
793
- const t = target(db, table, connection);
794
- const actual = await t.connection.table(t.table).count();
795
- assert(actual === expected, `Failed asserting that table [${t.table}] matches expected entries count of ${expected}. Entries found: ${actual}.`, { actual, expected });
796
- }
797
- function softDeleteTarget(db, table, data, connection, deletedAtColumn) {
798
- if (typeof table === "string") return { ...target(db, table, connection), data, column: deletedAtColumn };
799
- const model = table;
800
- return { ...target(db, model, connection), data: { [model.getKeyName()]: model.getKey() }, column: model.getDeletedAtColumn?.() ?? deletedAtColumn };
801
- }
802
- async function assertSoftDeleted(db, table, data = {}, connection, deletedAtColumn = "deleted_at") {
803
- const t = softDeleteTarget(db, table, data, connection, deletedAtColumn);
804
- const count = await constrain(t.connection.table(t.table), t.data).whereNotNull(t.column).count();
805
- assert(count > 0, `Failed asserting that any soft deleted row in the table [${t.table}] matches the attributes ${JSON.stringify(t.data, null, 2)}.`);
806
- }
807
- async function assertNotSoftDeleted(db, table, data = {}, connection, deletedAtColumn = "deleted_at") {
808
- const t = softDeleteTarget(db, table, data, connection, deletedAtColumn);
809
- const count = await constrain(t.connection.table(t.table), t.data).whereNull(t.column).count();
810
- assert(count > 0, `Failed asserting that any existing row in the table [${t.table}] matches the attributes ${JSON.stringify(t.data, null, 2)}.`);
811
- }
812
- var RefreshDatabaseState = { migrated: false };
813
- async function runCommand(app, command, parameters = {}) {
814
- if (!app.bound(ConsoleKernel)) throw new Error(`Running [${command}] needs the Console component (ConsoleKernel is not bound).`);
815
- const kernel = app.make(ConsoleKernel);
816
- const code = await kernel.call(command, parameters);
817
- if (code !== 0) throw new Error(`[${command}] failed with exit code ${code}:
818
- ${kernel.output()}`);
819
- }
820
- function seedOptions(test) {
821
- if (test.seeder !== void 0) return { "--seeder": test.seeder };
822
- return test.seed === true ? { "--seed": true } : {};
823
- }
824
- function usingInMemoryDatabase(app) {
825
- const config = app.make(ConfigRepository);
826
- const name = config.get("database.default");
827
- return name !== void 0 && config.get(`database.connections.${name}.database`) === ":memory:";
828
- }
829
- function connectionsOf(test) {
830
- const db = test.app.make(DatabaseManager);
831
- return (test.connectionsToTransact ?? [void 0]).map((name) => db.connection(name));
832
- }
833
- async function beginTransactions(test, connections) {
834
- const levels = connections.map((connection) => connection.transactionLevel());
835
- for (const connection of connections) await connection.beginTransaction();
836
- test.beforeApplicationDestroyed(async () => {
837
- for (const [index, connection] of [...connections.entries()].reverse()) await rollBackTo(connection, levels[index] ?? 0);
838
- });
839
- }
840
- async function rollBackTo(connection, level) {
841
- let current = connection.transactionLevel();
842
- while (current > level) {
843
- await connection.rollBack();
844
- const next = connection.transactionLevel();
845
- if (next >= current) return;
846
- current = next;
847
- }
848
- }
849
- var RefreshDatabase = {
850
- async setUp(test) {
851
- if (usingInMemoryDatabase(test.app)) return runCommand(test.app, "migrate", seedOptions(test));
852
- if (!RefreshDatabaseState.migrated) {
853
- if (test.migrateFreshUsing !== void 0) await test.migrateFreshUsing();
854
- else await runCommand(test.app, "migrate:fresh", seedOptions(test));
855
- RefreshDatabaseState.migrated = true;
856
- }
857
- await beginTransactions(test, connectionsOf(test));
858
- }
859
- };
860
- var DatabaseMigrations = {
861
- async setUp(test) {
862
- await runCommand(test.app, "migrate:fresh", seedOptions(test));
863
- test.beforeApplicationDestroyed(() => runCommand(test.app, "migrate:rollback"));
864
- }
865
- };
866
- var DatabaseTransactions = {
867
- async setUp(test) {
868
- await beginTransactions(test, connectionsOf(test));
869
- }
870
- };
871
- var TESTING_ENVIRONMENT = Object.freeze({
872
- APP_ENV: "testing",
873
- BCRYPT_ROUNDS: "4",
874
- CACHE_STORE: "array",
875
- DB_CONNECTION: "sqlite",
876
- DB_DATABASE: ":memory:",
877
- MAIL_MAILER: "array",
878
- QUEUE_CONNECTION: "sync",
879
- SESSION_DRIVER: "array"
880
- });
881
- function applyEnvironment(values, env2 = process.env) {
882
- for (const [key, value] of Object.entries(values)) env2[key] ??= value;
883
- }
884
- function clonePlain2(value) {
885
- if (Array.isArray(value)) return value.map((item) => clonePlain2(item));
886
- if (typeof value === "object" && value !== null) {
887
- const proto = Object.getPrototypeOf(value);
888
- if (proto !== Object.prototype && proto !== null) return value;
889
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clonePlain2(item)]));
890
- }
891
- return value;
892
- }
893
- function typeOf(value) {
894
- if (value === null) return ["null"];
895
- if (Array.isArray(value)) return ["array"];
896
- switch (typeof value) {
897
- case "string":
898
- return ["string"];
899
- case "boolean":
900
- return ["boolean"];
901
- case "number":
902
- return Number.isInteger(value) ? ["integer", "number"] : ["double", "float", "number"];
903
- case "object":
904
- return ["array", "object"];
905
- default:
906
- return [typeof value];
907
- }
908
- }
909
- var AssertableJson = class _AssertableJson {
910
- constructor(props, path = void 0) {
911
- this.props = props;
912
- this.path = path;
913
- }
914
- props;
915
- path;
916
- interactedKeys = [];
917
- static fromJson(json) {
918
- return new _AssertableJson(json);
919
- }
920
- /** A child scope of the same class (subclasses override to keep their extra state out of scopes). */
921
- newScope(props, path) {
922
- return new _AssertableJson(props, path);
923
- }
924
- dotPath(key = "") {
925
- if (this.path === void 0) return key;
926
- return key === "" ? this.path : `${this.path}.${key}`;
927
- }
928
- /** The value at `key` in this scope, or the whole scope. */
929
- prop(key) {
930
- if (key !== void 0 && this.hasOwnKey(key)) return this.props[key];
931
- return Arr.get(this.props, key);
932
- }
933
- /** A literal key of this scope wins over a dot path, like Laravel's Arr::get (keys such as 'items.0.name'). */
934
- hasOwnKey(key) {
935
- return typeof this.props === "object" && this.props !== null && Object.hasOwn(this.props, key);
936
- }
937
- exists(key) {
938
- return this.hasOwnKey(key) || Arr.has(this.props, key);
939
- }
940
- interactsWith(key) {
941
- const prop = this.hasOwnKey(String(key)) ? String(key) : String(key).split(".")[0];
942
- if (!this.interactedKeys.includes(prop)) this.interactedKeys.push(prop);
943
- }
944
- /** Every property of the scope was checked (called when a scope ends). */
945
- interacted() {
946
- const props = this.prop();
947
- const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
948
- const unexpected = keys.filter((key) => !this.interactedKeys.includes(key));
949
- assert(
950
- unexpected.length === 0,
951
- this.path === void 0 ? "Unexpected properties were found on the root level." : `Unexpected properties were found in scope [${this.path}].`,
952
- { actual: unexpected, expected: [] }
953
- );
954
- }
955
- /** Allow the properties this scope didn't check. */
956
- etc() {
957
- const props = this.prop();
958
- this.interactedKeys = typeof props === "object" && props !== null ? Object.keys(props) : [];
959
- return this;
960
- }
961
- // Has ------------------------------------------------------------------------------------------------------
962
- /** has('key'), has('key', 3), has('key', json => …), has('key', 3, first => …), or has(3) for the root size. */
963
- has(key, length, callback) {
964
- if (typeof key === "number" && length === void 0) return this.count(key);
965
- const name = String(key);
966
- assert(this.exists(name), `Property [${this.dotPath(name)}] does not exist.`);
967
- this.interactsWith(name);
968
- if (callback !== void 0) {
969
- return this.has(name, (scope) => {
970
- if (typeof length === "number") scope.count(length);
971
- scope.first(callback).etc();
972
- });
973
- }
974
- if (typeof length === "function") return this.scope(name, length);
975
- if (typeof length === "number") return this.count(name, length);
976
- return this;
977
- }
978
- /** hasAll(['a', 'b']) or hasAll({ items: 3 }). */
979
- hasAll(keys) {
980
- if (typeof keys === "string" || Array.isArray(keys)) {
981
- for (const key of [keys].flat()) this.has(key);
982
- return this;
983
- }
984
- for (const [key, length] of Object.entries(keys)) this.has(key, length);
985
- return this;
986
- }
987
- hasAny(keys) {
988
- const list = [keys].flat();
989
- const present = list.filter((key) => this.exists(key));
990
- assert(present.length > 0, `None of properties [${list.map((key) => this.dotPath(key)).join(", ")}] exist.`);
991
- for (const key of present) this.interactsWith(key);
992
- return this;
993
- }
994
- missing(key) {
995
- assert(!this.exists(key), `Property [${this.dotPath(key)}] was found while it was expected to be missing.`);
996
- return this;
997
- }
998
- missingAll(keys) {
999
- for (const key of [keys].flat()) this.missing(key);
1000
- return this;
1001
- }
1002
- /** count(3) checks this scope's size; count('items', 3) a property's. */
1003
- count(key, length) {
1004
- if (typeof key === "number") {
1005
- const size2 = sizeOf(this.prop());
1006
- assert(size2 === key, this.path === void 0 ? "Root level does not have the expected size." : `Property [${this.path}] does not have the expected size.`, { actual: size2, expected: key });
1007
- return this;
1008
- }
1009
- this.has(key);
1010
- const size = sizeOf(this.prop(key));
1011
- assert(size === length, `Property [${this.dotPath(key)}] does not have the expected size.`, { actual: size, expected: length });
1012
- return this;
1013
- }
1014
- countBetween(key, min, max) {
1015
- this.has(key);
1016
- const size = sizeOf(this.prop(key));
1017
- assert(size >= min && size <= max, `Property [${this.dotPath(key)}] size is not between ${min} and ${max}.`, { actual: size });
1018
- return this;
1019
- }
1020
- // Matching -------------------------------------------------------------------------------------------------
1021
- /** where('name', 'Ann'), or where('name', value => value.startsWith('A')). Objects compare ignoring key order. */
1022
- where(key, expected) {
1023
- this.has(key);
1024
- const actual = this.prop(key);
1025
- if (typeof expected === "function") {
1026
- assert(expected(actual), `Property [${this.dotPath(key)}] was marked as invalid using a closure.`);
1027
- return this;
1028
- }
1029
- const normalized = toPlain(expected);
1030
- assert(equals(normalized, actual), `Property [${this.dotPath(key)}] does not match the expected value.`, { actual, expected: normalized });
1031
- return this;
1032
- }
1033
- whereNot(key, expected) {
1034
- this.has(key);
1035
- const actual = this.prop(key);
1036
- if (typeof expected === "function") {
1037
- assert(!expected(actual), `Property [${this.dotPath(key)}] was marked as invalid using a closure.`);
1038
- return this;
1039
- }
1040
- assert(!equals(toPlain(expected), actual), `Property [${this.dotPath(key)}] contains a value that should be missing: [${key}, ${format(expected)}]`);
1041
- return this;
1042
- }
1043
- whereNull(key) {
1044
- this.has(key);
1045
- const actual = this.prop(key);
1046
- assert(actual === null, `Property [${this.dotPath(key)}] should be null.`, { actual, expected: null });
1047
- return this;
1048
- }
1049
- whereNotNull(key) {
1050
- this.has(key);
1051
- assert(this.prop(key) !== null, `Property [${this.dotPath(key)}] should not be null.`);
1052
- return this;
1053
- }
1054
- whereAll(bindings) {
1055
- for (const [key, value] of Object.entries(bindings)) this.where(key, value);
1056
- return this;
1057
- }
1058
- /** whereType('id', 'integer'), whereType('name', 'string|null'), whereType('tags', ['array', 'null']). */
1059
- whereType(key, expected) {
1060
- this.has(key);
1061
- const types = typeof expected === "string" ? expected.split("|") : expected;
1062
- const actual = typeOf(this.prop(key));
1063
- assert(
1064
- types.some((type) => actual.includes(type)),
1065
- `Property [${this.dotPath(key)}] is not of expected type [${types.join("|")}].`,
1066
- { actual: actual[0], expected: types.join("|") }
1067
- );
1068
- return this;
1069
- }
1070
- whereAllType(bindings) {
1071
- for (const [key, type] of Object.entries(bindings)) this.whereType(key, type);
1072
- return this;
1073
- }
1074
- /** The list contains every expected value (or one passing the callback); a string contains the substring. */
1075
- whereContains(key, expected) {
1076
- this.has(key);
1077
- const actual = this.prop(key);
1078
- if (typeof actual === "string") {
1079
- assert(actual.includes(String(expected)), `Property [${this.dotPath(key)}] does not contain [${format(expected)}].`);
1080
- return this;
1081
- }
1082
- const values = typeof actual === "object" && actual !== null ? Object.values(actual) : [actual];
1083
- const wanted = Array.isArray(expected) ? expected : [expected];
1084
- const missing = wanted.filter(
1085
- (item) => typeof item === "function" ? !values.some((value) => item(value)) : !values.some((value) => equals(toPlain(item), value))
1086
- );
1087
- assert(missing.length === 0, `Property [${this.dotPath(key)}] does not contain [${missing.map(format).join(", ")}].`, { actual });
1088
- return this;
1089
- }
1090
- // Scoping --------------------------------------------------------------------------------------------------
1091
- scope(key, callback) {
1092
- const props = this.prop(key);
1093
- const path = this.dotPath(key);
1094
- assert(typeof props === "object" && props !== null, `Property [${path}] is not scopeable.`);
1095
- const scope = this.newScope(props, path);
1096
- callback(scope);
1097
- scope.interacted();
1098
- return this;
1099
- }
1100
- first(callback) {
1101
- const props = this.prop();
1102
- const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
1103
- if (keys.length === 0) {
1104
- fail(this.path === void 0 ? "Cannot scope directly onto the first element of the root level because it is empty." : `Cannot scope directly onto the first element of property [${this.path}] because it is empty.`);
1105
- }
1106
- this.interactsWith(keys[0]);
1107
- return this.scope(keys[0], callback);
1108
- }
1109
- each(callback) {
1110
- const props = this.prop();
1111
- const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
1112
- if (keys.length === 0) {
1113
- fail(this.path === void 0 ? "Cannot scope directly onto each element of the root level because it is empty." : `Cannot scope directly onto each element of property [${this.path}] because it is empty.`);
1114
- }
1115
- for (const key of keys) {
1116
- this.interactsWith(key);
1117
- this.scope(key, callback);
1118
- }
1119
- return this;
1120
- }
1121
- // Helpers --------------------------------------------------------------------------------------------------
1122
- when(condition, callback, otherwise) {
1123
- if (condition) callback(this);
1124
- else otherwise?.(this);
1125
- return this;
1126
- }
1127
- dump(key) {
1128
- console.log(format(this.prop(key)));
1129
- return this;
1130
- }
1131
- toArray() {
1132
- return this.prop();
1133
- }
1134
- };
1135
- function sizeOf(value) {
1136
- if (Array.isArray(value)) return value.length;
1137
- if (typeof value === "object" && value !== null) return Object.keys(value).length;
1138
- if (typeof value === "string") return value.length;
1139
- return 0;
1140
- }
1141
- function toPlain(value) {
1142
- return value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
1143
- }
1144
- function inertiaPageOf(response) {
1145
- let page;
1146
- try {
1147
- if (response.headers.has("X-Inertia")) page = JSON.parse(response.content);
1148
- else {
1149
- const match = /<script data-page="[^"]*" type="application\/json">([\s\S]*?)<\/script>/.exec(response.content);
1150
- page = match === null ? void 0 : JSON.parse(match[1]);
1151
- }
1152
- } catch {
1153
- page = void 0;
1154
- }
1155
- const valid = typeof page === "object" && page !== null && ["component", "props", "url", "version"].every((key) => Object.hasOwn(page, key));
1156
- if (!valid) fail("Not a valid Inertia response.");
1157
- return page;
1158
- }
1159
- var AssertableInertia = class _AssertableInertia extends AssertableJson {
1160
- #page;
1161
- #response;
1162
- static fromTestResponse(response) {
1163
- const page = inertiaPageOf(response);
1164
- const instance = new _AssertableInertia(page.props);
1165
- instance.#page = page;
1166
- instance.#response = response;
1167
- return instance;
1168
- }
1169
- /** Scopes are plain AssertableJson: the page-level methods only make sense at the root. */
1170
- newScope(props, path) {
1171
- return new AssertableJson(props, path);
1172
- }
1173
- get page() {
1174
- if (this.#page === void 0) fail("This assertion is only available on the page root.");
1175
- return this.#page;
1176
- }
1177
- /** The component name; the page file must exist unless `shouldExist` is false (inertia.testing.ensure_pages_exist). */
1178
- component(value, shouldExist) {
1179
- const actual = this.page.component;
1180
- assert(actual === value, "Unexpected Inertia page component.", { actual, expected: value });
1181
- const app = this.#app();
1182
- const config = app?.bound(ConfigRepository) ? app.make(ConfigRepository) : void 0;
1183
- if (shouldExist === true || shouldExist === void 0 && config?.get("inertia.testing.ensure_pages_exist", true) !== false) {
1184
- const paths = config?.get("inertia.pages.paths", []) ?? [];
1185
- const extensions = config?.get("inertia.pages.extensions", []) ?? [];
1186
- const exists = paths.some((path) => extensions.some((extension) => existsSync4(join5(path, `${value}.${extension}`))));
1187
- if (!exists) fail(`Inertia page component file [${value}] does not exist.`);
1188
- }
1189
- return this;
1190
- }
1191
- url(value) {
1192
- assert(this.page.url === value, "Unexpected Inertia page url.", { actual: this.page.url, expected: value });
1193
- return this;
1194
- }
1195
- version(value) {
1196
- assert(this.page.version === value, "Unexpected Inertia asset version.", { actual: this.page.version, expected: value });
1197
- return this;
1198
- }
1199
- /** The flash data has `key` (dot paths allowed), equal to `expected` when given. */
1200
- hasFlash(key, ...expected) {
1201
- _AssertableInertia.assertFlashHas(this.page.flash ?? {}, key, ...expected);
1202
- return this;
1203
- }
1204
- missingFlash(key) {
1205
- _AssertableInertia.assertFlashMissing(this.page.flash ?? {}, key);
1206
- return this;
1207
- }
1208
- static assertFlashHas(flash, key, ...expected) {
1209
- assert(Arr.has(flash, key), `Inertia Flash Data is missing key [${key}].`);
1210
- if (expected.length > 0) {
1211
- const actual = Arr.get(flash, key);
1212
- assert(equals(actual, expected[0]), `Inertia Flash Data [${key}] does not match expected value.`, { actual, expected: expected[0] });
1213
- }
1214
- }
1215
- static assertFlashMissing(flash, key) {
1216
- assert(!Arr.has(flash, key), `Inertia Flash Data has unexpected key [${key}].`);
1217
- }
1218
- /**
1219
- * Requests the page again as a partial reload (only/except) and checks it is the same page. Uses the test's
1220
- * HTTP client, so the session and cookies carry over.
1221
- */
1222
- async reload(callback, only, except) {
1223
- const client = this.#response?.context.client;
1224
- if (client === void 0) fail("reload() needs a response from the test HTTP client.");
1225
- const headers = {
1226
- "X-Inertia": "true",
1227
- "X-Requested-With": "XMLHttpRequest",
1228
- "X-Inertia-Version": this.page.version
1229
- };
1230
- const onlyList = only === void 0 ? void 0 : [only].flat().join(",");
1231
- const exceptList = except === void 0 ? void 0 : [except].flat().join(",");
1232
- if (onlyList !== void 0 || exceptList !== void 0) headers["X-Inertia-Partial-Component"] = this.page.component;
1233
- if (onlyList !== void 0) headers["X-Inertia-Partial-Data"] = onlyList;
1234
- if (exceptList !== void 0) headers["X-Inertia-Partial-Except"] = exceptList;
1235
- const response = await client.request("GET", this.page.url, { headers });
1236
- const reloaded = _AssertableInertia.fromTestResponse(response);
1237
- reloaded.component(this.page.component, false).url(this.page.url).version(this.page.version);
1238
- await callback?.(reloaded);
1239
- return this;
1240
- }
1241
- reloadOnly(only, callback) {
1242
- return this.reload(
1243
- async (page) => {
1244
- page.hasAll(typeof only === "string" ? only.split(",") : only);
1245
- await callback?.(page);
1246
- },
1247
- only
1248
- );
1249
- }
1250
- reloadExcept(except, callback) {
1251
- return this.reload(
1252
- async (page) => {
1253
- page.missingAll(typeof except === "string" ? except.split(",") : except);
1254
- await callback?.(page);
1255
- },
1256
- void 0,
1257
- except
1258
- );
1259
- }
1260
- /** Loads the deferred props of every group (or the given groups) in one partial reload. */
1261
- loadDeferredProps(groupsOrCallback, callback) {
1262
- const deferred = this.page.deferredProps ?? {};
1263
- const groups = typeof groupsOrCallback === "function" ? Object.keys(deferred) : [groupsOrCallback].flat();
1264
- const props = groups.flatMap((group) => deferred[group] ?? []);
1265
- return this.reloadOnly(props.join(","), typeof groupsOrCallback === "function" ? groupsOrCallback : callback);
1266
- }
1267
- toArray() {
1268
- return {
1269
- component: this.page.component,
1270
- props: this.prop(),
1271
- url: this.page.url,
1272
- version: this.page.version,
1273
- flash: this.page.flash ?? {},
1274
- ...this.page.encryptHistory === true ? { encryptHistory: true } : {},
1275
- ...this.page.clearHistory === true ? { clearHistory: true } : {}
1276
- };
1277
- }
1278
- #app() {
1279
- return this.#response?.context.app;
1280
- }
1281
- };
1282
- function cookiePrefix(name, key) {
1283
- return `${createHmac("sha1", key).update(`${name}v2`).digest("hex")}|`;
1284
- }
1285
- function encryptCookie(encrypter, name, value) {
1286
- return encrypter.encrypt(cookiePrefix(name, encrypter.getKey()) + value, false);
1287
- }
1288
- function decryptCookie(encrypter, name, value) {
1289
- try {
1290
- const plain = encrypter.decrypt(value, false);
1291
- const prefix = cookiePrefix(name, encrypter.getKey());
1292
- return plain.startsWith(prefix) ? plain.slice(prefix.length) : void 0;
1293
- } catch {
1294
- return void 0;
1295
- }
1296
- }
1297
- var decode = (text) => {
1298
- try {
1299
- return decodeURIComponent(text);
1300
- } catch {
1301
- return text;
1302
- }
1303
- };
1304
- function parseSetCookie(header) {
1305
- const [pair = "", ...attributes] = header.split(";");
1306
- const eq = pair.indexOf("=");
1307
- const cookie = {
1308
- name: decode(pair.slice(0, eq).trim()),
1309
- value: decode(pair.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1")),
1310
- httpOnly: false,
1311
- secure: false
1312
- };
1313
- for (const attribute of attributes) {
1314
- const at = attribute.indexOf("=");
1315
- const key = (at === -1 ? attribute : attribute.slice(0, at)).trim().toLowerCase();
1316
- const value = at === -1 ? "" : attribute.slice(at + 1).trim();
1317
- if (key === "path") cookie.path = value;
1318
- else if (key === "domain") cookie.domain = value;
1319
- else if (key === "max-age") cookie.maxAge = Number(value);
1320
- else if (key === "expires") cookie.expires = new Date(value);
1321
- else if (key === "httponly") cookie.httpOnly = true;
1322
- else if (key === "secure") cookie.secure = true;
1323
- else if (key === "samesite") cookie.sameSite = value;
1324
- }
1325
- return cookie;
1326
- }
1327
- function isExpired(cookie, now = Date.now()) {
1328
- if (cookie.maxAge !== void 0 && !Number.isNaN(cookie.maxAge)) return cookie.maxAge <= 0;
1329
- return cookie.expires !== void 0 && cookie.expires.getTime() <= now;
1330
- }
1331
- function responseCookies(response) {
1332
- return response.headers.getSetCookie().map(parseSetCookie);
1333
- }
1334
- function cookieHeader(cookies) {
1335
- return [...cookies].map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("; ");
1336
- }
1337
- var REDIRECTS = /* @__PURE__ */ new Set([201, 301, 302, 303, 307, 308]);
1338
- var TestResponse = class _TestResponse {
1339
- constructor(baseResponse, content, context = {}) {
1340
- this.baseResponse = baseResponse;
1341
- this.content = content;
1342
- this.context = context;
1343
- }
1344
- baseResponse;
1345
- content;
1346
- context;
1347
- decoded;
1348
- /** Reads the body once, so every assertion is synchronous. */
1349
- static async from(response, context = {}) {
1350
- return new _TestResponse(response, await response.text(), context);
1351
- }
1352
- get status() {
1353
- return this.baseResponse.status;
1354
- }
1355
- get headers() {
1356
- return this.baseResponse.headers;
1357
- }
1358
- /** The error the exception handler rendered for this request, if any. */
1359
- get exception() {
1360
- return this.context.exception;
1361
- }
1362
- getContent() {
1363
- return this.content;
1364
- }
1365
- /** The decoded JSON body, or the value at a dot path (`data.0.id`). */
1366
- json(path) {
1367
- if (this.decoded === void 0) {
1368
- try {
1369
- this.decoded = { value: JSON.parse(this.content) };
1370
- } catch {
1371
- fail(`Invalid JSON was returned from the route.
1372
-
1373
- ${this.excerpt()}`);
1374
- }
1375
- }
1376
- return Arr.get(this.decoded.value, path);
1377
- }
1378
- dump(path) {
1379
- console.log(path === void 0 ? this.content : this.json(path));
1380
- return this;
1381
- }
1382
- // Status ------------------------------------------------------------------------------------------------
1383
- assertStatus(status) {
1384
- if (this.status !== status) fail(this.statusMessage(`Expected response status code [${status}] but received ${this.status}.`), { actual: this.status, expected: status });
1385
- return this;
1386
- }
1387
- assertOk() {
1388
- return this.assertStatus(200);
1389
- }
1390
- assertCreated() {
1391
- return this.assertStatus(201);
1392
- }
1393
- assertAccepted() {
1394
- return this.assertStatus(202);
1395
- }
1396
- assertMovedPermanently() {
1397
- return this.assertStatus(301);
1398
- }
1399
- assertFound() {
1400
- return this.assertStatus(302);
1401
- }
1402
- assertNotModified() {
1403
- return this.assertStatus(304);
1404
- }
1405
- assertBadRequest() {
1406
- return this.assertStatus(400);
1407
- }
1408
- assertUnauthorized() {
1409
- return this.assertStatus(401);
1410
- }
1411
- assertPaymentRequired() {
1412
- return this.assertStatus(402);
1413
- }
1414
- assertForbidden() {
1415
- return this.assertStatus(403);
1416
- }
1417
- assertNotFound() {
1418
- return this.assertStatus(404);
1419
- }
1420
- assertMethodNotAllowed() {
1421
- return this.assertStatus(405);
1422
- }
1423
- assertNotAcceptable() {
1424
- return this.assertStatus(406);
1425
- }
1426
- assertRequestTimeout() {
1427
- return this.assertStatus(408);
1428
- }
1429
- assertConflict() {
1430
- return this.assertStatus(409);
1431
- }
1432
- assertGone() {
1433
- return this.assertStatus(410);
1434
- }
1435
- assertUnsupportedMediaType() {
1436
- return this.assertStatus(415);
1437
- }
1438
- assertUnprocessable() {
1439
- return this.assertStatus(422);
1440
- }
1441
- assertTooManyRequests() {
1442
- return this.assertStatus(429);
1443
- }
1444
- assertInternalServerError() {
1445
- return this.assertStatus(500);
1446
- }
1447
- assertServiceUnavailable() {
1448
- return this.assertStatus(503);
1449
- }
1450
- /** 204 (or `status`) with an empty body. */
1451
- assertNoContent(status = 204) {
1452
- this.assertStatus(status);
1453
- assert(this.content === "", "Response content is not empty.", { actual: this.content, expected: "" });
1454
- return this;
1455
- }
1456
- assertSuccessful() {
1457
- assert(this.status >= 200 && this.status < 300, () => this.statusMessage(`Expected response status code [>=200, <300] but received ${this.status}.`));
1458
- return this;
1459
- }
1460
- assertClientError() {
1461
- assert(this.status >= 400 && this.status < 500, () => this.statusMessage(`Expected response status code [>=400, <500] but received ${this.status}.`));
1462
- return this;
1463
- }
1464
- assertServerError() {
1465
- assert(this.status >= 500 && this.status < 600, () => this.statusMessage(`Expected response status code [>=500, <600] but received ${this.status}.`));
1466
- return this;
1467
- }
1468
- // Redirects ---------------------------------------------------------------------------------------------
1469
- /** A redirect status (201, 301, 302, 303, 307, 308); with `uri`, the Location must match it. */
1470
- assertRedirect(uri) {
1471
- assert(REDIRECTS.has(this.status), () => this.statusMessage(`Expected response status code [201, 301, 302, 303, 307, 308] but received ${this.status}.`));
1472
- if (uri !== void 0) this.assertLocation(uri);
1473
- return this;
1474
- }
1475
- assertRedirectContains(uri) {
1476
- this.assertRedirect();
1477
- const location = this.headers.get("Location") ?? "";
1478
- assert(location.includes(uri), `Redirect location [${location}] does not contain [${uri}].`);
1479
- return this;
1480
- }
1481
- /** Relative and absolute forms of the same URL are equal (both are resolved against the request URL). */
1482
- assertLocation(uri) {
1483
- const actual = this.absolute(this.headers.get("Location") ?? "");
1484
- const expected = this.absolute(uri);
1485
- assert(actual === expected, `Failed asserting that the redirect location [${actual}] equals [${expected}].`, { actual, expected });
1486
- return this;
1487
- }
1488
- // Headers -----------------------------------------------------------------------------------------------
1489
- assertHeader(name, value) {
1490
- const actual = this.headers.get(name);
1491
- assert(actual !== null, `Header [${name}] not present on response.`);
1492
- if (value !== void 0) assert(actual === value, `Header [${name}] was found, but value [${actual}] does not match [${value}].`, { actual, expected: value });
1493
- return this;
1494
- }
1495
- assertHeaderMissing(name) {
1496
- assert(!this.headers.has(name), `Unexpected header [${name}] is present on response.`);
1497
- return this;
1498
- }
1499
- /** Content-Disposition: attachment (optionally with this file name). */
1500
- assertDownload(filename) {
1501
- const disposition = this.headers.get("Content-Disposition") ?? "";
1502
- assert(/^attachment/i.test(disposition), "Response does not offer a file download.");
1503
- if (filename !== void 0) {
1504
- assert(disposition.includes(`filename="${filename}"`) || disposition.includes(`filename=${filename}`), `Expected file [${filename}] is not present in Content-Disposition header [${disposition}].`);
1505
- }
1506
- return this;
1507
- }
1508
- // Content -----------------------------------------------------------------------------------------------
1509
- assertContent(value) {
1510
- assert(this.content === value, "Response content does not match.", { actual: this.content, expected: value });
1511
- return this;
1512
- }
1513
- /** The content contains the value(s); values are HTML-escaped first unless `escape` is false (Laravel). */
1514
- assertSee(value, escape = true) {
1515
- for (const needle of this.needles(value, escape)) {
1516
- assert(this.content.includes(needle), `Failed asserting that the response contains [${needle}].
1517
-
1518
- ${this.excerpt()}`);
1519
- }
1520
- return this;
1521
- }
1522
- assertDontSee(value, escape = true) {
1523
- for (const needle of this.needles(value, escape)) {
1524
- assert(!this.content.includes(needle), `Failed asserting that the response does not contain [${needle}].
1525
-
1526
- ${this.excerpt()}`);
1527
- }
1528
- return this;
1529
- }
1530
- assertSeeInOrder(values, escape = true) {
1531
- return this.inOrder(this.content, this.needles(values, escape));
1532
- }
1533
- /** Like assertSee, against the content with its tags stripped. */
1534
- assertSeeText(value, escape = true) {
1535
- const text = stripTags(this.content);
1536
- for (const needle of this.needles(value, escape)) {
1537
- assert(text.includes(needle), `Failed asserting that the response text contains [${needle}].
1538
-
1539
- ${this.excerpt(text)}`);
1540
- }
1541
- return this;
1542
- }
1543
- assertDontSeeText(value, escape = true) {
1544
- const text = stripTags(this.content);
1545
- for (const needle of this.needles(value, escape)) {
1546
- assert(!text.includes(needle), `Failed asserting that the response text does not contain [${needle}].`);
1547
- }
1548
- return this;
1549
- }
1550
- assertSeeTextInOrder(values, escape = true) {
1551
- return this.inOrder(stripTags(this.content), this.needles(values, escape));
1552
- }
1553
- // JSON --------------------------------------------------------------------------------------------------
1554
- /**
1555
- * The response JSON contains `data` (a subset match: extra keys in the response are fine), or fluent
1556
- * assertions: assertJson(json => json.has('data', 3).etc()).
1557
- */
1558
- assertJson(data) {
1559
- if (typeof data === "function") {
1560
- const json = AssertableJson.fromJson(this.json());
1561
- data(json);
1562
- if (!Array.isArray(json.toArray())) json.interacted();
1563
- return this;
1564
- }
1565
- const actual = this.json();
1566
- assert(isSubset(data, actual), "Unable to find the expected JSON subset in the response.", { actual, expected: data });
1567
- return this;
1568
- }
1569
- assertInertia(callback) {
1570
- const page = AssertableInertia.fromTestResponse(this);
1571
- const result = callback?.(page);
1572
- if (typeof result?.then === "function") return Promise.resolve(result).then(() => this);
1573
- return this;
1574
- }
1575
- /** The Inertia page object of this response. */
1576
- inertiaPage() {
1577
- return inertiaPageOf(this);
1578
- }
1579
- /** The page props, or one prop by dot path. */
1580
- inertiaProps(key) {
1581
- return Arr.get(inertiaPageOf(this).props, key);
1582
- }
1583
- /** Inertia flash data waiting in the session for the next page (after a redirect). */
1584
- assertInertiaFlash(key, ...expected) {
1585
- AssertableInertia.assertFlashHas(this.session().get("inertia.flash_data", {}), key, ...expected);
1586
- return this;
1587
- }
1588
- assertInertiaFlashMissing(key) {
1589
- AssertableInertia.assertFlashMissing(this.session().get("inertia.flash_data", {}), key);
1590
- return this;
1591
- }
1592
- /** The response JSON equals `data` exactly (object key order is ignored). */
1593
- assertExactJson(data) {
1594
- const actual = this.json();
1595
- assert(equals(actual, data), "Failed asserting that the response JSON matches exactly.", { actual, expected: data });
1596
- return this;
1597
- }
1598
- /** The value at `path` equals `expected` (strictly), or passes the predicate. */
1599
- assertJsonPath(path, expected) {
1600
- assert(Arr.has(this.json(), path), `Failed asserting that the JSON has the path [${path}].`, { actual: this.json() });
1601
- const actual = this.json(path);
1602
- if (typeof expected === "function") {
1603
- assert(expected(actual) === true, `Failed asserting that the value at [${path}] passes the given check (got ${format(actual)}).`);
1604
- } else {
1605
- assert(equals(actual, expected), `Failed asserting that the value at [${path}] equals ${format(expected)} (got ${format(actual)}).`, { actual, expected });
1606
- }
1607
- return this;
1608
- }
1609
- assertJsonMissingPath(path) {
1610
- assert(!Arr.has(this.json(), path), `Failed asserting that the JSON does not have the path [${path}].`);
1611
- return this;
1612
- }
1613
- /** Each key/value pair appears somewhere in the JSON (Laravel's assertJsonFragment). */
1614
- assertJsonFragment(data) {
1615
- const actual = this.json();
1616
- for (const [key, value] of Object.entries(data)) {
1617
- assert(containsPair(actual, key, value), `Unable to find JSON fragment ${format({ [key]: value })} in the response.`, { actual, expected: data });
1618
- }
1619
- return this;
1620
- }
1621
- /** Passes when at least one pair of `data` is absent (Laravel's assertJsonMissing). */
1622
- assertJsonMissing(data) {
1623
- const actual = this.json();
1624
- const allPresent = Object.entries(data).every(([key, value]) => containsPair(actual, key, value));
1625
- assert(!allPresent, `Found unexpected JSON fragment ${format(data)} in the response.`, { actual });
1626
- return this;
1627
- }
1628
- /** `['id', { author: ['name'] }]`, `{ data: { '*': ['id'] } }`; without a structure, just checks it's JSON. */
1629
- assertJsonStructure(structure) {
1630
- const data = this.json();
1631
- if (structure !== void 0) assertStructure(structure, data);
1632
- return this;
1633
- }
1634
- /** The JSON (or the value at `key`) has `count` elements. */
1635
- assertJsonCount(count, key) {
1636
- const value = this.json(key);
1637
- const actual = Array.isArray(value) ? value.length : typeof value === "object" && value !== null ? Object.keys(value).length : -1;
1638
- assert(actual === count, `Failed to assert that the response ${key === void 0 ? "" : `[${key}] `}count matched the expected ${count} (got ${actual}).`, { actual, expected: count });
1639
- return this;
1640
- }
1641
- /** assertRedirect() to the URL of a named route (resolved with the app's UrlGenerator). */
1642
- assertRedirectToRoute(name, parameters) {
1643
- const app = this.context.app;
1644
- assert(app !== void 0 && app.bound(UrlGenerator), "assertRedirectToRoute() needs an application with a UrlGenerator (the Routing component).");
1645
- return this.assertRedirect(app.make(UrlGenerator).route(name, parameters));
1646
- }
1647
- // Validation --------------------------------------------------------------------------------------------
1648
- /** The JSON body has validation errors for these keys (and messages containing these fragments). */
1649
- assertJsonValidationErrors(errors, responseKey = "errors") {
1650
- const actual = this.json(responseKey);
1651
- assert(typeof actual === "object" && actual !== null, `Failed to find validation errors in the response under [${responseKey}].
1652
-
1653
- ${this.excerpt()}`);
1654
- this.assertErrorsContain(actual, errors, "response");
1655
- return this;
1656
- }
1657
- /** No validation errors in the JSON (or none for these keys). */
1658
- assertJsonMissingValidationErrors(keys, responseKey = "errors") {
1659
- if (this.content === "") return this;
1660
- const errors = Arr.get(this.json(), responseKey);
1661
- if (errors === void 0 || errors === null) return this;
1662
- const unexpected = keys === void 0 ? Object.keys(errors) : [keys].flat().filter((key) => Object.hasOwn(errors, key));
1663
- assert(unexpected.length === 0, `Found unexpected validation error(s) for [${unexpected.join(", ")}]: ${format(errors)}`);
1664
- return this;
1665
- }
1666
- /** No validation errors: JSON responses are checked in the body, others in the session. */
1667
- assertValid(keys, errorBag = "default", responseKey = "errors") {
1668
- if (this.isJson()) return this.assertJsonMissingValidationErrors(keys, responseKey);
1669
- if (this.context.session?.has("errors") !== true) return this;
1670
- return keys === void 0 ? this.assertSessionDoesntHaveErrors([], void 0, errorBag) : this.assertSessionDoesntHaveErrors(keys, void 0, errorBag);
1671
- }
1672
- /** Validation errors for these keys (message fragments match with "contains"): JSON body or session. */
1673
- assertInvalid(errors, errorBag = "default", responseKey = "errors") {
1674
- if (this.isJson()) return this.assertJsonValidationErrors(errors, responseKey);
1675
- this.assertSessionHas("errors");
1676
- this.assertErrorsContain(this.sessionErrors(errorBag), errors, "session");
1677
- return this;
1678
- }
1679
- // Session -----------------------------------------------------------------------------------------------
1680
- /** The request's session after the response (flashed data is still there). */
1681
- session() {
1682
- const session = this.context.session;
1683
- assert(session !== void 0, "The response has no session: is the Session component registered?");
1684
- return session;
1685
- }
1686
- /** A key (with an optional value or predicate), several keys, or a { key: value } record. */
1687
- assertSessionHas(key, value) {
1688
- if (Array.isArray(key)) {
1689
- for (const k of key) this.assertSessionHas(k);
1690
- return this;
1691
- }
1692
- if (typeof key === "object") return this.assertSessionHasAll(key);
1693
- const session = this.session();
1694
- if (value === void 0) {
1695
- assert(session.has(key), `Session is missing expected key [${key}].`);
1696
- } else if (typeof value === "function") {
1697
- assert(value(session.get(key)) === true, `Session value for [${key}] does not pass the given check.`);
1698
- } else {
1699
- assert(equals(session.get(key), value), `Session value for [${key}] does not match.`, { actual: session.get(key), expected: value });
1700
- }
1701
- return this;
1702
- }
1703
- assertSessionHasAll(bindings) {
1704
- for (const [key, value] of Object.entries(bindings)) this.assertSessionHas(key, value);
1705
- return this;
1706
- }
1707
- assertSessionMissing(key, value) {
1708
- const session = this.session();
1709
- for (const k of [key].flat()) {
1710
- if (value === void 0) assert(!session.has(k), `Session has unexpected key [${k}].`);
1711
- else assert(!equals(session.get(k), value), `Session has unexpected value for [${k}].`);
1712
- }
1713
- return this;
1714
- }
1715
- /** Old input flashed with the redirect (withInput()). */
1716
- assertSessionHasInput(key, value) {
1717
- const session = this.session();
1718
- const entries = typeof key === "string" ? [[key, value]] : Array.isArray(key) ? key.map((k) => [k, void 0]) : Object.entries(key);
1719
- for (const [k, expected] of entries) {
1720
- const actual = session.getOldInput(k);
1721
- assert(actual !== void 0, `Session is missing expected key [${k}] in the old input.`);
1722
- if (expected !== void 0) assert(equals(actual, expected), `Old input [${k}] does not match.`, { actual, expected });
1723
- }
1724
- return this;
1725
- }
1726
- /**
1727
- * The session has errors in `errorBag`: any at all, these keys, or these exact messages (Laravel's rules).
1728
- * `format` is a ':message' template applied to the actual messages first.
1729
- */
1730
- assertSessionHasErrors(keys = [], format2, errorBag = "default") {
1731
- this.assertSessionHas("errors");
1732
- const errors = this.formatted(this.sessionErrors(errorBag), format2);
1733
- const expected = typeof keys === "string" ? [keys] : keys;
1734
- if (Array.isArray(expected) && expected.length === 0) {
1735
- assert(Object.keys(errors).length > 0, `Session has no errors in the [${errorBag}] bag.`);
1736
- return this;
1737
- }
1738
- for (const [key, messages] of normalizeExpected(expected)) {
1739
- assert(Object.hasOwn(errors, key), `Session missing error: ${key}`, { actual: errors });
1740
- for (const message of messages) {
1741
- assert(errors[key].includes(message), `Session error for [${key}] does not include ${format2 === void 0 ? "" : "the formatted "}message ${JSON.stringify(message)}.`, { actual: errors[key], expected: message });
1742
- }
1743
- }
1744
- return this;
1745
- }
1746
- assertSessionHasErrorsIn(errorBag, keys = [], format2) {
1747
- return this.assertSessionHasErrors(keys, format2, errorBag);
1748
- }
1749
- /** No errors in any bag. */
1750
- assertSessionHasNoErrors() {
1751
- const bags = this.context.session?.get("errors");
1752
- const found = Object.fromEntries(Object.entries(bags ?? {}).filter(([, messages]) => Object.keys(messages ?? {}).length > 0));
1753
- assert(Object.keys(found).length === 0, `Session has unexpected errors:
1754
-
1755
- ${JSON.stringify(found, null, 2)}`, { actual: found, expected: {} });
1756
- return this;
1757
- }
1758
- /** No errors in the bag (or none for these keys). */
1759
- assertSessionDoesntHaveErrors(keys = [], format2, errorBag = "default") {
1760
- const errors = this.formatted(this.sessionErrors(errorBag), format2);
1761
- const list = [keys].flat();
1762
- if (list.length === 0) {
1763
- assert(Object.keys(errors).length === 0, `Session has unexpected errors:
1764
-
1765
- ${JSON.stringify(errors, null, 2)}`, { actual: errors, expected: {} });
1766
- return this;
1767
- }
1768
- for (const key of list) assert(!Object.hasOwn(errors, key), `Session has unexpected error: ${key}`, { actual: errors[key] });
1769
- return this;
1770
- }
1771
- // Cookies -----------------------------------------------------------------------------------------------
1772
- /** A cookie the response sets (the last Set-Cookie with that name). */
1773
- getCookie(name) {
1774
- return responseCookies(this.baseResponse).findLast((cookie) => cookie.name === name);
1775
- }
1776
- /** The cookie is set; with `value`, its (decrypted, unless `encrypted` is false) value matches. */
1777
- assertCookie(name, value, encrypted = true) {
1778
- const cookie = this.getCookie(name);
1779
- assert(cookie !== void 0, `Cookie [${name}] not present on response.`);
1780
- if (value === void 0) return this;
1781
- const actual = encrypted ? this.decrypted(cookie) : cookie.value;
1782
- assert(actual === value, `Cookie [${name}] was found, but value [${actual}] does not match [${value}].`, { actual, expected: value });
1783
- return this;
1784
- }
1785
- assertPlainCookie(name, value) {
1786
- return this.assertCookie(name, value, false);
1787
- }
1788
- assertCookieExpired(name) {
1789
- const cookie = this.getCookie(name);
1790
- assert(cookie !== void 0, `Cookie [${name}] not present on response.`);
1791
- assert(isExpired(cookie), `Cookie [${name}] is not expired.`);
1792
- return this;
1793
- }
1794
- assertCookieNotExpired(name) {
1795
- const cookie = this.getCookie(name);
1796
- assert(cookie !== void 0, `Cookie [${name}] not present on response.`);
1797
- assert(!isExpired(cookie), `Cookie [${name}] is expired.`);
1798
- return this;
1799
- }
1800
- assertCookieMissing(name) {
1801
- assert(this.getCookie(name) === void 0, `Cookie [${name}] is present on response.`);
1802
- return this;
1803
- }
1804
- // Views -------------------------------------------------------------------------------------------------
1805
- /**
1806
- * The view the route returned. A request can make other views too (a mailable's, an error page), so this is the
1807
- * last view whose rendered HTML is the response body, else the last view made.
1808
- */
1809
- get original() {
1810
- const views = this.context.views ?? [];
1811
- return views.findLast((view) => renderedHtml(view) === this.content) ?? views.at(-1);
1812
- }
1813
- assertViewIs(name) {
1814
- const view = this.ensureView();
1815
- assert(view.name === name, `Failed asserting that the response view [${view.name}] is [${name}].`, { actual: view.name, expected: name });
1816
- return this;
1817
- }
1818
- /** The view received `key` (dot paths allowed), optionally with this value or passing this predicate. */
1819
- assertViewHas(key, value) {
1820
- if (typeof key !== "string") return this.assertViewHasAll(key);
1821
- const data = this.viewDataOf(this.ensureView());
1822
- assert(Arr.has(data, key), `Failed asserting that the view has key [${key}].`, { actual: Object.keys(data) });
1823
- const actual = Arr.get(data, key);
1824
- if (typeof value === "function") assert(value(actual) === true, `View data [${key}] does not pass the given check.`);
1825
- else if (value !== void 0) assert(equals(actual, value), `View data [${key}] does not match the expected value.`, { actual, expected: value });
1826
- return this;
1827
- }
1828
- assertViewHasAll(bindings) {
1829
- if (Array.isArray(bindings)) for (const key of bindings) this.assertViewHas(key);
1830
- else for (const [key, value] of Object.entries(bindings)) this.assertViewHas(key, value);
1831
- return this;
1832
- }
1833
- assertViewMissing(key) {
1834
- assert(!Arr.has(this.viewDataOf(this.ensureView()), key), `Failed asserting that the view does not have key [${key}].`);
1835
- return this;
1836
- }
1837
- /** The data the view received (dot path), or all of it. */
1838
- viewData(key) {
1839
- return Arr.get(this.viewDataOf(this.ensureView()), key);
1840
- }
1841
- // Internals ---------------------------------------------------------------------------------------------
1842
- absolute(url) {
1843
- try {
1844
- return new URL(url, this.context.request?.url ?? "http://localhost").href;
1845
- } catch {
1846
- return url;
1847
- }
1848
- }
1849
- excerpt(text = this.content) {
1850
- const limit = 2e3;
1851
- return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
1852
- }
1853
- /** Adds the rendered exception (or the body) to a status failure, like Laravel does. */
1854
- statusMessage(message) {
1855
- const error = this.context.exception;
1856
- if (error instanceof Error) return `${message}
1857
-
1858
- The following exception occurred during the request:
1859
-
1860
- ${error.stack ?? error.message}`;
1861
- return this.content === "" ? message : `${message}
1862
-
1863
- ${this.excerpt()}`;
1864
- }
1865
- isJson() {
1866
- return (this.headers.get("Content-Type") ?? "").includes("json");
1867
- }
1868
- sessionErrors(errorBag) {
1869
- const bags = this.context.session?.get("errors");
1870
- return bags?.[errorBag] ?? {};
1871
- }
1872
- formatted(errors, template) {
1873
- if (template === void 0) return errors;
1874
- return Object.fromEntries(Object.entries(errors).map(([key, messages]) => [key, messages.map((m) => template.replaceAll(":message", m))]));
1875
- }
1876
- /** Laravel's assertInvalid/assertJsonValidationErrors: each key present; each expected fragment contained in a message. */
1877
- assertErrorsContain(errors, expected, where) {
1878
- for (const [key, fragments] of normalizeExpected(expected)) {
1879
- assert(Object.hasOwn(errors, key), `Failed to find a validation error in the ${where} for key: [${key}]`, { actual: errors });
1880
- const messages = [errors[key]].flat();
1881
- for (const fragment of fragments) {
1882
- assert(messages.some((message) => message.includes(fragment)), `Failed to find a validation error for key and message: [${key}] => [${fragment}]`, { actual: messages, expected: fragment });
1883
- }
1884
- }
1885
- }
1886
- ensureView() {
1887
- const view = this.original;
1888
- assert(view !== void 0, "The response is not a view.");
1889
- return view;
1890
- }
1891
- viewDataOf(view) {
1892
- const source = view;
1893
- return source.gatherData?.() ?? source.getData?.() ?? {};
1894
- }
1895
- decrypted(cookie) {
1896
- const app = this.context.app;
1897
- if (app === void 0 || !app.bound(Encrypter)) return cookie.value;
1898
- return decryptCookie(app.make(Encrypter), cookie.name, cookie.value);
1899
- }
1900
- needles(value, escape) {
1901
- return [value].flat().map((needle) => escape ? escapeHtml(needle) : needle);
1902
- }
1903
- inOrder(haystack, needles) {
1904
- let position = 0;
1905
- for (const needle of needles) {
1906
- const found = haystack.indexOf(needle, position);
1907
- if (found === -1) fail(`Failed asserting that the response contains ${format(needles)} in that order (missing or out of order: [${needle}]).`);
1908
- position = found + needle.length;
1909
- }
1910
- return this;
1911
- }
1912
- };
1913
- function normalizeExpected(expected) {
1914
- if (typeof expected === "string") return [[expected, []]];
1915
- if (Array.isArray(expected)) return expected.map((key) => [key, []]);
1916
- return Object.entries(expected).map(([key, messages]) => [key, [messages].flat()]);
1917
- }
1918
- function renderedHtml(view) {
1919
- try {
1920
- return view.toHtml();
1921
- } catch {
1922
- return void 0;
1923
- }
1924
- }
1925
- function forward(target2, overrides, before) {
1926
- return new Proxy(target2, {
1927
- get(object, property) {
1928
- before?.();
1929
- if (Object.hasOwn(overrides, property)) return overrides[property];
1930
- const value = Reflect.get(object, property, object);
1931
- return typeof value === "function" ? value.bind(object) : value;
1932
- }
1933
- });
1934
- }
1935
- function recordingViewFactory(factory, views) {
1936
- return forward(factory, {
1937
- make(name, data) {
1938
- const view = factory.make(name, data);
1939
- views.push(view);
1940
- return view;
1941
- }
1942
- });
1943
- }
1944
- function recordingExceptionHandler(handler, record) {
1945
- return forward(handler, {
1946
- async render(request, error) {
1947
- record(error);
1948
- return handler.render(request, error);
1949
- }
1950
- });
1951
- }
1952
- function actingAsAuthFactory(auth, users, defaultGuard, onApply) {
1953
- let applied = false;
1954
- const apply = () => {
1955
- if (applied) return;
1956
- applied = true;
1957
- if (defaultGuard !== void 0) auth.shouldUse(defaultGuard);
1958
- for (const [guard, user] of users) auth.guard(guard).setUser(user);
1959
- onApply?.();
1960
- };
1961
- return forward(auth, {}, apply);
1962
- }
1963
- var CLIENT_ADDRESS = /* @__PURE__ */ Symbol.for("oriel:http.client-address");
1964
- var MAX_REDIRECTS = 20;
1965
- var HttpClient = class {
1966
- constructor(app) {
1967
- this.app = app;
1968
- }
1969
- app;
1970
- headers = {};
1971
- /** Cookies to encrypt on send (withCookie) and to send as-is (withUnencryptedCookie, or captured from responses). */
1972
- cookies = /* @__PURE__ */ new Map();
1973
- /** actingAs() users by guard name (undefined = the default guard). */
1974
- users = /* @__PURE__ */ new Map();
1975
- followRedirects = false;
1976
- handleExceptions = true;
1977
- /** The guard actingAs(user, guard) made the default. */
1978
- defaultGuard;
1979
- /** Session attributes carried from one request to the next (Laravel keeps one session store per test). */
1980
- session;
1981
- /** from(url): the session's previous URL for the next request only (Laravel sets it once). */
1982
- previousUrl;
1983
- /** Requests whose actingAs users reached the guards (the AuthFactory they went through). */
1984
- actingAsApplied = /* @__PURE__ */ new WeakMap();
1985
- async request(method, uri, options = {}) {
1986
- let response = await this.send(this.buildRequest(method, uri, options));
1987
- if (!this.followRedirects) return response;
1988
- for (let hops = 0; isRedirect(response.status); hops++) {
1989
- if (hops >= MAX_REDIRECTS) fail(`Too many redirects (more than ${MAX_REDIRECTS}), last location [${response.headers.get("Location")}].`);
1990
- response = await this.send(this.buildRequest("GET", response.headers.get("Location") ?? "/", {}));
1991
- }
1992
- return response;
1993
- }
1994
- /** Runs `fn` in a scope prepared like a request's (carried session, actingAs users), e.g. for assertAuthenticated(). */
1995
- async inScope(fn) {
1996
- await this.app.boot();
1997
- const scope = this.app.createScope();
1998
- return scope.run(async () => {
1999
- await this.beforeRequest(scope, {});
2000
- return fn(scope);
2001
- });
2002
- }
2003
- /** Runs the request through the app's real HttpKernel, in a request scope this client owns. */
2004
- async send(request) {
2005
- const app = this.app;
2006
- await app.boot();
2007
- setClientAddress(request, "127.0.0.1");
2008
- const scope = app.createScope();
2009
- const context = { request, app, client: this };
2010
- const response = await scope.run(async () => {
2011
- await this.beforeRequest(scope, context);
2012
- this.previousUrl = void 0;
2013
- return scope.make(HttpKernel).handle(request);
2014
- });
2015
- await scope.run(() => this.afterRequest(scope, context));
2016
- this.rememberCookies(response);
2017
- await app.make(HttpKernel).terminate?.(request, response);
2018
- if (!this.handleExceptions && context.exception !== void 0) throw context.exception;
2019
- return TestResponse.from(response, context);
2020
- }
2021
- /** Prepares the request scope: recorders, the carried session and actingAs users. */
2022
- beforeRequest(scope, context) {
2023
- const views = context.views = [];
2024
- if (this.app.bound(ViewFactory)) scope.instance(ViewFactory, recordingViewFactory(this.app.make(ViewFactory), views));
2025
- if (this.app.bound(ExceptionHandler)) {
2026
- scope.instance(ExceptionHandler, recordingExceptionHandler(this.app.make(ExceptionHandler), (error) => context.exception = error));
2027
- }
2028
- const session = this.scopeSession(scope);
2029
- if (session !== void 0) {
2030
- if (this.session !== void 0) session.put(clonePlain2(this.session));
2031
- if (this.previousUrl !== void 0) session.setPreviousUrl(this.previousUrl);
2032
- context.session = session;
2033
- }
2034
- if ((this.users.size > 0 || this.defaultGuard !== void 0) && this.app.bound(AuthFactory)) {
2035
- const auth = this.app.make(AuthFactory);
2036
- const acting = actingAsAuthFactory(auth, this.users, this.defaultGuard, () => {
2037
- this.actingAsApplied.set(context, auth);
2038
- });
2039
- scope.instance(AuthFactory, acting);
2040
- const manager = auth.constructor;
2041
- if (typeof manager === "function" && manager !== Object && this.app.bound(manager)) {
2042
- scope.instance(manager, acting);
2043
- }
2044
- }
2045
- }
2046
- /** Keeps the session for the next request, like Laravel's per-test session store, and the guards' users. */
2047
- async afterRequest(_scope, context) {
2048
- if (context.session !== void 0) this.session = clonePlain2(context.session.all());
2049
- const auth = this.actingAsApplied.get(context);
2050
- if (auth !== void 0) await this.syncActingAs(auth);
2051
- }
2052
- /**
2053
- * Laravel's actingAs() sets the user on the guard once, so a logout (or a login as someone else) in the
2054
- * request sticks. Reads each actingAs guard back and forgets or replaces its user to match.
2055
- */
2056
- async syncActingAs(auth) {
2057
- for (const guard of [...this.users.keys()]) {
2058
- const user = await auth.guard(guard).user();
2059
- if (user === void 0) this.users.delete(guard);
2060
- else this.users.set(guard, user);
2061
- }
2062
- }
2063
- /** The request's (scoped) Session, created now so the carried attributes are in place before StartSession. */
2064
- scopeSession(scope) {
2065
- if (!scope.bound(Session)) return void 0;
2066
- try {
2067
- return scope.make(Session);
2068
- } catch {
2069
- return void 0;
2070
- }
2071
- }
2072
- buildRequest(method, uri, options) {
2073
- const verb = method.toUpperCase();
2074
- const url = new URL(uri, this.baseUrl());
2075
- const headers = new Headers({ ...this.headers, ...options.headers });
2076
- let body;
2077
- const data = options.data ?? {};
2078
- if (verb === "GET" || verb === "HEAD") {
2079
- for (const [key, value] of flatten(data)) if (typeof value === "string") url.searchParams.append(key, value);
2080
- } else if (options.json) {
2081
- body = JSON.stringify(data);
2082
- } else if (Object.keys(data).length > 0) {
2083
- const fields = flatten(data);
2084
- if (fields.some(([, value]) => typeof value !== "string")) {
2085
- const form = new FormData();
2086
- for (const [key, value] of fields) form.append(key, value);
2087
- body = form;
2088
- } else {
2089
- body = new URLSearchParams(fields);
2090
- }
2091
- }
2092
- if (options.json) {
2093
- if (body !== void 0) headers.set("Content-Type", "application/json");
2094
- headers.set("Accept", "application/json");
2095
- }
2096
- const cookies = this.cookieHeader();
2097
- if (cookies !== "") headers.set("Cookie", cookies);
2098
- return new Request(url, { method: verb, headers, body });
2099
- }
2100
- baseUrl() {
2101
- const configured = this.app.bound(ConfigRepository) ? this.app.make(ConfigRepository).get("app.url") : void 0;
2102
- return typeof configured === "string" && configured !== "" ? configured : "http://localhost";
2103
- }
2104
- cookieHeader() {
2105
- const encrypter = this.app.bound(Encrypter) ? this.app.make(Encrypter) : void 0;
2106
- const values = /* @__PURE__ */ new Map();
2107
- for (const [name, { value, encrypt }] of this.cookies) {
2108
- values.set(name, encrypt && encrypter !== void 0 ? encryptCookie(encrypter, name, value) : value);
2109
- }
2110
- return cookieHeader(values);
2111
- }
2112
- /** Like a browser: cookies the app sets are sent back on later requests in the same test; deleted ones are dropped. */
2113
- rememberCookies(response) {
2114
- for (const cookie of responseCookies(response)) {
2115
- if (isExpired(cookie)) this.cookies.delete(cookie.name);
2116
- else this.cookies.set(cookie.name, { value: cookie.value, encrypt: false });
2117
- }
2118
- }
2119
- };
2120
- function isRedirect(status) {
2121
- return status >= 300 && status < 400 && status !== 304;
2122
- }
2123
- function setClientAddress(request, address) {
2124
- const g = globalThis;
2125
- (g[CLIENT_ADDRESS] ??= /* @__PURE__ */ new WeakMap()).set(request, address);
2126
- }
2127
- function flatten(data, prefix = "") {
2128
- const out = [];
2129
- for (const [key, value] of Object.entries(data)) {
2130
- const name = prefix === "" ? key : `${prefix}[${key}]`;
2131
- if (value === void 0) continue;
2132
- if (value === null) out.push([name, ""]);
2133
- else if (value instanceof Blob) out.push([name, value]);
2134
- else if (value instanceof Date) out.push([name, value.toISOString()]);
2135
- else if (typeof value === "object") out.push(...flatten(value, name));
2136
- else if (typeof value === "boolean") out.push([name, value ? "1" : "0"]);
2137
- else out.push([name, String(value)]);
2138
- }
2139
- return out;
2140
- }
2141
- var globalHooks = () => {
2142
- const g = globalThis;
2143
- return typeof g.beforeEach === "function" && typeof g.afterEach === "function" ? { beforeEach: g.beforeEach, afterEach: g.afterEach } : void 0;
2144
- };
2145
- var InteractsWithApplication = class {
2146
- /** Set once in tests/TestCase.ts: `static override hooks = { beforeEach, afterEach }` (from vitest). */
2147
- static hooks;
2148
- /** Traits this test case uses (Laravel's `use RefreshDatabase;`). */
2149
- uses = [];
2150
- application;
2151
- extraTraits = [];
2152
- destroying = [];
2153
- /**
2154
- * Creates the test case and registers its setUp/tearDown as beforeEach/afterEach hooks.
2155
- * `const t = TestCase.boot()` at the top of a test file (or inside a describe block).
2156
- */
2157
- static boot(options = {}) {
2158
- const hooks = options.hooks ?? this.hooks ?? globalHooks();
2159
- if (hooks === void 0) {
2160
- throw new Error(
2161
- "TestCase.boot() needs the test runner hooks: set `static override hooks = { beforeEach, afterEach }` (imported from vitest) in tests/TestCase.ts, or pass { hooks }."
2162
- );
2163
- }
2164
- const test = new this();
2165
- test.extraTraits = [...options.uses ?? []];
2166
- hooks.beforeEach(() => test.setUp());
2167
- hooks.afterEach(() => test.tearDown());
2168
- return test;
2169
- }
2170
- /** The current test's application. */
2171
- get app() {
2172
- if (this.application === void 0) {
2173
- throw new Error("There is no application outside a test: use it inside test() (after TestCase.boot()).");
2174
- }
2175
- return this.application;
2176
- }
2177
- /**
2178
- * A fresh application for one test. tests/TestCase.ts overrides this to call bootstrap/app's factory.
2179
- * The default builds one at basePath() with providers(), for package tests with no skeleton.
2180
- */
2181
- createApplication() {
2182
- return Application2.configure({ basePath: this.basePath() }).withProviders(this.providers()).create();
2183
- }
2184
- /** Used by the default createApplication(). */
2185
- basePath() {
2186
- return process.cwd();
2187
- }
2188
- /** Used by the default createApplication(). */
2189
- providers() {
2190
- return [];
2191
- }
2192
- /** The variables applied (unless already set) before each application is created. */
2193
- environment() {
2194
- return { ...TESTING_ENVIRONMENT };
2195
- }
2196
- /** Adjust the fresh app before it boots, e.g. `app.make(ConfigRepository).set('mail.default', 'array')`. */
2197
- defineEnvironment(_app) {
2198
- }
2199
- async setUp() {
2200
- applyEnvironment(this.environment());
2201
- const app = await this.createApplication();
2202
- this.application = app;
2203
- app.setRunningInConsole(true);
2204
- await this.defineEnvironment(app);
2205
- await app.boot();
2206
- for (const trait of [...this.uses, ...this.extraTraits]) await trait.setUp(this);
2207
- }
2208
- async tearDown() {
2209
- const app = this.application;
2210
- if (app === void 0) return;
2211
- try {
2212
- for (const callback of this.destroying) await callback();
2213
- if (app.resolved(DatabaseManager)) await disconnectAll(app.make(DatabaseManager));
2214
- } finally {
2215
- this.destroying = [];
2216
- this.application = void 0;
2217
- const global = Container.getInstance();
2218
- if (global === app) Container.setInstance(void 0);
2219
- }
2220
- }
2221
- /** Runs in tearDown(), before the application is thrown away (in registration order). */
2222
- beforeApplicationDestroyed(callback) {
2223
- this.destroying.push(callback);
2224
- }
2225
- };
2226
- async function disconnectAll(db) {
2227
- const manager = db;
2228
- if (typeof manager.disconnectAll === "function") await manager.disconnectAll();
2229
- else await manager.disconnect();
2230
- }
2231
- var MakesHttpRequests = class extends InteractsWithApplication {
2232
- client;
2233
- /** The current test's HTTP client (created on first use). */
2234
- get http() {
2235
- return this.client ??= this.createHttpClient();
2236
- }
2237
- createHttpClient() {
2238
- return new HttpClient(this.app);
2239
- }
2240
- async tearDown() {
2241
- try {
2242
- await super.tearDown();
2243
- } finally {
2244
- this.client = void 0;
2245
- }
2246
- }
2247
- withHeaders(headers) {
2248
- Object.assign(this.http.headers, headers);
2249
- return this;
2250
- }
2251
- withHeader(name, value) {
2252
- this.http.headers[name] = value;
2253
- return this;
2254
- }
2255
- withToken(token, type = "Bearer") {
2256
- return this.withHeader("Authorization", `${type} ${token}`);
2257
- }
2258
- withBasicAuth(username, password) {
2259
- return this.withToken(Buffer.from(`${username}:${password}`).toString("base64"), "Basic");
2260
- }
2261
- /** Removes the Authorization header, whatever case it was set with (header names are case-insensitive). */
2262
- withoutToken() {
2263
- for (const name of Object.keys(this.http.headers)) if (name.toLowerCase() === "authorization") delete this.http.headers[name];
2264
- return this;
2265
- }
2266
- /** Cookies are encrypted the way EncryptCookies expects when the app has an Encrypter. */
2267
- withCookies(cookies) {
2268
- for (const [name, value] of Object.entries(cookies)) this.http.cookies.set(name, { value, encrypt: true });
2269
- return this;
2270
- }
2271
- withCookie(name, value) {
2272
- return this.withCookies({ [name]: value });
2273
- }
2274
- withUnencryptedCookies(cookies) {
2275
- for (const [name, value] of Object.entries(cookies)) this.http.cookies.set(name, { value, encrypt: false });
2276
- return this;
2277
- }
2278
- withUnencryptedCookie(name, value) {
2279
- return this.withUnencryptedCookies({ [name]: value });
2280
- }
2281
- /** Follow redirects (always with GET, like Laravel) and return the final response. */
2282
- followingRedirects() {
2283
- this.http.followRedirects = true;
2284
- return this;
2285
- }
2286
- /** Sets the Referer and the session's previous URL, so redirect()->back() goes there. */
2287
- from(url) {
2288
- this.http.previousUrl = url;
2289
- return this.withHeader("Referer", url);
2290
- }
2291
- /** Puts data in the session for the following requests (Laravel's withSession()). */
2292
- withSession(data) {
2293
- this.http.session = { ...this.http.session, ...data };
2294
- return this;
2295
- }
2296
- /** Empties the session carried between requests. */
2297
- flushSession() {
2298
- this.http.session = {};
2299
- return this;
2300
- }
2301
- /** Every following request is authenticated as `user` on `guard` (which also becomes the default guard). */
2302
- actingAs(user, guard) {
2303
- this.http.users.set(guard, user);
2304
- if (guard !== void 0) this.http.defaultGuard = guard;
2305
- return this;
2306
- }
2307
- be(user, guard) {
2308
- return this.actingAs(user, guard);
2309
- }
2310
- /** Errors escape from the request instead of being rendered (Laravel's withoutExceptionHandling()). */
2311
- withoutExceptionHandling() {
2312
- this.http.handleExceptions = false;
2313
- return this;
2314
- }
2315
- withExceptionHandling() {
2316
- this.http.handleExceptions = true;
2317
- return this;
2318
- }
2319
- /** The user is authenticated (after the requests so far: carried session and actingAs). */
2320
- async assertAuthenticated(guard) {
2321
- assert(await this.isAuthenticated(guard), "The user is not authenticated");
2322
- return this;
2323
- }
2324
- async assertGuest(guard) {
2325
- assert(!await this.isAuthenticated(guard), "The user is authenticated");
2326
- return this;
2327
- }
2328
- /** Same identifier and class as `user`. */
2329
- async assertAuthenticatedAs(user, guard) {
2330
- const current = await this.http.inScope((scope) => scope.make(AuthFactory).guard(guard).user());
2331
- assert(current !== void 0, "The current user is not authenticated.");
2332
- assert(
2333
- current.constructor === user.constructor && current.getAuthIdentifier() === user.getAuthIdentifier(),
2334
- `The currently authenticated user [${String(current.getAuthIdentifier())}] is not who was expected [${String(user.getAuthIdentifier())}].`
2335
- );
2336
- return this;
2337
- }
2338
- isAuthenticated(guard) {
2339
- return this.http.inScope((scope) => scope.make(AuthFactory).guard(guard).check());
2340
- }
2341
- get(uri, headers = {}) {
2342
- return this.http.request("GET", uri, { headers });
2343
- }
2344
- getJson(uri, headers = {}) {
2345
- return this.http.request("GET", uri, { headers, json: true });
2346
- }
2347
- post(uri, data = {}, headers = {}) {
2348
- return this.http.request("POST", uri, { data, headers });
2349
- }
2350
- postJson(uri, data = {}, headers = {}) {
2351
- return this.http.request("POST", uri, { data, headers, json: true });
2352
- }
2353
- put(uri, data = {}, headers = {}) {
2354
- return this.http.request("PUT", uri, { data, headers });
2355
- }
2356
- putJson(uri, data = {}, headers = {}) {
2357
- return this.http.request("PUT", uri, { data, headers, json: true });
2358
- }
2359
- patch(uri, data = {}, headers = {}) {
2360
- return this.http.request("PATCH", uri, { data, headers });
2361
- }
2362
- patchJson(uri, data = {}, headers = {}) {
2363
- return this.http.request("PATCH", uri, { data, headers, json: true });
2364
- }
2365
- delete(uri, data = {}, headers = {}) {
2366
- return this.http.request("DELETE", uri, { data, headers });
2367
- }
2368
- deleteJson(uri, data = {}, headers = {}) {
2369
- return this.http.request("DELETE", uri, { data, headers, json: true });
2370
- }
2371
- options(uri, data = {}, headers = {}) {
2372
- return this.http.request("OPTIONS", uri, { data, headers });
2373
- }
2374
- head(uri, headers = {}) {
2375
- return this.http.request("HEAD", uri, { headers });
2376
- }
2377
- /** A JSON request with any verb; GET/HEAD data goes into the query string. */
2378
- json(method, uri, data = {}, headers = {}) {
2379
- return this.http.request(method, uri, { data, headers, json: true });
2380
- }
2381
- /** Any verb with form data (Laravel's call()). */
2382
- call(method, uri, data = {}, headers = {}) {
2383
- return this.http.request(method, uri, { data, headers });
2384
- }
2385
- };
2386
- var InteractsWithDatabase = class extends MakesHttpRequests {
2387
- /** Seed after RefreshDatabase/DatabaseMigrations migrate (Laravel's $seed). */
2388
- seed = false;
2389
- /** Seed with this seeder class instead of DatabaseSeeder (Laravel's $seeder). */
2390
- seeder = void 0;
2391
- /** Connections RefreshDatabase/DatabaseTransactions wrap in a transaction (undefined = the default connection). */
2392
- connectionsToTransact = [void 0];
2393
- get database() {
2394
- return this.app.make(DatabaseManager);
2395
- }
2396
- async assertDatabaseHas(table, data, connection) {
2397
- await assertDatabaseHas(this.database, table, data, connection);
2398
- return this;
2399
- }
2400
- async assertDatabaseMissing(table, data, connection) {
2401
- await assertDatabaseMissing(this.database, table, data, connection);
2402
- return this;
2403
- }
2404
- async assertDatabaseCount(table, count, connection) {
2405
- await assertDatabaseCount(this.database, table, count, connection);
2406
- return this;
2407
- }
2408
- async assertDatabaseEmpty(table, connection) {
2409
- return this.assertDatabaseCount(table, 0, connection);
2410
- }
2411
- async assertModelExists(model) {
2412
- return this.assertDatabaseHas(model, { [model.getKeyName()]: model.getKey() });
2413
- }
2414
- async assertModelMissing(model) {
2415
- return this.assertDatabaseMissing(model, { [model.getKeyName()]: model.getKey() });
2416
- }
2417
- async assertSoftDeleted(table, data = {}, connection, deletedAtColumn = "deleted_at") {
2418
- await assertSoftDeleted(this.database, table, data, connection, deletedAtColumn);
2419
- return this;
2420
- }
2421
- async assertNotSoftDeleted(table, data = {}, connection, deletedAtColumn = "deleted_at") {
2422
- await assertNotSoftDeleted(this.database, table, data, connection, deletedAtColumn);
2423
- return this;
2424
- }
2425
- /** Runs `db:seed` (with a seeder class name, like Laravel's $this->seed(UserSeeder::class)). */
2426
- async runSeeder(seeder) {
2427
- await runCommand(this.app, "db:seed", seeder === void 0 ? {} : { "--class": seeder });
2428
- return this;
2429
- }
2430
- };
2431
- var InteractsWithConsole = class extends InteractsWithDatabase {
2432
- /** The command runs when the returned PendingCommand is awaited; without `await` it never runs. */
2433
- artisan(command, parameters = {}) {
2434
- return new PendingCommand(this.app, command, parameters);
2435
- }
2436
- };
2437
- var TestCase = class extends InteractsWithConsole {
2438
- };
2439
- function install(token, fake) {
2440
- facade(token).swap(fake);
2441
- return fake;
2442
- }
2443
- function bound(token) {
2444
- const container = currentContainer();
2445
- return container !== void 0 && container.bound(token) ? container.make(token) : void 0;
2446
- }
2447
- function nameOf(target2) {
2448
- if (typeof target2 === "string") return target2;
2449
- if (typeof target2 === "function") return target2.name;
2450
- return target2.constructor.name;
2451
- }
2452
- function matches(item, target2) {
2453
- if (typeof target2 === "string") return typeof item === "string" ? item === target2 : typeof item === "object" && item !== null && item.constructor.name === target2;
2454
- return typeof item === "object" && item !== null && item.constructor === target2;
2455
- }
2456
- function matchesAny(item, targets) {
2457
- return targets.some((target2) => matches(item, target2));
2458
- }
2459
- function sameJob(actual, expected) {
2460
- if (typeof expected === "function" || typeof expected === "string") return matches(actual, expected);
2461
- return actual.constructor === expected.constructor && equals({ ...actual }, { ...expected });
2462
- }
2463
- var SHOULD_QUEUE = /* @__PURE__ */ Symbol.for("oriel:bus.shouldQueue");
2464
- var QUEUE_OPTIONS = ["connection", "queue", "delay", "tries", "backoff", "timeout"];
2465
- function isQueueable(job) {
2466
- if (typeof job !== "object" || job === null) return false;
2467
- const members = job;
2468
- if (members[SHOULD_QUEUE] === true) return true;
2469
- if (QUEUE_OPTIONS.some((option) => members[option] !== void 0)) return true;
2470
- return typeof members.uniqueId === "function" || typeof members.retryUntil === "function";
2471
- }
2472
- var EventFake = class _EventFake {
2473
- constructor(dispatcher, eventsToFake = [], eventsToDispatch = []) {
2474
- this.dispatcher = dispatcher;
2475
- this.eventsToFake = eventsToFake;
2476
- this.eventsToDispatch = eventsToDispatch;
2477
- }
2478
- dispatcher;
2479
- eventsToFake;
2480
- eventsToDispatch;
2481
- events = [];
2482
- /** Installs the fake (on the current container) and returns it: `const events = EventFake.fake([OrderShipped])`. */
2483
- static fake(eventsToFake = []) {
2484
- return install(EventDispatcher, new _EventFake(_EventFake.original(), [eventsToFake].flat()));
2485
- }
2486
- /** Fakes every event except these. */
2487
- static fakeExcept(eventsToDispatch) {
2488
- return install(EventDispatcher, new _EventFake(_EventFake.original(), [], [eventsToDispatch].flat()));
2489
- }
2490
- static original() {
2491
- const current = bound(EventDispatcher);
2492
- return current instanceof _EventFake ? current.dispatcher : current;
2493
- }
2494
- except(events) {
2495
- this.eventsToDispatch = [...this.eventsToDispatch, ...[events].flat()];
2496
- return this;
2497
- }
2498
- // EventDispatcher -----------------------------------------------------------------------------------------
2499
- listen(events, listener) {
2500
- this.dispatcher?.listen(events, listener);
2501
- }
2502
- hasListeners(event) {
2503
- return this.dispatcher?.hasListeners(event) ?? false;
2504
- }
2505
- async dispatch(event, payload = []) {
2506
- if (this.shouldFake(event)) {
2507
- this.events.push({ event, payload });
2508
- return [];
2509
- }
2510
- return this.dispatcher?.dispatch(event, payload) ?? [];
2511
- }
2512
- /** Faked events return undefined (never false), so halting model events such as `saving` still proceed. */
2513
- async until(event, payload = []) {
2514
- if (this.shouldFake(event)) {
2515
- this.events.push({ event, payload });
2516
- return void 0;
2517
- }
2518
- return this.dispatcher?.until(event, payload);
2519
- }
2520
- subscribe(subscriber) {
2521
- this.dispatcher?.subscribe(subscriber);
2522
- }
2523
- forget(event) {
2524
- this.dispatcher?.forget(event);
2525
- }
2526
- // Assertions ------------------------------------------------------------------------------------------------
2527
- /** Dispatched at least once; a number asserts the exact count; a function filters (event, payload). */
2528
- assertDispatched(event, callback) {
2529
- if (typeof callback === "number") return this.assertDispatchedTimes(event, callback);
2530
- assert(this.dispatched(event, callback).length > 0, `The expected [${nameOf(event)}] event was not dispatched.`);
2531
- }
2532
- assertDispatchedTimes(event, times = 1) {
2533
- const count = this.dispatched(event).length;
2534
- assert(count === times, `The expected [${nameOf(event)}] event was dispatched ${count} times instead of ${times} times.`, { actual: count, expected: times });
2535
- }
2536
- assertNotDispatched(event, callback) {
2537
- assert(this.dispatched(event, callback).length === 0, `The unexpected [${nameOf(event)}] event was dispatched.`);
2538
- }
2539
- assertNothingDispatched() {
2540
- const names = this.events.map(({ event }) => nameOf(event));
2541
- assert(names.length === 0, `${names.length} unexpected events were dispatched:
2542
-
2543
- - ${names.join("\n- ")}`);
2544
- }
2545
- /** The original dispatcher has a listener for the event. */
2546
- assertListening(event) {
2547
- assert(this.hasListeners(event), `Event [${nameOf(event)}] does not have any listeners.`);
2548
- }
2549
- /** The recorded events (objects, or names for string events) that match. */
2550
- dispatched(event, callback) {
2551
- return this.events.filter((record) => matchesAny(record.event, [event]) && (callback === void 0 || callback(record.event, record.payload))).map((record) => record.event);
2552
- }
2553
- hasDispatched(event) {
2554
- return this.dispatched(event).length > 0;
2555
- }
2556
- shouldFake(event) {
2557
- if (matchesAny(event, this.eventsToDispatch)) return false;
2558
- return this.eventsToFake.length === 0 || matchesAny(event, this.eventsToFake);
2559
- }
2560
- };
2561
- var FakeBatch = class {
2562
- constructor(record) {
2563
- this.record = record;
2564
- }
2565
- record;
2566
- get id() {
2567
- return this.record.id;
2568
- }
2569
- get name() {
2570
- return this.record.name;
2571
- }
2572
- get totalJobs() {
2573
- return this.record.jobs.length;
2574
- }
2575
- get pendingJobs() {
2576
- return this.record.jobs.length;
2577
- }
2578
- failedJobs = 0;
2579
- isCancelled = false;
2580
- progress() {
2581
- return 0;
2582
- }
2583
- finished() {
2584
- return false;
2585
- }
2586
- cancelled() {
2587
- return this.isCancelled;
2588
- }
2589
- async cancel() {
2590
- this.isCancelled = true;
2591
- }
2592
- };
2593
- var BusFake = class _BusFake {
2594
- constructor(dispatcher, jobsToFake = [], jobsToDispatch = []) {
2595
- this.dispatcher = dispatcher;
2596
- this.jobsToFake = jobsToFake;
2597
- this.jobsToDispatch = jobsToDispatch;
2598
- }
2599
- dispatcher;
2600
- jobsToFake;
2601
- jobsToDispatch;
2602
- commands = [];
2603
- commandsSync = [];
2604
- commandsAfterResponse = [];
2605
- batches = [];
2606
- /** The rest of the chain, by the first job (Bus.chain([a, b, c]) records a with [b, c]). */
2607
- chains = /* @__PURE__ */ new Map();
2608
- static fake(jobsToFake = []) {
2609
- return install(BusDispatcher, new _BusFake(_BusFake.original(), [jobsToFake].flat()));
2610
- }
2611
- static fakeExcept(jobsToDispatch) {
2612
- return install(BusDispatcher, new _BusFake(_BusFake.original(), [], [jobsToDispatch].flat()));
2613
- }
2614
- static original() {
2615
- const current = bound(BusDispatcher);
2616
- return current instanceof _BusFake ? current.dispatcher : current;
2617
- }
2618
- except(jobs) {
2619
- this.jobsToDispatch = [...this.jobsToDispatch, ...[jobs].flat()];
2620
- return this;
2621
- }
2622
- // BusDispatcher ----------------------------------------------------------------------------------------
2623
- async dispatch(job) {
2624
- if (!this.shouldFake(job)) return this.real().dispatch(job);
2625
- this.commands.push(job);
2626
- return void 0;
2627
- }
2628
- async dispatchSync(job) {
2629
- if (!this.shouldFake(job)) return this.real().dispatchSync(job);
2630
- this.commandsSync.push(job);
2631
- return void 0;
2632
- }
2633
- dispatchAfterResponse(job) {
2634
- if (!this.shouldFake(job)) return this.real().dispatchAfterResponse(job);
2635
- this.commandsAfterResponse.push(job);
2636
- }
2637
- /**
2638
- * Bus's terminating callback flushes whatever BusDispatcher is bound, which is this fake: forward to the real
2639
- * dispatcher so the after-response jobs it didn't fake still run (Laravel runs them). Faked ones never run.
2640
- */
2641
- async flushAfterResponse() {
2642
- await this.dispatcher?.flushAfterResponse?.();
2643
- }
2644
- chain(jobs) {
2645
- const options = {};
2646
- const pending = {
2647
- onConnection: (connection) => {
2648
- options.connection = connection;
2649
- return pending;
2650
- },
2651
- onQueue: (queue) => {
2652
- options.queue = queue;
2653
- return pending;
2654
- },
2655
- catch: () => pending,
2656
- dispatch: async () => {
2657
- const [first, ...rest] = jobs;
2658
- if (first === void 0) return;
2659
- if (!this.shouldFake(first)) return this.real().chain(jobs).dispatch();
2660
- if (options.connection !== void 0) first.connection = options.connection;
2661
- if (options.queue !== void 0) first.queue = options.queue;
2662
- this.commands.push(first);
2663
- this.chains.set(first, rest);
2664
- }
2665
- };
2666
- return pending;
2667
- }
2668
- batch(jobs) {
2669
- const record = { id: randomUUID(), jobs, name: "", allowFailures: false, then: [], catch: [], finally: [] };
2670
- const batches = this.batches;
2671
- const pending = {
2672
- name(name) {
2673
- record.name = name;
2674
- return pending;
2675
- },
2676
- onConnection(connection) {
2677
- record.connection = connection;
2678
- return pending;
2679
- },
2680
- onQueue(queue) {
2681
- record.queue = queue;
2682
- return pending;
2683
- },
2684
- allowFailures(allow = true) {
2685
- record.allowFailures = allow;
2686
- return pending;
2687
- },
2688
- then(job) {
2689
- record.then.push(job);
2690
- return pending;
2691
- },
2692
- catch(job) {
2693
- record.catch.push(job);
2694
- return pending;
2695
- },
2696
- finally(job) {
2697
- record.finally.push(job);
2698
- return pending;
2699
- },
2700
- async dispatch() {
2701
- batches.push(record);
2702
- return new FakeBatch(record);
2703
- }
2704
- };
2705
- return pending;
2706
- }
2707
- async findBatch(id) {
2708
- const record = this.batches.find((batch) => batch.id === id);
2709
- return record === void 0 ? void 0 : new FakeBatch(record);
2710
- }
2711
- // Assertions ---------------------------------------------------------------------------------------------
2712
- /** Dispatched in any way (dispatch, dispatchSync or dispatchAfterResponse); a number asserts the count. */
2713
- assertDispatched(job, callback) {
2714
- if (typeof callback === "number") return this.assertDispatchedTimes(job, callback);
2715
- const count = this.dispatched(job, callback).length + this.dispatchedSync(job, callback).length + this.dispatchedAfterResponse(job, callback).length;
2716
- assert(count > 0, `The expected [${nameOf(job)}] job was not dispatched.`);
2717
- }
2718
- assertDispatchedTimes(job, times = 1) {
2719
- const count = this.dispatched(job).length + this.dispatchedSync(job).length + this.dispatchedAfterResponse(job).length;
2720
- assert(count === times, `The expected [${nameOf(job)}] job was pushed ${count} times instead of ${times} times.`, { actual: count, expected: times });
2721
- }
2722
- assertNotDispatched(job, callback) {
2723
- const count = this.dispatched(job, callback).length + this.dispatchedSync(job, callback).length + this.dispatchedAfterResponse(job, callback).length;
2724
- assert(count === 0, `The unexpected [${nameOf(job)}] job was dispatched.`);
2725
- }
2726
- assertNothingDispatched() {
2727
- const all = [...this.commands, ...this.commandsSync, ...this.commandsAfterResponse].map((job) => nameOf(job));
2728
- assert(all.length === 0, `The following jobs were dispatched unexpectedly:
2729
-
2730
- - ${all.join("\n- ")}`);
2731
- }
2732
- assertDispatchedSync(job, callback) {
2733
- if (typeof callback === "number") {
2734
- const count = this.dispatchedSync(job).length;
2735
- assert(count === callback, `The expected [${nameOf(job)}] job was synchronously pushed ${count} times instead of ${callback} times.`, { actual: count, expected: callback });
2736
- return;
2737
- }
2738
- assert(this.dispatchedSync(job, callback).length > 0, `The expected [${nameOf(job)}] job was not dispatched synchronously.`);
2739
- }
2740
- assertNotDispatchedSync(job, callback) {
2741
- assert(this.dispatchedSync(job, callback).length === 0, `The unexpected [${nameOf(job)}] job was dispatched synchronously.`);
2742
- }
2743
- assertDispatchedAfterResponse(job, callback) {
2744
- if (typeof callback === "number") {
2745
- const count = this.dispatchedAfterResponse(job).length;
2746
- assert(count === callback, `The expected [${nameOf(job)}] job was pushed ${count} times instead of ${callback} times.`, { actual: count, expected: callback });
2747
- return;
2748
- }
2749
- assert(this.dispatchedAfterResponse(job, callback).length > 0, `The expected [${nameOf(job)}] job was not dispatched after sending the response.`);
2750
- }
2751
- assertNotDispatchedAfterResponse(job, callback) {
2752
- assert(this.dispatchedAfterResponse(job, callback).length === 0, `The unexpected [${nameOf(job)}] job was dispatched after sending the response.`);
2753
- }
2754
- /** `[First, Second, new Third(1)]`: a dispatched First whose chain is exactly the rest (classes or equal instances). */
2755
- assertChained(expectedChain) {
2756
- const [first, ...rest] = expectedChain;
2757
- if (first === void 0) fail("assertChained() needs at least one job.");
2758
- const candidates = this.commands.filter((job) => sameJob(job, first));
2759
- assert(candidates.length > 0, `The expected [${nameOf(first)}] job was not dispatched.`);
2760
- const found = candidates.some((job) => {
2761
- const chain = this.chains.get(job) ?? [];
2762
- return chain.length === rest.length && chain.every((link, index) => sameJob(link, rest[index]));
2763
- });
2764
- assert(found, `The expected chain [${expectedChain.map(nameOf).join(", ")}] was not dispatched.`);
2765
- }
2766
- assertDispatchedWithoutChain(job, callback) {
2767
- const jobs = this.dispatched(job, callback);
2768
- assert(jobs.length > 0, `The expected [${nameOf(job)}] job was not dispatched.`);
2769
- assert(jobs.some((dispatched) => (this.chains.get(dispatched) ?? []).length === 0), `The expected [${nameOf(job)}] job was dispatched with a chain.`);
2770
- }
2771
- /** A batch was dispatched that passes the check. */
2772
- assertBatched(callback) {
2773
- assert(this.batches.some(callback), "The expected batch was not dispatched.");
2774
- }
2775
- assertBatchCount(count) {
2776
- assert(this.batches.length === count, `${this.batches.length} batches were dispatched instead of ${count}.`, { actual: this.batches.length, expected: count });
2777
- }
2778
- assertNothingBatched() {
2779
- assert(this.batches.length === 0, "Batched jobs were dispatched unexpectedly.");
2780
- }
2781
- dispatched(job, callback) {
2782
- return filter(this.commands, job, callback);
2783
- }
2784
- dispatchedSync(job, callback) {
2785
- return filter(this.commandsSync, job, callback);
2786
- }
2787
- dispatchedAfterResponse(job, callback) {
2788
- return filter(this.commandsAfterResponse, job, callback);
2789
- }
2790
- hasDispatched(job) {
2791
- return this.dispatched(job).length > 0;
2792
- }
2793
- shouldFake(job) {
2794
- if (matchesAny(job, this.jobsToDispatch)) return false;
2795
- return this.jobsToFake.length === 0 || matchesAny(job, this.jobsToFake);
2796
- }
2797
- real() {
2798
- if (this.dispatcher === void 0) fail("Bus.fake() was given jobs to fake, but there is no real BusDispatcher for the others.");
2799
- return this.dispatcher;
2800
- }
2801
- };
2802
- function filter(jobs, target2, callback) {
2803
- return jobs.filter((job) => matchesAny(job, [target2])).filter((job) => callback === void 0 || callback(job));
2804
- }
2805
- var CHAIN = /* @__PURE__ */ Symbol.for("oriel:queue.chain");
2806
- var QueueFake = class _QueueFake {
2807
- constructor(manager, jobsToFake = [], jobsToPush = []) {
2808
- this.manager = manager;
2809
- this.jobsToFake = jobsToFake;
2810
- this.jobsToPush = jobsToPush;
2811
- }
2812
- manager;
2813
- jobsToFake;
2814
- jobsToPush;
2815
- jobs = [];
2816
- static fake(jobsToFake = []) {
2817
- return install(QueueManager, new _QueueFake(_QueueFake.original(), [jobsToFake].flat()));
2818
- }
2819
- static fakeExcept(jobsToPush) {
2820
- return install(QueueManager, new _QueueFake(_QueueFake.original(), [], [jobsToPush].flat()));
2821
- }
2822
- static original() {
2823
- const current = bound(QueueManager);
2824
- return current instanceof _QueueFake ? current.manager : current;
2825
- }
2826
- except(jobs) {
2827
- this.jobsToPush = [...this.jobsToPush, ...[jobs].flat()];
2828
- return this;
2829
- }
2830
- // QueueManager ---------------------------------------------------------------------------------------------
2831
- /**
2832
- * A recording view of the connection (pushes remember its name). Its size()/clear() only see the jobs pushed
2833
- * through that connection name (no name: the jobs pushed without one, i.e. through the manager or connection()).
2834
- */
2835
- connection(name) {
2836
- return {
2837
- push: (job, queue) => this.record(name, job, queue, void 0),
2838
- later: (delay, job, queue) => this.record(name, job, queue, delay),
2839
- pushRaw: (payload, queue, delay) => this.recordRaw(name, payload, queue, delay),
2840
- pop: (queue) => this.pop(queue),
2841
- size: async (queue) => this.jobs.filter((pushed) => this.on(pushed, queue, { name })).length,
2842
- clear: async (queue) => this.remove(queue, { name })
2843
- };
2844
- }
2845
- push(job, queue) {
2846
- return this.record(void 0, job, queue, void 0);
2847
- }
2848
- later(delaySeconds, job, queue) {
2849
- return this.record(void 0, job, queue, delaySeconds);
2850
- }
2851
- pushRaw(payload, queue, delaySeconds) {
2852
- return this.recordRaw(void 0, payload, queue, delaySeconds);
2853
- }
2854
- async pop(_queue) {
2855
- return void 0;
2856
- }
2857
- /** Jobs on `queue` (every queue when omitted), whatever connection they were pushed on, like Laravel's fake. */
2858
- async size(queue) {
2859
- return this.jobs.filter((pushed) => this.on(pushed, queue)).length;
2860
- }
2861
- /** Removes the jobs size() counts and returns how many. */
2862
- async clear(queue) {
2863
- return this.remove(queue);
2864
- }
2865
- extend(_driver, _factory) {
2866
- }
2867
- // Assertions ---------------------------------------------------------------------------------------------
2868
- /** Pushed at least once; a number asserts the count; a filter gets (job, queue, connection). */
2869
- assertPushed(job, callback) {
2870
- if (typeof callback === "number") return this.assertPushedTimes(job, callback);
2871
- assert(this.pushed(job, callback).length > 0, `The expected [${nameOf(job)}] job was not pushed.`);
2872
- }
2873
- assertPushedTimes(job, times = 1) {
2874
- const count = this.pushed(job).length;
2875
- assert(count === times, `The expected [${nameOf(job)}] job was pushed ${count} times instead of ${times} times.`, { actual: count, expected: times });
2876
- }
2877
- assertPushedOn(queue, job, callback) {
2878
- this.assertPushed(job, (pushed, on, connection) => on === queue && (callback === void 0 || callback(pushed, on, connection)));
2879
- }
2880
- assertNotPushed(job, callback) {
2881
- assert(this.pushed(job, callback).length === 0, `The unexpected [${nameOf(job)}] job was pushed.`);
2882
- }
2883
- assertNothingPushed() {
2884
- const names = this.jobs.map((pushed) => this.nameOf(pushed));
2885
- assert(names.length === 0, `The following jobs were pushed unexpectedly:
2886
-
2887
- - ${names.join("\n- ")}`);
2888
- }
2889
- assertCount(expected) {
2890
- assert(this.jobs.length === expected, `Expected ${expected} jobs to be pushed, but found ${this.jobs.length} instead.`, { actual: this.jobs.length, expected });
2891
- }
2892
- /** The job was pushed with exactly this chain (Bus stores it on the first job under the protocol key). */
2893
- assertPushedWithChain(job, expectedChain, callback) {
2894
- const jobs = this.pushed(job, callback);
2895
- assert(jobs.length > 0, `The expected [${nameOf(job)}] job was not pushed.`);
2896
- const found = jobs.some((pushed) => {
2897
- const chain = chainOf(pushed);
2898
- return chain.length === expectedChain.length && chain.every((link, index) => sameJob(link, expectedChain[index]));
2899
- });
2900
- assert(found, `The expected chain was not pushed with [${nameOf(job)}].`);
2901
- }
2902
- assertPushedWithoutChain(job, callback) {
2903
- const jobs = this.pushed(job, callback);
2904
- assert(jobs.length > 0, `The expected [${nameOf(job)}] job was not pushed.`);
2905
- assert(jobs.some((pushed) => chainOf(pushed).length === 0), `The expected [${nameOf(job)}] job was pushed with a chain.`);
2906
- }
2907
- /** Pushed jobs matching the target (raw payloads match by their registry name or display name). */
2908
- pushed(job, callback) {
2909
- return this.jobs.filter((pushed) => this.targets(pushed, job) && (callback === void 0 || callback(pushed.job, pushed.queue, pushed.connection))).map((pushed) => pushed.job);
2910
- }
2911
- hasPushed(job) {
2912
- return this.pushed(job).length > 0;
2913
- }
2914
- // Internals ----------------------------------------------------------------------------------------------
2915
- async record(connection, job, queue, delay) {
2916
- if (!this.shouldFake(job)) {
2917
- const real = this.real().connection(connection);
2918
- return delay === void 0 ? real.push(job, queue) : real.later(delay, job, queue);
2919
- }
2920
- this.jobs.push({ job, raw: false, connection, queue, delay });
2921
- return randomUUID2();
2922
- }
2923
- async recordRaw(connection, payload, queue, delay) {
2924
- this.jobs.push({ job: payload, raw: true, connection, queue, delay });
2925
- return payload.uuid;
2926
- }
2927
- /** The job is on `queue` (any when undefined) and, when `connection` is given, was pushed on that connection name. */
2928
- on(pushed, queue, connection) {
2929
- return (queue === void 0 || pushed.queue === queue) && (connection === void 0 || pushed.connection === connection.name);
2930
- }
2931
- remove(queue, connection) {
2932
- const before = this.jobs.length;
2933
- const kept = this.jobs.filter((pushed) => !this.on(pushed, queue, connection));
2934
- this.jobs.splice(0, this.jobs.length, ...kept);
2935
- return before - kept.length;
2936
- }
2937
- targets(pushed, target2) {
2938
- if (!pushed.raw) return matchesAny(pushed.job, [target2]);
2939
- const payload = pushed.job;
2940
- const name = nameOf(target2);
2941
- return payload.job === name || payload.displayName === name;
2942
- }
2943
- nameOf(pushed) {
2944
- return pushed.raw ? pushed.job.displayName : nameOf(pushed.job);
2945
- }
2946
- shouldFake(job) {
2947
- if (matchesAny(job, this.jobsToPush)) return false;
2948
- return this.jobsToFake.length === 0 || matchesAny(job, this.jobsToFake);
2949
- }
2950
- real() {
2951
- if (this.manager === void 0) fail("Queue.fake() was given jobs to fake, but there is no real QueueManager for the others.");
2952
- return this.manager;
2953
- }
2954
- };
2955
- function chainOf(job) {
2956
- const chain = job[CHAIN];
2957
- return chain?.jobs ?? [];
2958
- }
2959
- var MailRecord = class {
2960
- constructor(mailable, mailer, to, cc, bcc, subject, queued, delay) {
2961
- this.mailable = mailable;
2962
- this.mailer = mailer;
2963
- this.to = to;
2964
- this.cc = cc;
2965
- this.bcc = bcc;
2966
- this.subject = subject;
2967
- this.queued = queued;
2968
- this.delay = delay;
2969
- }
2970
- mailable;
2971
- mailer;
2972
- to;
2973
- cc;
2974
- bcc;
2975
- subject;
2976
- queued;
2977
- delay;
2978
- hasTo(address) {
2979
- return includes(this.to, address);
2980
- }
2981
- hasCc(address) {
2982
- return includes(this.cc, address);
2983
- }
2984
- hasBcc(address) {
2985
- return includes(this.bcc, address);
2986
- }
2987
- hasSubject(subject) {
2988
- return this.subject === subject;
2989
- }
2990
- };
2991
- var MailFake = class _MailFake {
2992
- constructor(manager = void 0) {
2993
- this.manager = manager;
2994
- }
2995
- manager;
2996
- records = [];
2997
- static fake() {
2998
- const current = bound(MailManager);
2999
- return install(MailManager, new _MailFake(current instanceof _MailFake ? current.manager : current));
3000
- }
3001
- // MailManager ----------------------------------------------------------------------------------------------
3002
- mailer(name) {
3003
- return {
3004
- to: (recipients) => this.pending(name, recipients),
3005
- send: (mailable) => this.sendVia(name, mailable, empty()),
3006
- queue: (mailable) => this.queueVia(name, mailable, empty(), void 0)
3007
- };
3008
- }
3009
- to(recipients) {
3010
- return this.pending(void 0, recipients);
3011
- }
3012
- send(mailable) {
3013
- return this.sendVia(void 0, mailable, empty());
3014
- }
3015
- queue(mailable) {
3016
- return this.queueVia(void 0, mailable, empty(), void 0);
3017
- }
3018
- extend(_transport, _factory) {
3019
- }
3020
- // Assertions ---------------------------------------------------------------------------------------------
3021
- /** Sent (not queued) at least once; a number asserts the count; a filter gets (mailable, record). */
3022
- assertSent(mailable, callback) {
3023
- if (typeof callback === "number") return this.assertSentTimes(mailable, callback);
3024
- const hint = this.queued(mailable).length > 0 ? "\n\nThe mailable was queued: did you mean assertQueued()?" : "";
3025
- assert(this.sent(mailable, callback).length > 0, `The expected [${nameOf(mailable)}] mailable was not sent.${hint}`);
3026
- }
3027
- assertSentTimes(mailable, times = 1) {
3028
- const count = this.sent(mailable).length;
3029
- assert(count === times, `The expected [${nameOf(mailable)}] mailable was sent ${count} times instead of ${times} times.`, { actual: count, expected: times });
3030
- }
3031
- assertNotSent(mailable, callback) {
3032
- assert(this.sent(mailable, callback).length === 0, `The unexpected [${nameOf(mailable)}] mailable was sent.`);
3033
- }
3034
- assertNothingSent() {
3035
- const names = this.records.filter((record) => !record.queued).map((record) => nameOf(record.mailable));
3036
- assert(names.length === 0, `The following mailables were sent unexpectedly:
3037
-
3038
- - ${names.join("\n- ")}`);
3039
- }
3040
- assertSentCount(count) {
3041
- const actual = this.records.filter((record) => !record.queued).length;
3042
- assert(actual === count, `The total number of mailables sent was ${actual} instead of ${count}.`, { actual, expected: count });
3043
- }
3044
- assertQueued(mailable, callback) {
3045
- if (typeof callback === "number") {
3046
- const count = this.queued(mailable).length;
3047
- assert(count === callback, `The expected [${nameOf(mailable)}] mailable was queued ${count} times instead of ${callback} times.`, { actual: count, expected: callback });
3048
- return;
3049
- }
3050
- assert(this.queued(mailable, callback).length > 0, `The expected [${nameOf(mailable)}] mailable was not queued.`);
3051
- }
3052
- assertNotQueued(mailable, callback) {
3053
- assert(this.queued(mailable, callback).length === 0, `The unexpected [${nameOf(mailable)}] mailable was queued.`);
3054
- }
3055
- assertNothingQueued() {
3056
- const names = this.records.filter((record) => record.queued).map((record) => nameOf(record.mailable));
3057
- assert(names.length === 0, `The following mailables were queued unexpectedly:
3058
-
3059
- - ${names.join("\n- ")}`);
3060
- }
3061
- assertQueuedCount(count) {
3062
- const actual = this.records.filter((record) => record.queued).length;
3063
- assert(actual === count, `The total number of mailables queued was ${actual} instead of ${count}.`, { actual, expected: count });
3064
- }
3065
- /** Sent or queued. */
3066
- assertOutgoingCount(count) {
3067
- assert(this.records.length === count, `The total number of outgoing mailables was ${this.records.length} instead of ${count}.`, { actual: this.records.length, expected: count });
3068
- }
3069
- assertNothingOutgoing() {
3070
- this.assertNothingSent();
3071
- this.assertNothingQueued();
3072
- }
3073
- sent(mailable, callback) {
3074
- return this.matching(mailable, false, callback);
3075
- }
3076
- queued(mailable, callback) {
3077
- return this.matching(mailable, true, callback);
3078
- }
3079
- hasSent(mailable) {
3080
- return this.sent(mailable).length > 0;
3081
- }
3082
- hasQueued(mailable) {
3083
- return this.queued(mailable).length > 0;
3084
- }
3085
- // Internals ----------------------------------------------------------------------------------------------
3086
- pending(mailer, recipients) {
3087
- const overrides = { to: addresses(recipients), cc: [], bcc: [] };
3088
- const pending = {
3089
- cc: (cc) => {
3090
- overrides.cc.push(...addresses(cc));
3091
- return pending;
3092
- },
3093
- bcc: (bcc) => {
3094
- overrides.bcc.push(...addresses(bcc));
3095
- return pending;
3096
- },
3097
- send: (mailable) => this.sendVia(mailer, mailable, overrides),
3098
- queue: (mailable) => this.queueVia(mailer, mailable, overrides, void 0),
3099
- later: (delay, mailable) => this.queueVia(mailer, mailable, overrides, delay)
3100
- };
3101
- return pending;
3102
- }
3103
- async sendVia(mailer, mailable, overrides) {
3104
- await this.record(mailer, mailable, overrides, isQueueable(mailable), void 0);
3105
- return void 0;
3106
- }
3107
- async queueVia(mailer, mailable, overrides, delay) {
3108
- await this.record(mailer, mailable, overrides, true, delay);
3109
- }
3110
- async record(mailer, mailable, overrides, queued, delay) {
3111
- const envelope = await mailable.envelope();
3112
- const to = overrides.to.length > 0 ? overrides.to : addresses(envelope.to ?? []);
3113
- const cc = [...addresses(envelope.cc ?? []), ...overrides.cc];
3114
- const bcc = [...addresses(envelope.bcc ?? []), ...overrides.bcc];
3115
- this.records.push(new MailRecord(mailable, mailer, to, cc, bcc, envelope.subject, queued, delay));
3116
- }
3117
- matching(mailable, queued, callback) {
3118
- return this.records.filter((record) => record.queued === queued && matchesAny(record.mailable, [mailable]) && (callback === void 0 || callback(record.mailable, record))).map((record) => record.mailable);
3119
- }
3120
- };
3121
- function empty() {
3122
- return { to: [], cc: [], bcc: [] };
3123
- }
3124
- function addresses(recipients) {
3125
- return [recipients].flat().map((recipient) => {
3126
- if (typeof recipient === "string") return { address: recipient };
3127
- if ("address" in recipient) return recipient.name === void 0 ? { address: recipient.address } : { address: recipient.address, name: recipient.name };
3128
- return recipient.name === void 0 ? { address: recipient.email } : { address: recipient.email, name: recipient.name };
3129
- });
3130
- }
3131
- function includes(list, recipient) {
3132
- const [wanted] = addresses(recipient);
3133
- return list.some((address) => address.address === wanted.address && (wanted.name === void 0 || address.name === wanted.name));
3134
- }
3135
- var AnonymousNotifiable = class {
3136
- constructor(dispatcher) {
3137
- this.dispatcher = dispatcher;
3138
- }
3139
- dispatcher;
3140
- routes = /* @__PURE__ */ new Map();
3141
- route(channel, route) {
3142
- this.routes.set(channel, route);
3143
- return this;
3144
- }
3145
- routeNotificationFor(channel) {
3146
- return this.routes.get(channel);
3147
- }
3148
- notify(notification) {
3149
- return this.dispatcher.send(this, notification);
3150
- }
3151
- };
3152
- function toNotifiableList(notifiables) {
3153
- if (Array.isArray(notifiables)) return notifiables;
3154
- if (typeof notifiables.routeNotificationFor === "function") return [notifiables];
3155
- if (typeof notifiables === "object" && notifiables !== null && Symbol.iterator in notifiables) {
3156
- return [...notifiables];
3157
- }
3158
- return [notifiables];
3159
- }
3160
- function sameNotifiable(a, b) {
3161
- if (a === b) return true;
3162
- const keyOf = (n) => {
3163
- const model = n;
3164
- return typeof model.getKey === "function" ? model.getKey() : void 0;
3165
- };
3166
- const key = keyOf(a);
3167
- return key !== void 0 && a.constructor === b.constructor && key === keyOf(b);
3168
- }
3169
- var NotificationFake = class _NotificationFake {
3170
- constructor(dispatcher = void 0) {
3171
- this.dispatcher = dispatcher;
3172
- }
3173
- dispatcher;
3174
- notifications = [];
3175
- static fake() {
3176
- const current = bound(NotificationDispatcher);
3177
- return install(NotificationDispatcher, new _NotificationFake(current instanceof _NotificationFake ? current.dispatcher : current));
3178
- }
3179
- // NotificationDispatcher -------------------------------------------------------------------------------------
3180
- async send(notifiables, notification) {
3181
- this.record(notifiables, notification);
3182
- }
3183
- async sendNow(notifiables, notification, channels) {
3184
- this.record(notifiables, notification, channels);
3185
- }
3186
- route(channel, route) {
3187
- return new AnonymousNotifiable(this).route(channel, route);
3188
- }
3189
- /** The Notifications provider calls this in boot(); a fake has no channels to register. */
3190
- extend(_channel, _factory) {
3191
- }
3192
- // Assertions ---------------------------------------------------------------------------------------------
3193
- /** Sent to the notifiable(s); a number asserts the count; a filter gets (notification, channels, notifiable). */
3194
- assertSentTo(notifiable, notification, callback) {
3195
- for (const one of toNotifiableList(notifiable)) {
3196
- if (typeof callback === "number") {
3197
- this.assertSentToTimes(one, notification, callback);
3198
- continue;
3199
- }
3200
- assert(this.sent(one, notification, callback).length > 0, `The expected [${nameOf(notification)}] notification was not sent.`);
3201
- }
3202
- }
3203
- assertSentToTimes(notifiable, notification, times = 1) {
3204
- const count = this.sent(notifiable, notification).length;
3205
- assert(count === times, `Expected [${nameOf(notification)}] to be sent ${times} times, but was sent ${count} times.`, { actual: count, expected: times });
3206
- }
3207
- assertNotSentTo(notifiable, notification, callback) {
3208
- for (const one of toNotifiableList(notifiable)) {
3209
- assert(this.sent(one, notification, callback).length === 0, `The unexpected [${nameOf(notification)}] notification was sent.`);
3210
- }
3211
- }
3212
- /** Sent through Notification.route(...) (an anonymous notifiable). */
3213
- assertSentOnDemand(notification, callback) {
3214
- const matching = this.notifications.filter(
3215
- (record) => record.notifiable instanceof AnonymousNotifiable && matchesAny(record.notification, [notification]) && (typeof callback !== "function" || callback(record.notification, record.channels, record.notifiable))
3216
- );
3217
- if (typeof callback === "number") {
3218
- assert(matching.length === callback, `Expected [${nameOf(notification)}] to be sent on demand ${callback} times, but was sent ${matching.length} times.`);
3219
- return;
3220
- }
3221
- assert(matching.length > 0, `The expected [${nameOf(notification)}] notification was not sent on demand.`);
3222
- }
3223
- assertNothingSent() {
3224
- const names = this.notifications.map((record) => nameOf(record.notification));
3225
- assert(names.length === 0, `Notifications were sent unexpectedly:
3226
-
3227
- - ${names.join("\n- ")}`);
3228
- }
3229
- assertNothingSentTo(notifiable) {
3230
- const count = this.notifications.filter((record) => sameNotifiable(record.notifiable, notifiable)).length;
3231
- assert(count === 0, "Notifications were sent unexpectedly.");
3232
- }
3233
- /** Sent this many times in total, to anyone. */
3234
- assertSentTimes(notification, times) {
3235
- const count = this.notifications.filter((record) => matchesAny(record.notification, [notification])).length;
3236
- assert(count === times, `Expected [${nameOf(notification)}] to be sent ${times} times, but was sent ${count} times.`, { actual: count, expected: times });
3237
- }
3238
- assertCount(expected) {
3239
- const actual = this.notifications.length;
3240
- assert(actual === expected, `Expected ${expected} notifications to be sent, but ${actual} were sent.`, { actual, expected });
3241
- }
3242
- sent(notifiable, notification, callback) {
3243
- return this.notifications.filter(
3244
- (record) => sameNotifiable(record.notifiable, notifiable) && matchesAny(record.notification, [notification]) && (callback === void 0 || callback(record.notification, record.channels, record.notifiable))
3245
- ).map((record) => record.notification);
3246
- }
3247
- hasSent(notifiable, notification) {
3248
- return this.sent(notifiable, notification).length > 0;
3249
- }
3250
- record(notifiables, notification, channels) {
3251
- notification.id ??= randomUUID3();
3252
- for (const notifiable of toNotifiableList(notifiables)) {
3253
- this.notifications.push({ notifiable, notification, channels: channels ?? notification.via(notifiable) });
3254
- }
3255
- }
3256
- };
3257
- var FakeDiskError = class extends Error {
3258
- constructor(name, message, path) {
3259
- super(message);
3260
- this.path = path;
3261
- this.name = name;
3262
- }
3263
- path;
3264
- kind = "Server";
3265
- status = 500;
3266
- };
3267
- var MIME = {
3268
- txt: "text/plain",
3269
- html: "text/html",
3270
- css: "text/css",
3271
- js: "text/javascript",
3272
- json: "application/json",
3273
- csv: "text/csv",
3274
- pdf: "application/pdf",
3275
- zip: "application/zip",
3276
- png: "image/png",
3277
- jpg: "image/jpeg",
3278
- jpeg: "image/jpeg",
3279
- gif: "image/gif",
3280
- webp: "image/webp",
3281
- svg: "image/svg+xml",
3282
- mp3: "audio/mpeg",
3283
- mp4: "video/mp4"
3284
- };
3285
- function guessMimeType(path) {
3286
- const extension = path.split(".").pop()?.toLowerCase() ?? "";
3287
- return MIME[extension];
3288
- }
3289
- function hashName(file) {
3290
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
3291
- const name = [...randomBytes2(40)].map((byte) => alphabet[byte % alphabet.length]).join("");
3292
- const extension = file.extension();
3293
- return extension === "" ? name : `${name}.${extension}`;
3294
- }
3295
- function normalize(path) {
3296
- const parts = [];
3297
- for (const segment of path.replaceAll("\\", "/").split("/")) {
3298
- if (segment === "" || segment === ".") continue;
3299
- if (segment !== "..") parts.push(segment);
3300
- else if (parts.pop() === void 0) {
3301
- throw new FakeDiskError("PathTraversalError", `Path traversal detected: "${path}" resolves outside the disk root.`, path);
3302
- }
3303
- }
3304
- return parts.join("/");
3305
- }
3306
- var within = (path, directory) => directory === "" || path.startsWith(`${directory}/`);
3307
- var FakeDisk = class {
3308
- constructor(name, urlPrefix = `/storage`) {
3309
- this.name = name;
3310
- this.urlPrefix = urlPrefix;
3311
- }
3312
- name;
3313
- urlPrefix;
3314
- /** Stored files by normalized path. */
3315
- stored = /* @__PURE__ */ new Map();
3316
- /** Directories made explicitly (others exist because files are in them). */
3317
- made = /* @__PURE__ */ new Set();
3318
- async exists(path) {
3319
- const key = normalize(path);
3320
- return this.stored.has(key) || this.isDirectory(key);
3321
- }
3322
- async missing(path) {
3323
- return !await this.exists(path);
3324
- }
3325
- async get(path) {
3326
- return new TextDecoder().decode(this.read(path).bytes);
3327
- }
3328
- async getBytes(path) {
3329
- return this.read(path).bytes.slice();
3330
- }
3331
- async readStream(path) {
3332
- const bytes = this.read(path).bytes.slice();
3333
- return new ReadableStream({
3334
- start(controller) {
3335
- controller.enqueue(bytes);
3336
- controller.close();
3337
- }
3338
- });
3339
- }
3340
- async put(path, contents, options = {}) {
3341
- const key = normalize(path);
3342
- const bytes = typeof contents === "string" ? new TextEncoder().encode(contents) : contents instanceof Uint8Array ? contents.slice() : await collect(contents);
3343
- this.stored.set(key, { bytes, visibility: options.visibility ?? "private", mimeType: options.mimeType ?? guessMimeType(key), modified: /* @__PURE__ */ new Date() });
3344
- return true;
3345
- }
3346
- putFile(directory, file, options = {}) {
3347
- return this.putFileAs(directory, file, hashName(file), options);
3348
- }
3349
- async putFileAs(directory, file, name, options = {}) {
3350
- const path = normalize(`${directory}/${name}`);
3351
- await this.put(path, new Uint8Array(await file.arrayBuffer()), { ...options, mimeType: options.mimeType ?? file.mimeType });
3352
- return path;
3353
- }
3354
- async delete(paths) {
3355
- for (const path of [paths].flat()) this.stored.delete(normalize(path));
3356
- return true;
3357
- }
3358
- async copy(from, to) {
3359
- const file = this.read(from);
3360
- this.stored.set(normalize(to), { ...file, bytes: file.bytes.slice(), modified: /* @__PURE__ */ new Date() });
3361
- return true;
3362
- }
3363
- async move(from, to) {
3364
- await this.copy(from, to);
3365
- this.stored.delete(normalize(from));
3366
- return true;
3367
- }
3368
- async size(path) {
3369
- return this.read(path).bytes.byteLength;
3370
- }
3371
- async lastModified(path) {
3372
- return this.read(path).modified;
3373
- }
3374
- async mimeType(path) {
3375
- return this.read(path).mimeType;
3376
- }
3377
- url(path) {
3378
- return `${this.urlPrefix}/${normalize(path)}`;
3379
- }
3380
- async temporaryUrl(path, expiresAt) {
3381
- return `${this.url(path)}?expiration=${Math.floor(expiresAt.getTime() / 1e3)}`;
3382
- }
3383
- async files(directory = "", recursive = false) {
3384
- const dir = normalize(directory);
3385
- return [...this.stored.keys()].filter((path) => within(path, dir) && (recursive || !path.slice(dir === "" ? 0 : dir.length + 1).includes("/"))).sort();
3386
- }
3387
- async directories(directory = "", recursive = false) {
3388
- const dir = normalize(directory);
3389
- const all = /* @__PURE__ */ new Set([...this.made]);
3390
- for (const path of this.stored.keys()) {
3391
- const parts = path.split("/").slice(0, -1);
3392
- for (let i = 1; i <= parts.length; i++) all.add(parts.slice(0, i).join("/"));
3393
- }
3394
- return [...all].filter((path) => within(path, dir) && path !== dir && (recursive || !path.slice(dir === "" ? 0 : dir.length + 1).includes("/"))).sort();
3395
- }
3396
- async makeDirectory(path) {
3397
- this.made.add(normalize(path));
3398
- return true;
3399
- }
3400
- async deleteDirectory(path) {
3401
- const dir = normalize(path);
3402
- for (const file of [...this.stored.keys()]) if (within(file, dir)) this.stored.delete(file);
3403
- for (const directory of [...this.made]) if (directory === dir || within(directory, dir)) this.made.delete(directory);
3404
- return true;
3405
- }
3406
- /** A virtual absolute path (the files only exist in memory). */
3407
- path(path) {
3408
- return `/fake-disks/${this.name}/${normalize(path)}`;
3409
- }
3410
- // Assertions (Laravel's FilesystemAdapter test helpers) -----------------------------------------------------
3411
- /** The file(s) exist; with `content`, the (single) file has exactly that content. */
3412
- async assertExists(path, content) {
3413
- for (const one of [path].flat()) {
3414
- assert(this.stored.has(normalize(one)), `Unable to find a file or directory at path [${one}].`);
3415
- if (content !== void 0) {
3416
- const actual = await this.get(one);
3417
- assert(actual === content, `File or directory [${one}] was found, but content [${actual}] does not match [${content}].`, { actual, expected: content });
3418
- }
3419
- }
3420
- return this;
3421
- }
3422
- async assertMissing(path) {
3423
- for (const one of [path].flat()) assert(!this.stored.has(normalize(one)), `Found unexpected file or directory at path [${one}].`);
3424
- return this;
3425
- }
3426
- async assertDirectoryEmpty(path) {
3427
- const files = await this.files(path, true);
3428
- assert(files.length === 0, `Directory [${path}] is not empty.`, { actual: files });
3429
- return this;
3430
- }
3431
- async assertCount(path, count, recursive = false) {
3432
- const actual = (await this.files(path, recursive)).length;
3433
- assert(actual === count, `Expected [${count}] files at [${path}], but found [${actual}].`, { actual, expected: count });
3434
- return this;
3435
- }
3436
- read(path) {
3437
- const file = this.stored.get(normalize(path));
3438
- if (file === void 0) throw new FakeDiskError("FileNotFoundError", `File not found at path: ${path}`, path);
3439
- return file;
3440
- }
3441
- isDirectory(key) {
3442
- return this.made.has(key) || [...this.stored.keys()].some((path) => key !== "" && path.startsWith(`${key}/`));
3443
- }
3444
- };
3445
- async function collect(stream) {
3446
- const chunks = [];
3447
- for await (const chunk2 of stream) chunks.push(chunk2);
3448
- const out = new Uint8Array(chunks.reduce((total, chunk2) => total + chunk2.byteLength, 0));
3449
- let offset = 0;
3450
- for (const chunk2 of chunks) {
3451
- out.set(chunk2, offset);
3452
- offset += chunk2.byteLength;
3453
- }
3454
- return out;
3455
- }
3456
- var StorageFake = class _StorageFake {
3457
- constructor(manager, defaultDisk) {
3458
- this.manager = manager;
3459
- this.defaultDisk = defaultDisk;
3460
- }
3461
- manager;
3462
- defaultDisk;
3463
- disks = /* @__PURE__ */ new Map();
3464
- static fake(disk) {
3465
- const current = bound(FilesystemManager);
3466
- const config = bound(ConfigRepository);
3467
- const fake = current instanceof _StorageFake ? current : install(FilesystemManager, new _StorageFake(current, config?.get("filesystems.default", "local") ?? "local"));
3468
- const name = disk ?? fake.defaultDisk;
3469
- const faked = new FakeDisk(name);
3470
- fake.disks.set(name, faked);
3471
- return faked;
3472
- }
3473
- disk(name) {
3474
- const disk = name ?? this.defaultDisk;
3475
- const faked = this.disks.get(disk);
3476
- if (faked !== void 0) return faked;
3477
- if (this.manager === void 0) fail(`Disk [${disk}] is not faked and there is no real FilesystemManager.`);
3478
- return this.manager.disk(disk);
3479
- }
3480
- extend(driver, factory) {
3481
- this.manager?.extend(driver, factory);
3482
- }
3483
- exists(path) {
3484
- return this.disk().exists(path);
3485
- }
3486
- missing(path) {
3487
- return this.disk().missing(path);
3488
- }
3489
- get(path) {
3490
- return this.disk().get(path);
3491
- }
3492
- getBytes(path) {
3493
- return this.disk().getBytes(path);
3494
- }
3495
- readStream(path) {
3496
- return this.disk().readStream(path);
3497
- }
3498
- put(path, contents, options) {
3499
- return this.disk().put(path, contents, options);
3500
- }
3501
- putFile(directory, file, options) {
3502
- return this.disk().putFile(directory, file, options);
3503
- }
3504
- putFileAs(directory, file, name, options) {
3505
- return this.disk().putFileAs(directory, file, name, options);
3506
- }
3507
- delete(paths) {
3508
- return this.disk().delete(paths);
3509
- }
3510
- copy(from, to) {
3511
- return this.disk().copy(from, to);
3512
- }
3513
- move(from, to) {
3514
- return this.disk().move(from, to);
3515
- }
3516
- size(path) {
3517
- return this.disk().size(path);
3518
- }
3519
- lastModified(path) {
3520
- return this.disk().lastModified(path);
3521
- }
3522
- mimeType(path) {
3523
- return this.disk().mimeType(path);
3524
- }
3525
- url(path) {
3526
- return this.disk().url(path);
3527
- }
3528
- temporaryUrl(path, expiresAt) {
3529
- return this.disk().temporaryUrl(path, expiresAt);
3530
- }
3531
- files(directory, recursive) {
3532
- return this.disk().files(directory, recursive);
3533
- }
3534
- directories(directory, recursive) {
3535
- return this.disk().directories(directory, recursive);
3536
- }
3537
- makeDirectory(path) {
3538
- return this.disk().makeDirectory(path);
3539
- }
3540
- deleteDirectory(path) {
3541
- return this.disk().deleteDirectory(path);
3542
- }
3543
- path(path) {
3544
- return this.disk().path(path);
3545
- }
3546
- };
3547
- var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
3548
- let c = n;
3549
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3550
- return c >>> 0;
3551
- });
3552
- function crc32(bytes) {
3553
- let crc = 4294967295;
3554
- for (const byte of bytes) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
3555
- return (crc ^ 4294967295) >>> 0;
3556
- }
3557
- function chunk(type, data) {
3558
- const out = new Uint8Array(12 + data.length);
3559
- const view = new DataView(out.buffer);
3560
- view.setUint32(0, data.length);
3561
- out.set(new TextEncoder().encode(type), 4);
3562
- out.set(data, 8);
3563
- view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
3564
- return out;
3565
- }
3566
- function png(width, height) {
3567
- const header = new Uint8Array(13);
3568
- const view = new DataView(header.buffer);
3569
- view.setUint32(0, width);
3570
- view.setUint32(4, height);
3571
- header.set([8, 2, 0, 0, 0], 8);
3572
- const rows = new Uint8Array((width * 3 + 1) * height).fill(255);
3573
- for (let y = 0; y < height; y++) rows[y * (width * 3 + 1)] = 0;
3574
- const parts = [new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(rows)), chunk("IEND", new Uint8Array())];
3575
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
3576
- let offset = 0;
3577
- for (const part of parts) {
3578
- out.set(part, offset);
3579
- offset += part.length;
3580
- }
3581
- return out;
3582
- }
3583
- var FakeFile = {
3584
- /** A file of `kilobytes` KiB (zero bytes), typed by `mimeType` or the extension. */
3585
- create(name, kilobytes = 0, mimeType) {
3586
- return new File([new Uint8Array(Math.round(kilobytes * 1024))], name, { type: mimeType ?? guessMimeType(name) ?? "application/octet-stream" });
3587
- },
3588
- /** A real PNG image of the given size (typed by the name's extension, like Laravel's image()). */
3589
- image(name, width = 10, height = 10) {
3590
- return new File([png(width, height)], name, { type: guessMimeType(name) ?? "image/png" });
3591
- }
3592
- };
3593
-
3594
- export {
3595
- Repository,
3596
- env,
3597
- ApplicationBuilder,
3598
- ProviderBootError,
3599
- ProviderRegisterError,
3600
- ViteError,
3601
- ViteHtml,
3602
- Vite,
3603
- currentVite,
3604
- vite,
3605
- viteReactRefresh,
3606
- Application2 as Application,
3607
- ClosureKernel,
3608
- PendingCommand,
3609
- RefreshDatabaseState,
3610
- runCommand,
3611
- usingInMemoryDatabase,
3612
- RefreshDatabase,
3613
- DatabaseMigrations,
3614
- DatabaseTransactions,
3615
- TESTING_ENVIRONMENT,
3616
- applyEnvironment,
3617
- AssertableJson,
3618
- inertiaPageOf,
3619
- AssertableInertia,
3620
- cookiePrefix,
3621
- encryptCookie,
3622
- decryptCookie,
3623
- isExpired,
3624
- responseCookies,
3625
- cookieHeader,
3626
- TestResponse,
3627
- recordingViewFactory,
3628
- recordingExceptionHandler,
3629
- actingAsAuthFactory,
3630
- HttpClient,
3631
- flatten,
3632
- InteractsWithApplication,
3633
- MakesHttpRequests,
3634
- InteractsWithDatabase,
3635
- InteractsWithConsole,
3636
- TestCase,
3637
- install,
3638
- nameOf,
3639
- matches,
3640
- EventFake,
3641
- BusFake,
3642
- QueueFake,
3643
- MailRecord,
3644
- MailFake,
3645
- addresses,
3646
- AnonymousNotifiable,
3647
- NotificationFake,
3648
- guessMimeType,
3649
- FakeDisk,
3650
- StorageFake,
3651
- FakeFile
3652
- };
3653
- //# sourceMappingURL=chunk-UPZAML2Q.js.map