forked from fediversity/fediversity
		
	closes #34. Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io> Reviewed-on: Fediversity/Fediversity#457 Reviewed-by: Valentin Gagarin <valentin.gagarin@tweag.io> Co-authored-by: Kiara Grouwstra <kiara@procolix.eu> Co-committed-by: Kiara Grouwstra <kiara@procolix.eu>
		
			
				
	
	
		
			170 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
## 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, ... }:
 | 
						|
 | 
						|
let
 | 
						|
  inherit (pkgs) lib writeText;
 | 
						|
 | 
						|
  ## 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
 | 
						|
 | 
						|
{
 | 
						|
  name = "mastodon";
 | 
						|
 | 
						|
  nodes = {
 | 
						|
    server =
 | 
						|
      { config, ... }:
 | 
						|
      {
 | 
						|
        virtualisation.memorySize = lib.mkVMOverride 4096;
 | 
						|
        imports = [
 | 
						|
          ../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 = "$(cat ${config.fediversity.mastodon.s3AccessKeyFile})";
 | 
						|
          AWS_SECRET_ACCESS_KEY = "$(cat ${config.fediversity.mastodon.s3SecretKeyFile})";
 | 
						|
        };
 | 
						|
        services.mastodon.extraEnvFiles = [
 | 
						|
          # generate as: cd ${pkgs.mastodon}; IGNORE_ALREADY_SET_SECRETS=true RAILS_ENV=development ${pkgs.mastodon}/bin/rails db:encryption:init
 | 
						|
          (writeText "rest" ''
 | 
						|
            ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=naGoEzeyjUmwIlmgZZmGQDWJrlWud5eX
 | 
						|
            ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=A0tE1VJ7S3cjaOQ58mNkhrVFY7o5NKDB
 | 
						|
            ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=tGHhd5Os7hLxa8QTzWwjyVLrvsj5VsCw
 | 
						|
          '')
 | 
						|
        ];
 | 
						|
      };
 | 
						|
  };
 | 
						|
 | 
						|
  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.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.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.")
 | 
						|
    '';
 | 
						|
}
 |