diff --git a/garage.nix b/garage.nix
index 2c47ec98..3de3709a 100644
--- a/garage.nix
+++ b/garage.nix
@@ -38,7 +38,8 @@ let
 
       ${optionalString corsRules.enable ''
         garage bucket allow --read --write --owner ${bucketArg} --key tmp
-        aws --endpoint http://s3.garage.localhost:3900 s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
+        # TODO: endpoin-url should not be hard-coded
+        aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url http://s3.garage.localhost:3900 s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
         garage bucket deny --read --write --owner ${bucketArg} --key tmp
       ''}
     '';
@@ -124,21 +125,23 @@ in {
   };
 
   config = {
-    virtualisation.diskSize = 2048;
-    virtualisation.forwardPorts = [
-      {
-        from = "host";
-        host.port = 3901;
-        guest.port = 3901;
-      }
-      {
-        from = "host";
-        host.port = 3902;
-        guest.port = 3902;
-      }
-    ];
+    virtualisation.vmVariant = { config, ... }: {
+      virtualisation.diskSize = 2048;
+      virtualisation.forwardPorts = [
+        {
+          from = "host";
+          host.port = 3901;
+          guest.port = 3901;
+        }
+        {
+          from = "host";
+          host.port = 3902;
+          guest.port = 3902;
+        }
+      ];
 
-    environment.systemPackages = [ pkgs.minio-client pkgs.awscli ];
+      environment.systemPackages = [ pkgs.minio-client pkgs.awscli ];
+    };
 
     networking.firewall.allowedTCPPorts = [ 3901 3902 ];
     services.garage = {
diff --git a/tests/fediversity.png b/tests/fediversity.png
deleted file mode 100644
index 24881fb4..00000000
Binary files a/tests/fediversity.png and /dev/null differ
diff --git a/tests/green.png b/tests/green.png
new file mode 100644
index 00000000..35161098
Binary files /dev/null and b/tests/green.png differ
diff --git a/tests/mastodon-garage.nix b/tests/mastodon-garage.nix
index c20a7716..c423f861 100644
--- a/tests/mastodon-garage.nix
+++ b/tests/mastodon-garage.nix
@@ -1,7 +1,37 @@
 { pkgs, self }:
 let
+  lib = pkgs.lib;
   # python = pkgs.python310.withPackages (ps: with ps; [ requests aiokafka ]);
   rebuildableTest = import ./rebuildableTest.nix pkgs;
+  seleniumScript = pkgs.writers.writePython3Bin "selenium-script"
+    {
+      libraries = with pkgs.python3Packages; [ selenium ];
+    } ''
+    from selenium import webdriver
+    from selenium.webdriver.common.by import By
+    from selenium.webdriver.firefox.options import Options
+    from selenium.webdriver.support.ui import WebDriverWait
+
+    print(1)
+
+    options = Options()
+    options.add_argument("--headless")
+    # devtools don't show up in headless screenshots
+    # options.add_argument("-devtools")
+    service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}")  # noqa: E501
+
+    driver = webdriver.Firefox(options=options, service=service)
+    driver.get("http://mastodon.localhost:55001/public/local")
+
+    # wait until the statuses load
+    WebDriverWait(driver, 90).until(
+        lambda x: x.find_element(By.CLASS_NAME, "status"))
+
+    # XXX: how do I save this to the derivation output?
+    driver.save_screenshot("/mastodon-screenshot.png")
+
+    driver.close()
+  '';
 in
 rebuildableTest {
   name = "test-mastodon-garage";
@@ -10,13 +40,29 @@ rebuildableTest {
   # skipTypeCheck = true;
 
   nodes = {
-    server = {
+    server = {config, ...}: {
+      virtualisation.memorySize = lib.mkVMOverride 4096;
       imports = [ self.nixosModules.garage self.nixosModules.mastodon ];
-      environment.systemPackages = with pkgs; [ toot ];
+      # TODO: pair down
+      environment.systemPackages = with pkgs; [
+        python3
+        firefox-unwrapped
+        geckodriver
+        toot
+        xh
+        seleniumScript
+        helix
+        imagemagick
+      ];
+      environment.variables = {
+        POST_MEDIA = ./green.png;
+        AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.mastodon.id;
+        AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.mastodon.secret;
+      };
     };
   };
 
-  testScript = ''
+  testScript = {nodes, ...}: ''
     import re
     import time
 
@@ -26,33 +72,59 @@ rebuildableTest {
       server.wait_for_unit("mastodon-web.service")
 
     # make sure mastodon is fully up and running before we interact with it
+    # TODO: is there a way to test for this?
     time.sleep(180)
 
     with subtest("Account creation"):
       account_creation_output = server.succeed("mastodon-tootctl accounts create test --email test@test.com --confirmed --approve")
-      password_re = re.compile('New password: (.*)')
-      password_match = password_re.match(account_creation_output)
+      password_match = re.match('.*New password: ([^\n]*).*', account_creation_output, re.S)
       assert password_match is not None
-      password = password_match.groups()[0]
+      password = password_match.group(1)
+
+    with subtest("TTY Login"):
+      server.wait_until_tty_matches("1", "login: ")
+      server.send_chars("root\n");
 
     with subtest("Log in with toot"):
       # toot doesn't provide a way to just specify our login details as arguments, so we have to pretend we're typing them in at the prompt
-      server.send_chars("toot login_ctl\n")
-      time.sleep(0.2)
-      # Enter instance URL
-      server.send_chars("http://mastodon.localhost:55001\n")
-      time.sleep(0.2)
-      # Email
-      server.send_chars("test@test.com\n")
-      time.sleep(0.2)
-      # Password
+      server.send_chars("toot login_cli --instance http://mastodon.localhost:55001 --email test@test.com\n")
+      server.wait_until_tty_matches("1", "Password: ")
       server.send_chars(password + "\n")
+      server.wait_until_tty_matches("1", "Successfully logged in.")
 
-    with subtest("Post an image"):
-      server.succeed("toot post --media ${./fediversity.png}")
+    with subtest("post text"):
+      server.succeed("echo 'hello mastodon' | toot post")
 
-      # TODO: I don't think there's a good way to test for whether the image visually shows up. 
-      # we can test for CORS headers using curl / xh
-      # or **maybe** somehow read the javascript console?
+    with subtest("post image"):
+      server.succeed("toot post --media $POST_MEDIA")
+
+    with subtest("access garage"):
+      server.succeed("mc alias set garage http://s3.garage.localhost:3900 --api s3v4 --path off $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY")
+      server.succeed("mc ls garage/mastodon")
+
+    with subtest("access image in garage"):
+      image = server.succeed("mc find garage --regex original")
+      image = image.rstrip()
+      assert image != ""
+      server.succeed(f"mc cat {image} >/garage-image.webp")
+      garage_image_hash = server.succeed("identify -quiet -format '%#' /garage-image.webp")
+      image_hash = server.succeed("identify -quiet -format '%#' $POST_MEDIA")
+      assert garage_image_hash == image_hash
+
+    with subtest("Content security policy allows garage images"):
+      headers = server.succeed("xh -h http://masstodon.localhost:55001/public/local")
+      csp_match = re.match('^Content-Security-Policy: (.*)$', headers, re.M)
+      assert csp_match is not None
+      csp = csp_match.group(1)
+      # the content security policy should include the garage server
+      garage_csp = re.match(".*web\.garage\.localhost:3902.*", csp)
+      assert garage_csp is not None
+
+    with subtest("image displays"):
+      server.succeed("selenium-script")
+      server.copy_from_vm("/mastodon-screenshot.png", "")
+      displayed_colors = server.succeed("convert /mastodon-screenshot.png -define histogram:unique-colors=true -format %c histogram:info:")
+      # check that the green image displayed somewhere
+      re.match(".*#00FF00.*", displayed_colors, re.S)
   '';
 }