@geekmidas/constructs 10.0.0-alpha.31 → 10.0.0-alpha.33
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.
- package/README.md +21 -6
- package/dist/adaptors/testing.cjs +103 -25
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +91 -18
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +91 -18
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +101 -26
- package/dist/adaptors/testing.mjs.map +1 -1
- package/package.json +31 -31
package/README.md
CHANGED
|
@@ -933,19 +933,34 @@ Map it once in the app's `package.json` — `"imports": { "#test":
|
|
|
933
933
|
```typescript
|
|
934
934
|
import { it } from '#test';
|
|
935
935
|
|
|
936
|
-
it('shows the signed-in user their
|
|
937
|
-
await
|
|
938
|
-
await
|
|
936
|
+
it('shows the signed-in user their orders', async ({ browser, db, factories }) => {
|
|
937
|
+
const factory = await factories.get('database');
|
|
938
|
+
await factory.insert('orders', { email: 'ada@shop.test' });
|
|
939
939
|
|
|
940
|
-
|
|
940
|
+
await browser.signIn('ada@shop.test');
|
|
941
|
+
const { orders } = await browser.api.get('/orders');
|
|
942
|
+
expect(orders).toHaveLength(1);
|
|
943
|
+
|
|
944
|
+
const app = await db.get('database');
|
|
945
|
+
expect(await app.selectFrom('orders').selectAll().execute()).toHaveLength(1);
|
|
941
946
|
});
|
|
942
947
|
```
|
|
943
948
|
|
|
944
949
|
- **`browser`** — already the global `fetch`; its cookie jar follows the
|
|
945
950
|
browser's rules, so a session cookie set for the wrong domain never reaches
|
|
946
951
|
the API.
|
|
947
|
-
- **`
|
|
948
|
-
|
|
952
|
+
- **`browser.signIn(email)`** — when the auth server has the magic-link plugin
|
|
953
|
+
and the app sends mail: the link requested, read from the inbox and
|
|
954
|
+
followed, as a person signs in. Returns the session the auth server reports.
|
|
955
|
+
- **`db.get(name)`** — the app's own databases by service name
|
|
956
|
+
(`db.get('database')`), each this test's transaction, typed by its schema.
|
|
957
|
+
Opened on first use by whatever reaches it first — the test, a factory, an
|
|
958
|
+
endpoint — and the same one for all of them. A tenant an auth server owns is
|
|
959
|
+
not among them: it is reached through the auth server, as the app reaches
|
|
960
|
+
it. Nor is a reader.
|
|
961
|
+
- **`factories.get(name)`** — each of those databases' factory, from
|
|
962
|
+
`test/factories/<construct>.ts` at the project root, on the same
|
|
963
|
+
transaction: `(await factories.get('database')).insert('users', …)`.
|
|
949
964
|
- **`mailbox(address)`** — the mail actually sent, read from Mailpit.
|
|
950
965
|
- **`published(topic | queue)`** — what the test published or enqueued.
|
|
951
966
|
Nothing is delivered: a subscriber or a worker is run on its own with
|
|
@@ -506,10 +506,14 @@ function featureTest(options = {}) {
|
|
|
506
506
|
const manifest = options.manifest ?? loadTestManifest();
|
|
507
507
|
const BrowserClass = options.browser ?? _geekmidas_testkit_browser.Browser;
|
|
508
508
|
let app;
|
|
509
|
+
let factoryDatabases = [];
|
|
510
|
+
let owned = [];
|
|
509
511
|
let network;
|
|
510
512
|
let restoreFetch = () => {};
|
|
511
513
|
(0, vitest.beforeAll)(async () => {
|
|
512
|
-
app = await load(manifest, options.
|
|
514
|
+
app = await load(manifest, options.modules ?? {});
|
|
515
|
+
owned = ownDatabases(app, manifest);
|
|
516
|
+
factoryDatabases = databasesFor(options.factories ?? {}, owned);
|
|
513
517
|
for (const database of app.databases) bindToTests(database);
|
|
514
518
|
network = (0, msw_node.setupServer)(...surfaceHandlers(app.endpoints, manifest.env), ...app.auths.map((auth) => authHandler(auth, manifest.env, app.envParser)));
|
|
515
519
|
network.listen({ onUnhandledRequest(request, print) {
|
|
@@ -532,6 +536,11 @@ function featureTest(options = {}) {
|
|
|
532
536
|
addresses: /* @__PURE__ */ new Set(),
|
|
533
537
|
published: /* @__PURE__ */ new Map()
|
|
534
538
|
};
|
|
539
|
+
const readMail = app.readMail;
|
|
540
|
+
if (readMail) state.mailbox = (address) => {
|
|
541
|
+
state.addresses.add(address);
|
|
542
|
+
return readMail(address);
|
|
543
|
+
};
|
|
535
544
|
contexts.set(id, state);
|
|
536
545
|
await state.discovery.register(recorders(app.channels, state));
|
|
537
546
|
try {
|
|
@@ -539,13 +548,29 @@ function featureTest(options = {}) {
|
|
|
539
548
|
const browser = new BrowserClass();
|
|
540
549
|
const restore = browser.install();
|
|
541
550
|
try {
|
|
551
|
+
const transaction = (database) => state.transactions.get(database.id, urlOf(database, app.envParser), database.clientConfig);
|
|
552
|
+
const db = { get: (name) => {
|
|
553
|
+
const database = owned.find((candidate) => candidate.service.serviceName === name);
|
|
554
|
+
if (!database) throw new UnknownDatabase(name, owned.map((candidate) => candidate.service.serviceName));
|
|
555
|
+
return transaction(database);
|
|
556
|
+
} };
|
|
557
|
+
const built = /* @__PURE__ */ new Map();
|
|
542
558
|
await fn({
|
|
543
559
|
browser,
|
|
544
|
-
db
|
|
560
|
+
db,
|
|
561
|
+
factories: { get: (name) => {
|
|
562
|
+
const database = factoryDatabases.find(([candidate]) => candidate === name)?.[1];
|
|
563
|
+
if (!database) throw new UnknownFactory(name, factoryDatabases.map(([candidate]) => candidate));
|
|
564
|
+
let factory = built.get(name);
|
|
565
|
+
if (!factory) {
|
|
566
|
+
factory = transaction(database).then(options.factories[name]);
|
|
567
|
+
built.set(name, factory);
|
|
568
|
+
}
|
|
569
|
+
return factory;
|
|
570
|
+
} },
|
|
545
571
|
mailbox: (address) => {
|
|
546
|
-
if (!
|
|
547
|
-
state.
|
|
548
|
-
return app.readMail(address);
|
|
572
|
+
if (!state.mailbox) throw new NoInbox();
|
|
573
|
+
return state.mailbox(address);
|
|
549
574
|
},
|
|
550
575
|
published: (channel) => [...state.published.get(channel.id) ?? []],
|
|
551
576
|
subscriber: (subscriber) => new TestSubscriberAdaptor(subscriber, state.discovery),
|
|
@@ -571,7 +596,7 @@ function featureTest(options = {}) {
|
|
|
571
596
|
* The same modules the app runs, so a construct here *is* the app's construct —
|
|
572
597
|
* the one its endpoints were built from — never a copy declared again.
|
|
573
598
|
*/
|
|
574
|
-
async function load(manifest,
|
|
599
|
+
async function load(manifest, modules) {
|
|
575
600
|
const envParser = new _geekmidas_envkit.EnvironmentParser({ ...manifest.env });
|
|
576
601
|
const imported = async ({ file, export: name }) => (modules[file] ?? await import(file))[name];
|
|
577
602
|
const constructs = await Promise.all(Object.values(manifest.constructs).map(({ source }) => imported(source)));
|
|
@@ -589,7 +614,6 @@ async function load(manifest, database, modules) {
|
|
|
589
614
|
auths,
|
|
590
615
|
emails,
|
|
591
616
|
channels,
|
|
592
|
-
database: databaseOf(databases, endpoints, database),
|
|
593
617
|
...inbox ? { readMail: (0, _geekmidas_testkit_mailbox.createMailbox)({ inbox }) } : {}
|
|
594
618
|
};
|
|
595
619
|
}
|
|
@@ -612,18 +636,22 @@ function recorders(channels, state) {
|
|
|
612
636
|
});
|
|
613
637
|
}
|
|
614
638
|
/**
|
|
615
|
-
* The
|
|
616
|
-
*
|
|
617
|
-
*
|
|
639
|
+
* The databases a test is handed: the app's own. A schema tenant an auth server
|
|
640
|
+
* owns is reached through that server, as the app reaches it, and a reader is
|
|
641
|
+
* the same database through a read-only role — neither is another database to
|
|
642
|
+
* test against.
|
|
618
643
|
*/
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
644
|
+
function ownDatabases(app, manifest) {
|
|
645
|
+
const owners = new Set(app.auths.map((auth) => auth.databaseId));
|
|
646
|
+
return app.databases.filter((database) => !owners.has(database.id) && manifest.constructs[database.id]?.kind !== "database-reader");
|
|
647
|
+
}
|
|
648
|
+
/** Each factory's database, found by the service name it is keyed by. */
|
|
649
|
+
function databasesFor(factories, databases) {
|
|
650
|
+
return Object.keys(factories).map((name) => {
|
|
651
|
+
const database = databases.find((candidate) => candidate.service.serviceName === name);
|
|
652
|
+
if (!database) throw new UnknownFactory(name, databases.map((candidate) => candidate.service.serviceName));
|
|
653
|
+
return [name, database];
|
|
654
|
+
});
|
|
627
655
|
}
|
|
628
656
|
/**
|
|
629
657
|
* Make a database resolve, inside a test, to that test's transaction.
|
|
@@ -701,6 +729,42 @@ function trim(url) {
|
|
|
701
729
|
function isLocal(url) {
|
|
702
730
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname.endsWith(".localhost");
|
|
703
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Sign a test's browser in the way a person does: ask the auth server for a
|
|
734
|
+
* magic link, open the email it sent, follow the link.
|
|
735
|
+
*
|
|
736
|
+
* What the generated `browser.signIn(email)` calls. The address's mail is
|
|
737
|
+
* cleared first — so the link is this request's, not one an earlier test left
|
|
738
|
+
* — and cleared again after the test, like any address a test reads. Returns
|
|
739
|
+
* the session the auth server then reports, as better-auth hands it back.
|
|
740
|
+
*/
|
|
741
|
+
async function signInWithMagicLink(browser, auth, email) {
|
|
742
|
+
const context = (0, _geekmidas_testkit_context.currentTestContext)();
|
|
743
|
+
const state = context && contexts.get(context.id);
|
|
744
|
+
if (!state) throw new UnknownTestContext("signIn", context?.id);
|
|
745
|
+
if (!state.mailbox) throw new NoInbox();
|
|
746
|
+
const inbox = state.mailbox(email);
|
|
747
|
+
await inbox.clear();
|
|
748
|
+
const requested = await auth.signIn.magicLink({ email });
|
|
749
|
+
if (requested.error) throw new SignInFailed(email, `the auth server refused the magic link: ${requested.error.message ?? "no reason given"}`);
|
|
750
|
+
const mail = await inbox.last();
|
|
751
|
+
if (!mail.link) throw new SignInFailed(email, "its email held no link");
|
|
752
|
+
await browser.visit(mail.link);
|
|
753
|
+
const { data } = await auth.getSession();
|
|
754
|
+
if (!data) throw new SignInFailed(email, "the auth server reported no session after the link was opened");
|
|
755
|
+
return data;
|
|
756
|
+
}
|
|
757
|
+
/** `browser.signIn` finished without a session. */
|
|
758
|
+
var SignInFailed = class extends Error {
|
|
759
|
+
email;
|
|
760
|
+
reason;
|
|
761
|
+
constructor(email, reason) {
|
|
762
|
+
super(`Signing ${email} in failed: ${reason}.`);
|
|
763
|
+
this.email = email;
|
|
764
|
+
this.reason = reason;
|
|
765
|
+
this.name = "SignInFailed";
|
|
766
|
+
}
|
|
767
|
+
};
|
|
704
768
|
/** A request reached an in-process server without a test to belong to. */
|
|
705
769
|
var UnknownTestContext = class extends Error {
|
|
706
770
|
url;
|
|
@@ -719,20 +783,32 @@ var NoInbox = class extends Error {
|
|
|
719
783
|
this.name = "NoInbox";
|
|
720
784
|
}
|
|
721
785
|
};
|
|
722
|
-
/** `
|
|
786
|
+
/** `db.get(name)` named no database the test is handed. */
|
|
723
787
|
var UnknownDatabase = class extends Error {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
constructor(
|
|
727
|
-
super(`
|
|
728
|
-
this.
|
|
729
|
-
this.
|
|
788
|
+
database;
|
|
789
|
+
known;
|
|
790
|
+
constructor(database, known) {
|
|
791
|
+
super(`db.get('${database}') names no database of this app's. ` + (known.length > 0 ? `Its databases: ${known.join(", ")}. ` : "It declares no database of its own. ") + "A tenant an auth server owns is reached through that server, and a reader through the database it reads.");
|
|
792
|
+
this.database = database;
|
|
793
|
+
this.known = known;
|
|
730
794
|
this.name = "UnknownDatabase";
|
|
731
795
|
}
|
|
732
796
|
};
|
|
797
|
+
/** A factory keyed by a service name no database the app declares has. */
|
|
798
|
+
var UnknownFactory = class extends Error {
|
|
799
|
+
factory;
|
|
800
|
+
known;
|
|
801
|
+
constructor(factory, known) {
|
|
802
|
+
super(`There is a test factory for '${factory}', and no database this app declares is called that. ` + (known.length > 0 ? `Its databases: ${known.join(", ")}. ` : "It declares no database. ") + `Name the file after the database construct: test/factories/<construct>.ts.`);
|
|
803
|
+
this.factory = factory;
|
|
804
|
+
this.known = known;
|
|
805
|
+
this.name = "UnknownFactory";
|
|
806
|
+
}
|
|
807
|
+
};
|
|
733
808
|
//#endregion
|
|
734
809
|
exports.NoInbox = NoInbox;
|
|
735
810
|
exports.NoTestManifest = NoTestManifest;
|
|
811
|
+
exports.SignInFailed = SignInFailed;
|
|
736
812
|
exports.TEST_CONTEXT_HEADER = _geekmidas_testkit_context.TEST_CONTEXT_HEADER;
|
|
737
813
|
exports.TEST_MANIFEST_ENV = TEST_MANIFEST_ENV;
|
|
738
814
|
exports.TestEndpointAdaptor = TestEndpointAdaptor;
|
|
@@ -740,9 +816,11 @@ exports.TestFunctionAdaptor = TestFunctionAdaptor;
|
|
|
740
816
|
exports.TestQueueAdaptor = TestQueueAdaptor;
|
|
741
817
|
exports.TestSubscriberAdaptor = TestSubscriberAdaptor;
|
|
742
818
|
exports.UnknownDatabase = UnknownDatabase;
|
|
819
|
+
exports.UnknownFactory = UnknownFactory;
|
|
743
820
|
exports.UnknownTestContext = UnknownTestContext;
|
|
744
821
|
exports.createMswHandlers = createMswHandlers;
|
|
745
822
|
exports.featureTest = featureTest;
|
|
746
823
|
exports.loadTestManifest = loadTestManifest;
|
|
824
|
+
exports.signInWithMagicLink = signInWithMagicLink;
|
|
747
825
|
|
|
748
826
|
//# sourceMappingURL=testing.cjs.map
|