@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 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 profile', async ({ browser, mailbox }) => {
937
- await browser.auth.signIn.magicLink({ email: 'ada@shop.test' });
938
- await browser.visit((await mailbox('ada@shop.test').last()).link!);
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
- expect(await browser.api.get('/profile')).toMatchObject({ email: 'ada@shop.test' });
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
- - **`db`** — the transaction of the database the endpoints name, typed by its
948
- schema.
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.database, options.modules ?? {});
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: app.database ? await state.transactions.get(app.database.id, urlOf(app.database, app.envParser), app.database.clientConfig) : void 0,
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 (!app.readMail) throw new NoInbox();
547
- state.addresses.add(address);
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, database, modules) {
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 database a test sees as `db`: the one named, or else the one the
616
- * endpoints were given with `.database()` — found by the service they hold,
617
- * which is the construct's own.
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 databaseOf(databases, endpoints, named) {
620
- if (named) {
621
- const found = databases.find(({ id }) => id === named);
622
- if (!found) throw new UnknownDatabase(named, databases.map(({ id }) => id));
623
- return found;
624
- }
625
- const services = new Set(endpoints.map((e) => e.databaseService).filter(Boolean));
626
- return databases.find((database) => services.has(database.service));
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
- /** `featureTest({ database })` named a database the app does not declare. */
786
+ /** `db.get(name)` named no database the test is handed. */
723
787
  var UnknownDatabase = class extends Error {
724
- id;
725
- declared;
726
- constructor(id, declared) {
727
- super(`featureTest was asked for the database '${id}', and the app declares ${declared.length ? declared.map((d) => `'${d}'`).join(", ") : "none"}.`);
728
- this.id = id;
729
- this.declared = declared;
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