## This file is a basic test of Mastodon functionalities. ## ## NOTE: This test will fail for Mastodon < 4.3 because of ## https://github.com/mastodon/mastodon/issues/31145 { pkgs, self }: let lib = pkgs.lib; ## FIXME: this binding was not used, but maybe we want a side-effect or something? # rebuildableTest = import ./rebuildableTest.nix pkgs; testImage = pkgs.copyPathToStore ./green.png; testImageColour = "#00FF00"; 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 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/public/local") # wait until the statuses load WebDriverWait(driver, 90).until( lambda x: x.find_element(By.CLASS_NAME, "status")) driver.save_screenshot("/mastodon-screenshot.png") driver.close() ''; in pkgs.nixosTest { name = "mastodon"; nodes = { server = { config, ... }: { virtualisation.memorySize = lib.mkVMOverride 4096; imports = with self.nixosModules; [ fediversity ../vm/garage-vm.nix ../vm/mastodon-vm.nix ../vm/interactive-vm.nix ]; # TODO: pair down environment.systemPackages = with pkgs; [ python3 firefox-unwrapped geckodriver toot xh seleniumScript helix imagemagick expect ]; environment.variables = { AWS_ACCESS_KEY_ID = config.services.garage.ensureKeys.mastodon.id; AWS_SECRET_ACCESS_KEY = config.services.garage.ensureKeys.mastodon.secret; }; }; }; testScript = { nodes, ... }: '' import re import time server.start() with subtest("Mastodon starts"): 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_match = re.match('.*New password: ([^\n]*).*', account_creation_output, re.S) if password_match is None: raise Exception(f"account creation did not generate a password.\n{account_creation_output}") password = password_match.group(1) # print(f"Test user (test@test.com)'s password is: {password}") 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; # we use 'expect' for this purpose. server.succeed(f""" expect -c ' spawn toot login_cli --instance http://mastodon.localhost:55001 --email test@test.com expect "Password: " send "{password}\\n" interact ' >&2 """) with subtest("Post a text"): server.succeed("echo 'hello mastodon' | toot post") with subtest("Post an image"): server.succeed("toot post --media ${testImage}") with subtest("Access garage"): server.succeed("mc alias set garage ${nodes.server.fediversity.internal.garage.api.url} --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() if image == "": raise Exception("image posted to mastodon did not get stored in garage") 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 '%#' ${testImage}") if garage_image_hash != image_hash: raise Exception("image stored in garage did not match image uploaded") with subtest("Content-Security-Policy allows garage content"): headers = server.succeed("xh -h http://mastodon.localhost:55001/public/local") csp_match = None # I can't figure out re.MULTILINE for header in headers.split("\n"): csp_match = re.match('^Content-Security-Policy: (.*)$', header) if csp_match is not None: break if csp_match is None: raise Exception("mastodon did not send a content security policy header") csp = csp_match.group(1) # the connect-src content security policy should include the garage server ## TODO: use `nodes.server.fediversity.internal.garage.api.url` same as above, but beware of escaping the regex. Be careful with port 80 though. garage_csp = re.match(".*; img-src[^;]*web\.garage\.localhost.*", csp) if garage_csp is None: raise Exception("Mastodon's Content-Security-Policy does not include Garage.") # this could in theory give a false positive if mastodon changes it's colorscheme to include ${testImageColour}. 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 image displayed somewhere image_check = re.match(".*${testImageColour}.*", displayed_colors, re.S) if image_check is None: raise Exception("cannot detect the uploaded image on mastodon page.") ''; }