154 lines
5.6 KiB
Go
154 lines
5.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/control"
|
|
"git.stormux.org/storm/fenrir/internal/interaction"
|
|
)
|
|
|
|
func TestApplicationAliasesShellCheck(t *testing.T) {
|
|
path, err := exec.LookPath("shellcheck")
|
|
if err != nil {
|
|
t.Skip("shellcheck unavailable")
|
|
}
|
|
for _, id := range []string{"networkmanager", "ninjam", "rogue", "codex"} {
|
|
process := exec.Command(path, "-s", "bash", "-")
|
|
process.Stdin = strings.NewReader(application_alias(id, "") + "\n")
|
|
if output, err := process.CombinedOutput(); err != nil {
|
|
t.Fatalf("%s: %v: %s", id, err, output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplicationAliasThroughPrivateSocket(t *testing.T) {
|
|
socatPath, err := exec.LookPath("socat")
|
|
if err != nil {
|
|
t.Skip("socat unavailable")
|
|
}
|
|
directory := t.TempDir()
|
|
socketPath := filepath.Join(directory, "reader.sock")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
server, err := control.Open(ctx, control.Options{SocketFile: socketPath, RegistryDirectory: directory})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer server.Close()
|
|
if err := os.Symlink(socatPath, filepath.Join(directory, "socat")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(directory, "cninjam"), []byte("#!/bin/sh\necho application-launched\n"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, _ := persistence_application(t, true)
|
|
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
|
process := exec.CommandContext(ctx, "/bin/bash", "--noprofile", "--norc", "-c", "shopt -s expand_aliases\n"+application_alias("ninjam", socketPath)+"\ncninjam\n")
|
|
process.Env = append(os.Environ(), "PATH="+directory)
|
|
completed := make(chan struct{})
|
|
var output []byte
|
|
var processError error
|
|
go func() {
|
|
output, processError = process.CombinedOutput()
|
|
close(completed)
|
|
}()
|
|
select {
|
|
case request := <-server.Requests():
|
|
if request.Text != "command application ninjam" {
|
|
t.Fatal(request.Text)
|
|
}
|
|
a.remote_request(request)
|
|
case <-ctx.Done():
|
|
t.Fatal("alias did not send selection request")
|
|
}
|
|
<-completed
|
|
if processError != nil || string(output) != "application-launched\n" || a.overrides[1] == nil {
|
|
t.Fatalf("launch failed: %v, %s", processError, output)
|
|
}
|
|
override_read(t, a, override_screen(1, "ninjam"))
|
|
if a.overrideCurrent == nil || a.overrideCurrent.id != "ninjam" {
|
|
t.Fatal("pre-launch selection did not activate when application appeared")
|
|
}
|
|
}
|
|
|
|
func TestApplicationAliasLaunchesWithArgumentsDespiteReaderFailure(t *testing.T) {
|
|
for _, scenario := range []string{"success", "rejected", "failed", "missing"} {
|
|
t.Run(scenario, func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
requestPath := filepath.Join(directory, "request")
|
|
argsPath := filepath.Join(directory, "args")
|
|
program := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ALIAS_ARGS\"\nexit 7\n"
|
|
if err := os.WriteFile(filepath.Join(directory, "cninjam"), []byte(program), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if scenario != "missing" {
|
|
client := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ALIAS_REQUEST\"\n/bin/cat >> \"$ALIAS_REQUEST\"\n"
|
|
if scenario == "rejected" {
|
|
client += "echo 'Command remote control is disabled'\n"
|
|
}
|
|
if scenario == "failed" {
|
|
client += "exit 1\n"
|
|
}
|
|
if err := os.WriteFile(filepath.Join(directory, "socat"), []byte(client), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
socketPath := "/tmp/example socket'$(false).sock"
|
|
script := "set -euo pipefail\nshopt -s expand_aliases\n" + application_alias("ninjam", socketPath) + "\ncninjam 'argument with spaces' '*.wav'\n"
|
|
process := exec.Command("/bin/bash", "--noprofile", "--norc", "-c", script)
|
|
process.Env = append(os.Environ(), "PATH="+directory, "ALIAS_ARGS="+argsPath, "ALIAS_REQUEST="+requestPath)
|
|
output, err := process.CombinedOutput()
|
|
if failure, ok := err.(*exec.ExitError); !ok || failure.ExitCode() != 7 {
|
|
t.Fatalf("application status lost: %v, %s", err, output)
|
|
}
|
|
args, err := os.ReadFile(argsPath)
|
|
if err != nil || string(args) != "argument with spaces\n*.wav\n" {
|
|
t.Fatal("arguments lost", string(args), err)
|
|
}
|
|
if scenario != "missing" {
|
|
request, err := os.ReadFile(requestPath)
|
|
if err != nil || string(request) != "-T\n2\n-\nUNIX-CLIENT:"+socketPath+"\ncommand application ninjam\n" {
|
|
t.Fatal("socket or request changed by shell", string(request), err)
|
|
}
|
|
}
|
|
if scenario == "rejected" && !strings.Contains(string(output), "Command remote control is disabled") {
|
|
t.Fatal("reader error hidden", string(output))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationsMenuCopiesAliasWithoutSelecting(t *testing.T) {
|
|
a, voice := persistence_application(t, true)
|
|
override_read(t, a, override_screen(1, "ninjam"))
|
|
a.open_applications()
|
|
a.ui.handle(interaction.Key{Name: "tab"})
|
|
a.ui.handle(interaction.Key{Name: "enter"})
|
|
if got := voice.messages[len(voice.messages)-1]; got != "Default has no application alias" {
|
|
t.Fatal(got)
|
|
}
|
|
a.ui.handle(interaction.Key{Name: "tab", Shift: true})
|
|
// Use the list's prefix search, then Tab to the copy button.
|
|
a.ui.handle(interaction.Key{Text: "n"})
|
|
a.ui.handle(interaction.Key{Text: "i"})
|
|
a.ui.handle(interaction.Key{Name: "tab"})
|
|
a.ui.handle(interaction.Key{Name: "enter"})
|
|
if !a.ui.active || len(a.overrides) != 0 {
|
|
t.Fatal("copy closed menu or selected an override")
|
|
}
|
|
text, ok := a.clipboards.history.Current()
|
|
if !ok || text != application_alias("ninjam", "") {
|
|
t.Fatal("wrong clipboard contents", text)
|
|
}
|
|
a.ui.handle(interaction.Key{Name: "escape"})
|
|
}
|