{ pkgs, shb, ... }: let port = 9096; adminUser = "jellyfin2"; adminPassword = "admin"; commonExtraScript = { node, ... }: '' server.wait_until_succeeds("journalctl --since -1m --unit jellyfin --grep 'Startup complete'") headers = unline_with(" ", """ -H 'Content-Type: application/json' -H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"' """) import time with subtest("api login success"): ok = False for i in range(1, 5): response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", data="""{"Username": "${adminUser}", "Pw": "${adminPassword}"}""", extra=headers) if response['code'] == 200: ok = True break time.sleep(5) if not ok: raise Exception(f"Expected success, got: {response['code']}") with subtest("api login failure"): response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", data="""{"Username": "${adminUser}", "Pw": "badpassword"}""", extra=headers) if response['code'] != 401: raise Exception(f"Expected failure, got: {response['code']}") ''; commonTestScript = shb.test.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl); waitForServices = { ... }: [ "jellyfin.service" "nginx.service" ]; waitForPorts = { node, ... }: [ port ]; waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}/System/Info/Public" { url = "${proto_fqdn}/Users/AuthenticateByName"; status = 401; } ]; extraScript = commonExtraScript; }; basic = { config, ... }: { imports = [ shb.test.baseModule ../../modules/services/jellyfin.nix ]; # Jellyfin checks for minimum 2Gib on startup. virtualisation.diskSize = 4096; virtualisation.memorySize = 4096; test = { subdomain = "j"; }; shb.jellyfin = { enable = true; inherit (config.test) subdomain domain; inherit port; admin = { username = adminUser; password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result; }; debug = true; }; shb.hardcodedsecret.jellyfinAdminPassword = { request = config.shb.jellyfin.admin.password.request; settings.content = adminPassword; }; environment.systemPackages = [ pkgs.sqlite ]; }; clientLogin = { config, ... }: { imports = [ shb.test.baseModule shb.test.clientLoginModule ]; virtualisation.memorySize = 4096; test = { subdomain = "j"; }; test.login = { browser = "firefox"; startUrl = "${config.test.proto}://${config.test.fqdn}"; usernameFieldLabelRegex = "[Uu]ser"; loginButtonNameRegex = "Sign In"; testLoginWith = [ { username = adminUser; password = "badpassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } { username = adminUser; password = adminPassword; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } ]; }; }; https = { config, ... }: { shb.jellyfin = { ssl = config.shb.certs.certs.selfsigned.n; }; test = { hasSSL = true; }; }; ldap = { config, lib, ... }: { shb.jellyfin = { ldap = { enable = true; host = "127.0.0.1"; port = config.shb.lldap.ldapPort; dcdomain = config.shb.lldap.dcdomain; userGroup = "user_group"; adminGroup = "admin_group"; adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result; }; }; # There's something weird happending here # where this plugin disappears after a jellyfin restart. # I don't know why this is the case. # I tried using a real plugin here instead of a mock or just creating a meta.json file. # But this didn't help. shb.jellyfin.plugins = lib.mkBefore [ (shb.mkJellyfinPlugin (rec { pname = "jellyfin-plugin-ldapauth"; version = "19"; url = "https://github.com/jellyfin/${pname}/releases/download/v${version}/ldap-authentication_${version}.0.0.0.zip"; hash = "sha256-NunkpdYjsxYT6a4RaDXLkgRn4scRw8GaWvyHGs9IdWo="; })) ]; shb.hardcodedsecret.jellyfinLdapUserPassword = { request = config.shb.jellyfin.ldap.adminPassword.request; settings.content = "ldapUserPassword"; }; }; clientLoginLdap = { config, ... }: { imports = [ shb.test.baseModule shb.test.clientLoginModule ]; virtualisation.memorySize = 4096; test = { subdomain = "j"; }; test.login = { startUrl = "${config.test.proto}://${config.test.fqdn}"; usernameFieldLabelRegex = "[Uu]ser"; loginButtonNameRegex = "Sign In"; testLoginWith = [ { username = adminUser; password = "badpassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } { username = adminUser; password = adminPassword; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } { username = "alice"; password = "AlicePassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } { username = "bob"; password = "BobPassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } ]; }; }; sso = { config, ... }: { shb.jellyfin = { ldap = { userGroup = "user_group"; adminGroup = "admin_group"; }; sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result; sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result; }; }; shb.hardcodedsecret.jellyfinSSOPassword = { request = config.shb.jellyfin.sso.sharedSecret.request; settings.content = "ssoPassword"; }; shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = { request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request; settings.content = "ssoPassword"; }; }; clientLoginSso = { config, ... }: { imports = [ shb.test.baseModule shb.test.clientLoginModule ]; virtualisation.memorySize = 4096; test = { subdomain = "j"; }; test.login = { startUrl = "${config.test.proto}://${config.test.fqdn}"; beforeHook = '' page.locator('text=Sign in with Authelia').click() ''; usernameFieldLabelRegex = "Username"; passwordFieldLabelRegex = "Password"; loginButtonNameRegex = "[Ss]ign [Ii]n"; loginSpawnsNewPage = true; testLoginWith = [ { username = "alice"; password = "AlicePassword"; nextPageExpect = [ "page.get_by_text(re.compile('[Aa]ccept')).click()" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ # For a reason I can't explain, redirection needs to happen manually. # So for failing auth, we check we're back on the login page. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" ]; } { username = "bob"; password = "BobPassword"; nextPageExpect = [ "page.get_by_text(re.compile('[Aa]ccept')).click()" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" ]; } { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" ]; } ]; }; }; jellyfinTest = name: { nodes, testScript }: shb.test.runNixOSTest { name = "jellyfin_${name}"; interactive.sshBackdoor.enable = true; interactive.nodes.server = { environment.systemPackages = [ pkgs.sqlite ]; }; inherit nodes; inherit testScript; }; in { basic = jellyfinTest "basic" { nodes.server = { imports = [ basic ]; }; nodes.client = { imports = [ clientLogin ]; }; testScript = commonTestScript.access; }; backup = jellyfinTest "backup" { nodes.server = { config, ... }: { imports = [ basic (shb.test.backup config.shb.jellyfin.backup) ]; }; nodes.client = { }; testScript = commonTestScript.backup; }; https = jellyfinTest "https" { nodes.server = { imports = [ basic shb.test.certs https ]; }; nodes.client = { config, lib, ... }: { imports = [ clientLogin ]; }; testScript = commonTestScript.access; }; ldap = jellyfinTest "ldap" { nodes.server = { imports = [ basic shb.test.certs https shb.test.ldap ldap ]; }; nodes.client = { imports = [ clientLoginLdap ]; }; testScript = commonTestScript.access.override { extraScript = args@{ node, ... }: (commonExtraScript args) # I have no idea why the LDAP Authentication_19.0.0.0 plugin disappears. + '' r = server.execute('cat "${node.config.services.jellyfin.dataDir}/plugins/LDAP Authentication_19.0.0.0/meta.json"') if r[0] != 0: print("meta.json for plugin LDAP Authentication_19.0.0.0 not found") else: c = json.loads(r[1]) if "status" in c and c["status"] != "Disabled": raise Exception(f'meta.json status: expected Disabled, got: {c["status"]}') ''; }; }; sso = jellyfinTest "sso" { nodes.server = { config, pkgs, ... }: { imports = [ basic shb.test.certs https shb.test.ldap (shb.test.sso config.shb.certs.certs.selfsigned.n) sso ]; }; nodes.client = { imports = [ clientLoginSso ]; }; testScript = commonTestScript.access; }; }