Repository: getindiekit/indiekit (js)

Issue:
Allow different stores to be set post files versus media files
Posts filed are small text files; typically something that you would want to host using Git so that you can track changes.

Media files are large binary data; typically something you don’t want to host in a Git repository if you can help it. These are better hosted on dedicated object/bucket storage.

Currently you can only choose one type of content store; might it be possible to configure different stores for posts and files?

Likely a requirement for #14 and #532.

Pull request:
Allow publications to specify a separate store for media content
Closes #533.

* Updated `publication.store` to use either configured value, falling back to the first content store plugin registered (saved to `application.stores[]`)  
* Add `publication.mediaStore` to use either configured value, else falling back to `publication.store`.

This means existing configurations won’t break, but publication configuration’s can be updated to specify a separate store to save media files too.

Patch:
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index a1a8fda2..632a8b0e 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -36,24 +36,32 @@ Indiekit’s configuration file uses the following top-level properties:
 {
   "application": {
     "locale": "de",
-    "name": "Unabhängigkeit"
+    "name": "Indiekit-Server"
   },
   "publication": {
     "locale": "de",
     "me": "https://website.example",
-    "postTypes": {
-      "event": {
-        "name": "Veranstaltungen"
-      }
-    }
+    "mediaStore": "@indiekit/store-s3",
+    "store": "@indiekit/store-github"
   },
   "plugins": [
     "@indiekit/post-type-event",
-    "@indiekit/preset-hugo"
+    "@indiekit/preset-hugo",
+    "@indiekit/store-github",
+    "@indiekit/store-s3"
   ],
   "@indiekit/preset-hugo": {
     "frontMatterFormat": "yaml"
-  }
+  },
+  "@indiekit/store-github": {
+    "user": "example",
+    "repo": "website"
+  },
+  "@indiekit/store-s3": {
+    "region": "eu-central-1",
+    "endpoint": "https://s3-example-eu-central-1.amazonaws.com",
+    "bucket": "website"
+  },
 }
 ```
 
@@ -61,24 +69,32 @@ Indiekit’s configuration file uses the following top-level properties:
 export default {
   application: {
     locale: "de",
-    name: "Unabhängigkeit"
+    name: "Indiekit-Server"
   },
   publication: {
     locale: "de",
     me: "https://website.example",
-    postTypes: {
-      event: {
-        name: "Veranstaltungen"
-      }
-    }
+    mediaStore: "@indiekit/store-s3",
+    store: "@indiekit/store-github"
   },
   plugins: [
     "@indiekit/post-type-event",
-    "@indiekit/preset-hugo"
+    "@indiekit/preset-hugo",
+    "@indiekit/store-github",
+    "@indiekit/store-s3"
   ],
   "@indiekit/preset-hugo": {
     frontMatterFormat: "yaml"
-  }
+  },
+  "@indiekit/store-github": {
+    user: "example",
+    repo: "website"
+  },
+  "@indiekit/store-s3": {
+    region: "eu-central-1",
+    endpoint: "https://s3-example-eu-central-1.amazonaws.com",
+    bucket: "website"
+  },
 }
 ```
 
diff --git a/docs/configuration/publication.md b/docs/configuration/publication.md
index 882f694d..b857f7a5 100644
--- a/docs/configuration/publication.md
+++ b/docs/configuration/publication.md
@@ -61,7 +61,7 @@ Defaults to `false`.
 
 When enabled, Indiekit will try to fetch Microformats data for any URL in a new post (for example URLs for `bookmark-of`, `like-of`, `repost-of`, `in-reply-to`).
 
-If any data is found, a `references` property will be included in the resulting post data. For example, given the following Micropub request:
+If any data is found, a `references` property is included in the resulting post data. For example, given the following Micropub request:
 
 ```sh
 POST /micropub HTTP/1.1
@@ -104,6 +104,10 @@ Defaults to `"en"` (English).
 
 Your website’s URL. **Required**.
 
+## `mediaStore`
+
+A string representing the package name of a [content store plugin](../plugins/stores.md) to use for storing media files. If no value is provided, the [default content store](#store) is used.
+
 ## `postTemplate`
 
 A post template is a function that takes post properties received and parsed by the Micropub endpoint and renders them in a given file format, for example, a Markdown file with YAML front matter.
@@ -240,6 +244,10 @@ A string representing the character used to replace spaces when creating a slug.
 
 Defaults to `"-"` (hyphen).
 
+## `store`
+
+A string representing the package name of a [content store plugin](../plugins/stores.md). If no value is provided, the first store plug-in listed under `plugins` is used.
+
 ## `storeMessageTemplate`
 
 A function that can be used to customise messages used when saving files to content stores that save messages with each change, for example git repositories.
diff --git a/helpers/publication/index.js b/helpers/publication/index.js
index d6cfab39..c67fb57b 100644
--- a/helpers/publication/index.js
+++ b/helpers/publication/index.js
@@ -7,6 +7,9 @@ export const publication = {
   postTemplate(properties) {
     return JSON.stringify(properties);
   },
+  mediaStore: new TestStore({
+    user: "user",
+  }),
   store: new TestStore({
     user: "user",
   }),
diff --git a/indiekit.config.js b/indiekit.config.js
index addcf9c4..76340aab 100644
--- a/indiekit.config.js
+++ b/indiekit.config.js
@@ -33,6 +33,7 @@ const config = {
     me: process.env.PUBLICATION_URL,
     categories: ["internet", "indieweb", "indiekit", "test", "testing"],
     enrichPostData: true,
+    mediaStore: "@indiekit/store-s3",
     postTypes: {
       like: {
         name: "Favourite",
diff --git a/packages/endpoint-media/lib/media-content.js b/packages/endpoint-media/lib/media-content.js
index 38fc0f39..65dc4bef 100644
--- a/packages/endpoint-media/lib/media-content.js
+++ b/packages/endpoint-media/lib/media-content.js
@@ -13,7 +13,7 @@ export const mediaContent = {
   async upload(publication, mediaData, file) {
     debug(`upload %O`, { mediaData });
 
-    const { store, storeMessageTemplate } = publication;
+    const { mediaStore, storeMessageTemplate } = publication;
     const { path, properties } = mediaData;
     const metaData = {
       action: "upload",
@@ -23,7 +23,7 @@ export const mediaContent = {
     };
     const message = storeMessageTemplate(metaData);
 
-    await store.createFile(path, file.data, { message });
+    await mediaStore.createFile(path, file.data, { message });
 
     return {
       location: properties.url,
@@ -44,7 +44,7 @@ export const mediaContent = {
   async delete(publication, mediaData) {
     debug(`delete %O`, { mediaData });
 
-    const { store, storeMessageTemplate } = publication;
+    const { mediaStore, storeMessageTemplate } = publication;
     const { path, properties } = mediaData;
     const metaData = {
       action: "delete",
@@ -54,7 +54,7 @@ export const mediaContent = {
     };
     const message = storeMessageTemplate(metaData);
 
-    await store.deleteFile(path, { message });
+    await mediaStore.deleteFile(path, { message });
 
     return {
       status: 200,
diff --git a/packages/indiekit/config/defaults.js b/packages/indiekit/config/defaults.js
index 8f14358d..f16818a0 100644
--- a/packages/indiekit/config/defaults.js
+++ b/packages/indiekit/config/defaults.js
@@ -29,6 +29,7 @@ export const defaultConfig = {
     port: process.env.PORT || "3000",
     postTypes: {},
     repository: package_.repository,
+    stores: [],
     themeColor: "#04f",
     themeColorScheme: "automatic",
     timeZone: "UTC",
diff --git a/packages/indiekit/index.js b/packages/indiekit/index.js
index 2ce08819..f852ffd7 100644
--- a/packages/indiekit/index.js
+++ b/packages/indiekit/index.js
@@ -4,11 +4,12 @@ import Keyv from "keyv";
 import { expressConfig } from "./config/express.js";
 import { getCategories } from "./lib/categories.js";
 import { getIndiekitConfig } from "./lib/config.js";
-import { getInstalledPlugins } from "./lib/installed-plugins.js";
 import { getLocaleCatalog } from "./lib/locale-catalog.js";
 import { getMongodbClient } from "./lib/mongodb.js";
+import { getInstalledPlugins } from "./lib/plugins.js";
 import { getPostTemplate } from "./lib/post-template.js";
 import { getPostTypes } from "./lib/post-types.js";
+import { getMediaStore, getStore } from "./lib/store.js";
 
 export const Indiekit = class {
   /**
@@ -56,7 +57,7 @@ export const Indiekit = class {
   }
 
   addStore(store) {
-    this.publication.store = store;
+    this.application.stores.push(store);
   }
 
   addSyndicator(syndicator) {
@@ -106,8 +107,10 @@ export const Indiekit = class {
 
     // Update publication configuration
     this.publication.categories = await getCategories(this);
+    this.publication.mediaStore = getMediaStore(this);
     this.publication.postTemplate = getPostTemplate(this.publication);
     this.publication.postTypes = getPostTypes(this);
+    this.publication.store = getStore(this);
 
     return this;
   }
diff --git a/packages/indiekit/lib/installed-plugins.js b/packages/indiekit/lib/plugins.js
similarity index 59%
rename from packages/indiekit/lib/installed-plugins.js
rename to packages/indiekit/lib/plugins.js
index 5c0846b3..6dafc702 100644
--- a/packages/indiekit/lib/installed-plugins.js
+++ b/packages/indiekit/lib/plugins.js
@@ -18,7 +18,7 @@ export const getInstalledPlugins = async (Indiekit) => {
     plugin.filePath = path.dirname(require.resolve(pluginName));
 
     // Add plug-in ID
-    plugin.id = pluginName.replace("/", "-");
+    plugin.id = getPluginId(pluginName);
 
     // Register plug-in functions
     if (plugin.init) {
@@ -29,3 +29,24 @@ export const getInstalledPlugins = async (Indiekit) => {
 
   return installedPlugins;
 };
+
+/**
+ * Get installed plug-in
+ * @param {object} application - Application configuration
+ * @param {string} pluginName - Plug-in Name
+ * @returns {string} Plug-in ID
+ */
+export const getInstalledPlugin = (application, pluginName) => {
+  return application.installedPlugins.find(
+    (plugin) => plugin.id === getPluginId(pluginName),
+  );
+};
+
+/**
+ * Get normalised plug-in ID
+ * @param {string} pluginName - Plug-in Name
+ * @returns {string} Plug-in ID
+ */
+export const getPluginId = (pluginName) => {
+  return pluginName.replace("/", "-");
+};
diff --git a/packages/indiekit/lib/store.js b/packages/indiekit/lib/store.js
new file mode 100644
index 00000000..30cdd525
--- /dev/null
+++ b/packages/indiekit/lib/store.js
@@ -0,0 +1,37 @@
+import { getInstalledPlugin } from "./plugins.js";
+
+/**
+ * Get configured content store
+ * @param {object} Indiekit - Indiekit instance
+ * @returns {object} Content store
+ */
+export const getStore = (Indiekit) => {
+  const { application, publication } = Indiekit;
+
+  // `publication.store` may already be a resolved store function
+  if (typeof publication?.store === "object") {
+    return publication.store;
+  }
+
+  return publication?.store
+    ? getInstalledPlugin(application, publication.store)
+    : application?.stores[0];
+};
+
+/**
+ * Get configured media store
+ * @param {object} Indiekit - Indiekit instance
+ * @returns {object} Media store
+ */
+export const getMediaStore = (Indiekit) => {
+  const { application, publication } = Indiekit;
+
+  // `publication.mediaStore` may already be a resolved store function
+  if (typeof publication?.mediaStore === "object") {
+    return publication.mediaStore;
+  }
+
+  return publication?.mediaStore
+    ? getInstalledPlugin(application, publication.mediaStore)
+    : false;
+};
diff --git a/packages/indiekit/locales/de.json b/packages/indiekit/locales/de.json
index 2650f7fa..d8271436 100644
--- a/packages/indiekit/locales/de.json
+++ b/packages/indiekit/locales/de.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Sprache",
       "me": "Webadresse",
+      "mediaStore": "Medienspeicher",
       "postTypes": "Beitragsarten",
       "preset": "Voreinstellung",
       "store": "Inhaltsspeicher",
diff --git a/packages/indiekit/locales/en.json b/packages/indiekit/locales/en.json
index 18840ff8..6d3f1d97 100644
--- a/packages/indiekit/locales/en.json
+++ b/packages/indiekit/locales/en.json
@@ -87,9 +87,10 @@
       "summaryTitle": "Publication settings",
       "me": "Web address",
       "locale": "Language",
-      "store": "Content store",
+      "mediaStore": "Media store",
       "preset": "Preset",
       "postTypes": "Post types",
+      "store": "Content store",
       "syndicationTargets": "Syndication targets"
     }
   }
diff --git a/packages/indiekit/locales/es-419.json b/packages/indiekit/locales/es-419.json
index d440308d..7103510c 100644
--- a/packages/indiekit/locales/es-419.json
+++ b/packages/indiekit/locales/es-419.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Idioma",
       "me": "Dirección web",
+      "mediaStore": "Tienda multimedia",
       "postTypes": "Tipos de publicaciones",
       "preset": "Preestablecido",
       "store": "Tienda de contenido",
diff --git a/packages/indiekit/locales/es.json b/packages/indiekit/locales/es.json
index 40807b02..8ff35836 100644
--- a/packages/indiekit/locales/es.json
+++ b/packages/indiekit/locales/es.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Idioma",
       "me": "Dirección web",
+      "mediaStore": "Tienda multimedia",
       "postTypes": "Tipos de publicaciones",
       "preset": "Preestablecido",
       "store": "Almacén de contenido",
diff --git a/packages/indiekit/locales/fr.json b/packages/indiekit/locales/fr.json
index 1bd859ae..ea67afe5 100644
--- a/packages/indiekit/locales/fr.json
+++ b/packages/indiekit/locales/fr.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Langue",
       "me": "Adresse Web",
+      "mediaStore": "Magasin multimédia",
       "postTypes": "Types de publications",
       "preset": "Préréglage",
       "store": "Magasin de contenu",
diff --git a/packages/indiekit/locales/id.json b/packages/indiekit/locales/id.json
index 4d7477a2..fea0839a 100644
--- a/packages/indiekit/locales/id.json
+++ b/packages/indiekit/locales/id.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Bahasa",
       "me": "Alamat web",
+      "mediaStore": "Toko media",
       "postTypes": "Jenis postingan",
       "preset": "Prasetel",
       "store": "Penyimpanan Konten",
diff --git a/packages/indiekit/locales/nl.json b/packages/indiekit/locales/nl.json
index c69a3381..b42515cc 100644
--- a/packages/indiekit/locales/nl.json
+++ b/packages/indiekit/locales/nl.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Taal",
       "me": "Webadres",
+      "mediaStore": "Mediawinkel",
       "postTypes": "Artikeltype",
       "preset": "Vooringestelde",
       "store": "Content store",
diff --git a/packages/indiekit/locales/pl.json b/packages/indiekit/locales/pl.json
index 81037ead..9004c456 100644
--- a/packages/indiekit/locales/pl.json
+++ b/packages/indiekit/locales/pl.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Język",
       "me": "Adres sieci Web",
+      "mediaStore": "Sklep multimedialny",
       "postTypes": "Typy postów",
       "preset": "Szablon",
       "store": "Sklep z treściami",
diff --git a/packages/indiekit/locales/pt.json b/packages/indiekit/locales/pt.json
index 7214fcd4..f16e4db2 100644
--- a/packages/indiekit/locales/pt.json
+++ b/packages/indiekit/locales/pt.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Idioma",
       "me": "Endereço Web",
+      "mediaStore": "Loja de mídia",
       "postTypes": "Tipos de postagem",
       "preset": "Pré-ajuste",
       "store": "Loja de conteúdo",
diff --git a/packages/indiekit/locales/sr.json b/packages/indiekit/locales/sr.json
index 34f3f745..61dbbd33 100644
--- a/packages/indiekit/locales/sr.json
+++ b/packages/indiekit/locales/sr.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Jezik",
       "me": "Veb adresa",
+      "mediaStore": "Medijska prodavnica",
       "postTypes": "Vrste postova",
       "preset": "Postavljeno unapred",
       "store": "Prodavnica sadržaja",
diff --git a/packages/indiekit/locales/sv.json b/packages/indiekit/locales/sv.json
index 496c570f..2bd45074 100644
--- a/packages/indiekit/locales/sv.json
+++ b/packages/indiekit/locales/sv.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "Språk",
       "me": "Webbadress",
+      "mediaStore": "Mediebutik",
       "postTypes": "Inläggstyper",
       "preset": "Förval",
       "store": "Innehållsbank",
diff --git a/packages/indiekit/locales/zh-Hans-CN.json b/packages/indiekit/locales/zh-Hans-CN.json
index 5e62e302..ed0c0620 100644
--- a/packages/indiekit/locales/zh-Hans-CN.json
+++ b/packages/indiekit/locales/zh-Hans-CN.json
@@ -88,6 +88,7 @@
     "publication": {
       "locale": "语言",
       "me": "网址",
+      "mediaStore": "媒体商店",
       "postTypes": "帖文类型",
       "preset": "预设",
       "store": "内容存储",
diff --git a/packages/indiekit/views/status.njk b/packages/indiekit/views/status.njk
index 7a565c11..4e468d39 100644
--- a/packages/indiekit/views/status.njk
+++ b/packages/indiekit/views/status.njk
@@ -108,6 +108,23 @@
           })
         }
       } if publication.store, {
+        key: {
+          text: __("status.publication.mediaStore")
+        },
+        value: {
+          text: card({
+            photo: {
+              srcOnError: "/assets/plug-in.svg",
+              attributes: { height: 68, width: 68 },
+              url: "/assets/" + publication.mediaStore.id + "/icon.svg"
+            },
+            headingLevel: 3,
+            title: publication.mediaStore.name,
+            description: publication.mediaStore.info.uid,
+            url: "/plugins/" + publication.mediaStore.id
+          })
+        }
+      } if publication.mediaStore, {
         key: {
           text: __("status.publication.syndicationTargets")
         },


Test patch:
diff --git a/packages/indiekit/test/index.js b/packages/indiekit/test/index.js
index c85306b7..627c6db3 100644
--- a/packages/indiekit/test/index.js
+++ b/packages/indiekit/test/index.js
@@ -59,7 +59,7 @@ describe("indiekit", () => {
     const testStore = new TestStore();
     indiekit.addStore(testStore);
 
-    assert.equal(indiekit.publication.store.info.name, "Test store");
+    assert.equal(indiekit.application.stores[0].info.name, "Test store");
   });
 
   it("Exits process if no publication URL in configuration", async () => {
diff --git a/packages/indiekit/test/unit/plugins.js b/packages/indiekit/test/unit/plugins.js
new file mode 100644
index 00000000..cebd7d1f
--- /dev/null
+++ b/packages/indiekit/test/unit/plugins.js
@@ -0,0 +1,23 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { getInstalledPlugin, getPluginId } from "../../lib/plugins.js";
+
+describe("indiekit/lib/plugins", async () => {
+  it("Gets installed plug-in", () => {
+    const application = {
+      installedPlugins: [
+        {
+          id: "@scope-package-name",
+        },
+      ],
+    };
+
+    assert.deepEqual(getInstalledPlugin(application, "@scope/package-name"), {
+      id: "@scope-package-name",
+    });
+  });
+
+  it("Gets normalised plug-in ID", () => {
+    assert.equal(getPluginId("@scope/package-name"), "@scope-package-name");
+  });
+});
diff --git a/packages/indiekit/test/unit/store.js b/packages/indiekit/test/unit/store.js
new file mode 100644
index 00000000..0530c341
--- /dev/null
+++ b/packages/indiekit/test/unit/store.js
@@ -0,0 +1,44 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { getMediaStore, getStore } from "../../lib/store.js";
+
+describe("indiekit/lib/store", async () => {
+  const { default: IndiekitPlugin } = await import("@indiekit-test/store");
+  const store = new IndiekitPlugin();
+  // @ts-ignore
+  store.id = "@indiekit-test-store";
+
+  it("Gets store from application config", () => {
+    const Indiekit = { application: { stores: [store] } };
+    const result = getStore(Indiekit);
+
+    assert.equal(result.info.name, "Test store");
+  });
+
+  it("Gets store from publication config", () => {
+    const Indiekit = {
+      application: { installedPlugins: [store] },
+      publication: { store: "@indiekit-test/store" },
+    };
+    const result = getStore(Indiekit);
+
+    assert.equal(result.info.name, "Test store");
+  });
+
+  it("Gets media store from publication config", () => {
+    const Indiekit = {
+      application: { installedPlugins: [store] },
+      publication: { mediaStore: "@indiekit-test/store" },
+    };
+    const result = getMediaStore(Indiekit);
+
+    assert.equal(result.info.name, "Test store");
+  });
+
+  it("Returns false if no media store configured", () => {
+    const Indiekit = { publication: { store } };
+    const result = getMediaStore(Indiekit);
+
+    assert.equal(result, false);
+  });
+});
diff --git a/packages/store-bitbucket/test/index.js b/packages/store-bitbucket/test/index.js
index 45e435ba..6e25c1e9 100644
--- a/packages/store-bitbucket/test/index.js
+++ b/packages/store-bitbucket/test/index.js
@@ -31,12 +31,18 @@ describe("store-bitbucket", () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    bitbucket.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-bitbucket"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-bitbucket": { user: "user", repo: "repo" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(
       indiekit.publication.store.info.name,
-      "username/repo on Bitbucket",
+      "user/repo on Bitbucket",
     );
   });
 
diff --git a/packages/store-file-system/test/index.js b/packages/store-file-system/test/index.js
index e671ed53..5b885feb 100644
--- a/packages/store-file-system/test/index.js
+++ b/packages/store-file-system/test/index.js
@@ -29,8 +29,14 @@ describe("store-file-system", () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    fileSystem.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-file-system"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-file-system": { directory: "directory" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(indiekit.publication.store.info.name, "directory");
   });
diff --git a/packages/store-ftp/test/index.js b/packages/store-ftp/test/index.js
index 65c5359d..951fa8dd 100644
--- a/packages/store-ftp/test/index.js
+++ b/packages/store-ftp/test/index.js
@@ -49,8 +49,14 @@ describe("store-ftp", () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    ftp.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-ftp"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-ftp": { user: "username", host: "127.0.0.1" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(indiekit.publication.store.info.name, "username on 127.0.0.1");
   });
diff --git a/packages/store-gitea/test/index.js b/packages/store-gitea/test/index.js
index 9ed858c5..a29a2e6f 100644
--- a/packages/store-gitea/test/index.js
+++ b/packages/store-gitea/test/index.js
@@ -35,13 +35,16 @@ describe("store-github", async () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    gitea.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-gitea"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-gitea": { user: "user", repo: "repo" },
+      },
+    });
+    await indiekit.bootstrap();
 
-    assert.equal(
-      indiekit.publication.store.info.name,
-      "username/repo on Gitea",
-    );
+    assert.equal(indiekit.publication.store.info.name, "user/repo on Gitea");
   });
 
   it("Creates file", async () => {
diff --git a/packages/store-github/test/index.js b/packages/store-github/test/index.js
index f94965c2..35b4f391 100644
--- a/packages/store-github/test/index.js
+++ b/packages/store-github/test/index.js
@@ -28,8 +28,14 @@ describe("store-github", async () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    github.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-github"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-github": { user: "user", repo: "repo" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(indiekit.publication.store.info.name, "user/repo on GitHub");
   });
diff --git a/packages/store-gitlab/test/index.js b/packages/store-gitlab/test/index.js
index 3a9074ef..21976a8e 100644
--- a/packages/store-gitlab/test/index.js
+++ b/packages/store-gitlab/test/index.js
@@ -34,8 +34,14 @@ describe("store-gitlab", async () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    gitlab.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-gitlab"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-gitlab": { user: "username", repo: "repo" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(
       indiekit.publication.store.info.name,
diff --git a/packages/store-s3/test/index.js b/packages/store-s3/test/index.js
index 3dc0360c..cc1dc018 100644
--- a/packages/store-s3/test/index.js
+++ b/packages/store-s3/test/index.js
@@ -30,8 +30,14 @@ describe("store-s3", () => {
   });
 
   it("Initiates plug-in", async () => {
-    const indiekit = await Indiekit.initialize({ config: {} });
-    s3.init(indiekit);
+    const indiekit = await Indiekit.initialize({
+      config: {
+        plugins: ["@indiekit/store-s3"],
+        publication: { me: "https://website.example" },
+        "@indiekit/store-s3": { bucket: "website" },
+      },
+    });
+    await indiekit.bootstrap();
 
     assert.equal(indiekit.publication.store.info.name, "website bucket");
   });