335 lines
8.7 KiB
Go
335 lines
8.7 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// IP address to use when connecting to local containers.
|
||
|
var containerAddress string
|
||
|
|
||
|
// Path to compiled plugin DLL to install in local containers.
|
||
|
var pluginPath string
|
||
|
|
||
|
func flags() {
|
||
|
flag.StringVar(&pluginPath, "dll", "", "Path to plugin DLL to install in container images.")
|
||
|
flag.StringVar(&containerAddress, "caddr", "", "IP address to use when connecting to local containers.")
|
||
|
flag.Parse()
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
flags()
|
||
|
|
||
|
start := time.Now()
|
||
|
|
||
|
fmt.Printf("[+] Start time: %s\n", start)
|
||
|
|
||
|
// Load list of servers
|
||
|
fmt.Println("[+] Loading configuration")
|
||
|
config := loadConfiguration()
|
||
|
fmt.Println()
|
||
|
|
||
|
// Start Selenium by bringing up the compose file in detatched mode
|
||
|
fmt.Println("[+] Starting Selenium")
|
||
|
RunProgram("docker-compose", []string{"up", "-d"}, 10*time.Second)
|
||
|
|
||
|
// If any error occurs, bring Selenium down before exiting
|
||
|
defer func() {
|
||
|
fmt.Println("[+] Stopping Selenium")
|
||
|
RunProgram("docker-compose", []string{"down"}, 15*time.Second)
|
||
|
}()
|
||
|
|
||
|
// Test all provided Jellyfin servers
|
||
|
for _, server := range config.Servers {
|
||
|
if server.Skip {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
var configurationDirectory string
|
||
|
var apiKey string
|
||
|
var seleniumArgs []string
|
||
|
|
||
|
// LSIO containers use some slighly different paths & permissions
|
||
|
lsioImage := strings.Contains(server.Image, "linuxserver")
|
||
|
|
||
|
fmt.Println()
|
||
|
fmt.Printf("[+] Testing %s\n", server.Comment)
|
||
|
|
||
|
if server.Docker {
|
||
|
var err error
|
||
|
|
||
|
// Setup a temporary folder for the container's configuration
|
||
|
configurationDirectory, err = os.MkdirTemp("/dev/shm", "jf-e2e-*")
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
// Copy the contents of the base configuration directory to a temp folder for the container
|
||
|
src, dst := server.Base+"/.", configurationDirectory
|
||
|
fmt.Printf(" [+] Copying %s to %s\n", src, dst)
|
||
|
RunProgram("cp", []string{"-ar", src, dst}, 10*time.Second)
|
||
|
|
||
|
// Create a folder to install the plugin into
|
||
|
pluginDirectory := path.Join(configurationDirectory, "plugins", "intro-skipper")
|
||
|
if lsioImage {
|
||
|
pluginDirectory = path.Join(configurationDirectory, "data", "plugins", "intro-skipper")
|
||
|
}
|
||
|
|
||
|
fmt.Println(" [+] Creating plugin directory")
|
||
|
if err := os.MkdirAll(pluginDirectory, 0700); err != nil {
|
||
|
fmt.Printf(" [!] Failed to create plugin directory: %s\n", err)
|
||
|
goto cleanup
|
||
|
}
|
||
|
|
||
|
// If this is an LSIO container, adjust the permissions on the plugin directory
|
||
|
if lsioImage {
|
||
|
if err := os.Chown(pluginDirectory, 911, 911); err != nil {
|
||
|
fmt.Printf(" [!] Failed to change plugin directory UID/GID: %s\n", err)
|
||
|
goto cleanup
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Install the plugin
|
||
|
fmt.Printf(" [+] Copying plugin %s to %s\n", pluginPath, pluginDirectory)
|
||
|
RunProgram("cp", []string{pluginPath, pluginDirectory}, 2*time.Second)
|
||
|
|
||
|
/* Start the container with the following settings:
|
||
|
* Name: jf-e2e
|
||
|
* Port: 8097
|
||
|
* Media: Mounted to /media, read only
|
||
|
*/
|
||
|
containerArgs := []string{"run", "--name", "jf-e2e", "--rm", "-p", "8097:8096",
|
||
|
"-v", fmt.Sprintf("%s:%s:rw", configurationDirectory, "/config"),
|
||
|
"-v", fmt.Sprintf("%s:%s:ro", config.Common.Library, "/media"),
|
||
|
server.Image}
|
||
|
|
||
|
fmt.Printf(" [+] Starting container %s\n", server.Image)
|
||
|
go RunProgram("docker", containerArgs, 60*time.Second)
|
||
|
|
||
|
// Wait for the container to fully start
|
||
|
waitForServerStartup(server.Address)
|
||
|
} else {
|
||
|
fmt.Println("[+] Remote instance, assuming plugin is already installed")
|
||
|
}
|
||
|
|
||
|
// Get an API key
|
||
|
apiKey = login(server)
|
||
|
|
||
|
// Analyze episodes and save report
|
||
|
fmt.Println(" [+] Analyzing episodes")
|
||
|
fmt.Print("\033[37;1m") // change the color of the verifier's text
|
||
|
RunProgram(
|
||
|
"./verifier/verifier",
|
||
|
[]string{
|
||
|
"-address", server.Address,
|
||
|
"-key", apiKey, "-o",
|
||
|
fmt.Sprintf("reports/%s-%d.json", server.Comment, start.Unix())},
|
||
|
5*time.Minute)
|
||
|
fmt.Print("\033[39;0m") // reset terminal text color
|
||
|
|
||
|
// Setup base Selenium arguments
|
||
|
seleniumArgs = []string{
|
||
|
"-u", // force stdout to be unbuffered
|
||
|
"main.py",
|
||
|
"-host", server.Address,
|
||
|
"-user", server.Username,
|
||
|
"-pass", server.Password,
|
||
|
"-name", config.Common.Episode}
|
||
|
|
||
|
// Append all requested Selenium tests
|
||
|
seleniumArgs = append(seleniumArgs, "--tests")
|
||
|
seleniumArgs = append(seleniumArgs, server.Tests...)
|
||
|
|
||
|
// Append all requested browsers
|
||
|
seleniumArgs = append(seleniumArgs, "--browsers")
|
||
|
seleniumArgs = append(seleniumArgs, server.Browsers...)
|
||
|
|
||
|
// Run Selenium
|
||
|
os.Chdir("selenium")
|
||
|
RunProgram("python3", seleniumArgs, time.Minute)
|
||
|
os.Chdir("..")
|
||
|
|
||
|
cleanup:
|
||
|
if server.Docker {
|
||
|
// Stop the container
|
||
|
fmt.Println(" [+] Stopping and removing container")
|
||
|
RunProgram("docker", []string{"stop", "jf-e2e"}, 10*time.Second)
|
||
|
|
||
|
// Cleanup the container's configuration
|
||
|
fmt.Printf(" [+] Deleting %s\n", configurationDirectory)
|
||
|
|
||
|
if err := os.RemoveAll(configurationDirectory); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Login to the specified Jellyfin server and return an API key
|
||
|
func login(server Server) string {
|
||
|
type AuthenticateUserByName struct {
|
||
|
AccessToken string
|
||
|
}
|
||
|
|
||
|
fmt.Println(" [+] Sending authentication request")
|
||
|
|
||
|
// Create request body
|
||
|
rawBody := fmt.Sprintf(`{"Username":"%s","Pw":"%s"}`, server.Username, server.Password)
|
||
|
body := bytes.NewBufferString(rawBody)
|
||
|
|
||
|
// Create the request
|
||
|
req, err := http.NewRequest(
|
||
|
"POST",
|
||
|
fmt.Sprintf("%s/Users/AuthenticateByName", server.Address),
|
||
|
body)
|
||
|
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
// Set headers
|
||
|
req.Header.Set("Content-Type", "application/json")
|
||
|
req.Header.Set(
|
||
|
"X-Emby-Authorization",
|
||
|
`MediaBrowser Client="JF E2E Tests", Version="0.0.1", DeviceId="E2E", Device="E2E"`)
|
||
|
|
||
|
// Authenticate
|
||
|
res, err := http.DefaultClient.Do(req)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
} else if res.StatusCode != http.StatusOK {
|
||
|
panic(fmt.Sprintf("authentication returned code %d", res.StatusCode))
|
||
|
}
|
||
|
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
// Read body
|
||
|
fullBody, err := io.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
// Unmarshal body and return token
|
||
|
var token AuthenticateUserByName
|
||
|
if err := json.Unmarshal(fullBody, &token); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
return token.AccessToken
|
||
|
}
|
||
|
|
||
|
// Wait up to ten seconds for the provided Jellyfin server to fully startup
|
||
|
func waitForServerStartup(address string) {
|
||
|
attempts := 10
|
||
|
fmt.Println(" [+] Waiting for server to finish starting")
|
||
|
|
||
|
for {
|
||
|
// Sleep in between requests
|
||
|
time.Sleep(time.Second)
|
||
|
|
||
|
// Ping the /System/Info/Public endpoint
|
||
|
res, err := http.Get(fmt.Sprintf("%s/System/Info/Public", address))
|
||
|
|
||
|
// If the server didn't return 200 OK, loop
|
||
|
if err != nil || res.StatusCode != http.StatusOK {
|
||
|
if attempts--; attempts <= 0 {
|
||
|
panic("server is taking too long to startup")
|
||
|
}
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Assume startup has finished, break
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read configuration from config.json
|
||
|
func loadConfiguration() Configuration {
|
||
|
var config Configuration
|
||
|
|
||
|
// Load the contents of the configuration file
|
||
|
raw, err := os.ReadFile("config.json")
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
// Unmarshal
|
||
|
if err := json.Unmarshal(raw, &config); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
// Print debugging info
|
||
|
fmt.Printf("Library: %s\n", config.Common.Library)
|
||
|
fmt.Printf("Episode: \"%s\"\n", config.Common.Episode)
|
||
|
fmt.Println()
|
||
|
|
||
|
// Check the validity of all entries
|
||
|
for i, server := range config.Servers {
|
||
|
// If this is an entry for a local container, ensure the server address is correct
|
||
|
if server.Image != "" {
|
||
|
// Ensure that values were provided for the host's IP address, base configuration directory,
|
||
|
// and a path to the compiled plugin DLL to install.
|
||
|
if containerAddress == "" {
|
||
|
panic("The -caddr argument is required.")
|
||
|
}
|
||
|
|
||
|
if server.Base == "" {
|
||
|
panic("Original configuration directory is required")
|
||
|
}
|
||
|
|
||
|
if pluginPath == "" {
|
||
|
panic("The -dll argument is required.")
|
||
|
}
|
||
|
|
||
|
server.Address = fmt.Sprintf("http://%s:8097", containerAddress)
|
||
|
server.Docker = true
|
||
|
}
|
||
|
|
||
|
// If no browsers were specified, default to Chrome (for speed)
|
||
|
if len(server.Browsers) == 0 {
|
||
|
server.Browsers = []string{"chrome"}
|
||
|
}
|
||
|
|
||
|
// If no tests were specified, only test that the plugin settings page works
|
||
|
if len(server.Tests) == 0 {
|
||
|
server.Tests = []string{"settings"}
|
||
|
}
|
||
|
|
||
|
// Verify that an address was provided
|
||
|
if len(server.Address) == 0 {
|
||
|
panic("Server address is required")
|
||
|
}
|
||
|
|
||
|
fmt.Printf("===== Server: %s =====\n", server.Comment)
|
||
|
|
||
|
if server.Skip {
|
||
|
fmt.Println("Skip: true")
|
||
|
}
|
||
|
|
||
|
fmt.Printf("Docker: %t\n", server.Docker)
|
||
|
if server.Docker {
|
||
|
fmt.Printf("Image: %s\n", server.Image)
|
||
|
}
|
||
|
|
||
|
fmt.Printf("Address: %s\n", server.Address)
|
||
|
fmt.Printf("Browsers: %v\n", server.Browsers)
|
||
|
fmt.Printf("Tests: %v\n", server.Tests)
|
||
|
fmt.Println()
|
||
|
|
||
|
config.Servers[i] = server
|
||
|
}
|
||
|
|
||
|
fmt.Println("=================")
|
||
|
|
||
|
return config
|
||
|
}
|