Files
mesh-catalog/modules/clipmenu/cmd/clipmenu-tools/clipboard_test.go
T

247 lines
8.2 KiB
Go

package main
import (
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
)
const nobody = 4194400
func TestEntryIdsAreWhatClipmenuNamesItsFiles(t *testing.T) {
for _, line := range []string{"hello", "", "two words (3 lines)", "ünïcode ✓", strings.Repeat("x", 300)} {
out, err := exec.Command("bash", "-c", `cksum <<< "$1"`, "_", line).Output()
if err != nil {
t.Skip("bash or cksum is missing here")
}
if got, want := entryID(line), strings.TrimSpace(string(out)); got != want {
t.Errorf("%q: %s, cksum says %s", line, got, want)
}
}
}
// store makes clipmenu's store as clipmenud leaves it, for the account the tools run as.
func store(t *testing.T, clips map[string]string, order ...string) string {
t.Helper()
fakeMachine(t)
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
t.Setenv("USER", "op")
t.Setenv("CM_DIR", "")
dir := filepath.Join(runtime, "clipmenu.6.op")
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(runtime, "bus"), nil, 0o600); err != nil {
t.Fatal(err)
}
var cache strings.Builder
for i, line := range order {
cache.WriteString(strconv.FormatInt(1_700_000_000_000_000_000+int64(i)*1_000_000_000, 10) + " " + line + "\n")
if err := os.WriteFile(filepath.Join(dir, entryID(line)), []byte(clips[line]), 0o600); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(dir, "line_cache"), []byte(cache.String()), 0o600); err != nil {
t.Fatal(err)
}
return dir
}
func TestTheHistoryAnswersCountTimesAndLengthsNeverContent(t *testing.T) {
store(t, map[string]string{"first": "first", "sk-ant-api03-SECRETKEY (2 lines)": "sk-ant-api03-SECRETKEY\nline two"}, "first", "sk-ant-api03-SECRETKEY (2 lines)", "first")
fakeProcess(t, nobody, "clipmenud")
h, err := History(10)
if err != nil {
t.Fatal(err)
}
if !h.Collecting || h.Total != 2 || len(h.Entries) != 2 || h.Entries[0].Bytes != 5 || h.Entries[1].Bytes != 31 || h.Entries[0].At == "" {
t.Fatalf("%+v", h)
}
raw, _ := json.Marshal(h)
for _, leak := range []string{"first", "SECRETKEY", "line two"} {
if strings.Contains(string(raw), leak) {
t.Fatalf("the history answers %q: %s", leak, raw)
}
}
if h, _ := History(1); len(h.Entries) != 1 {
t.Fatalf("limited: %+v", h)
}
}
// at is the time History reports for the entry at place.
func at(t *testing.T, place int) string {
t.Helper()
h, err := History(100)
if err != nil || place > len(h.Entries) {
t.Fatalf("%+v, %v", h, err)
}
return h.Entries[place-1].At
}
func TestADeleteWhoseListingIsStaleIsRefused(t *testing.T) {
dir := store(t, map[string]string{"a": "a", "b": "b"}, "a", "b")
listed := at(t, 1) // b
f, _ := os.OpenFile(filepath.Join(dir, "line_cache"), os.O_APPEND|os.O_WRONLY, 0o600)
f.WriteString("1800000000000000000 a\n") // a copy arrives: place 1 is now a
f.Close()
if _, err := Delete(1, listed); err == nil || !strings.Contains(err.Error(), "list it again") {
t.Fatalf("the wrong entry was deleted: %v", err)
}
if h, _ := History(10); h.Total != 2 {
t.Fatalf("%+v", h)
}
}
func TestAnEntryIsDeletedByItsPlaceWithItsTextAndNeverByItsText(t *testing.T) {
dir := store(t, map[string]string{"a": "a", "b": "b", "c": "c"}, "a", "b", "c", "a")
// newest first: a, c, b
r, err := Delete(1, at(t, 1))
if err != nil || r.Removed != 2 || r.Remaining != 2 {
t.Fatalf("by place, both copies: %+v, %v", r, err)
}
if _, err := os.Stat(filepath.Join(dir, entryID("a"))); !errors.Is(err, os.ErrNotExist) {
t.Fatal("the text stayed")
}
if r, err := Delete(2, at(t, 2)); err != nil || r.Removed != 1 || r.Remaining != 1 {
t.Fatalf("second place (b): %+v, %v", r, err)
}
if _, err := Delete(5, "x"); err == nil {
t.Fatal("a missing place was reported deleted")
}
if _, err := Delete(0, "x"); err == nil {
t.Fatal("place 0 was accepted")
}
cache, _ := os.ReadFile(filepath.Join(dir, "line_cache"))
if !strings.HasSuffix(string(cache), " c\n") || strings.Count(string(cache), "\n") != 1 {
t.Fatalf("line cache: %q", cache)
}
for _, tool := range tools() {
if tool.Name == "clipmenu_delete" {
if _, ok := tool.Input["line"]; ok {
t.Fatal("delete still takes a line")
}
if _, ok := tool.Input["id"]; ok {
t.Fatal("delete still takes an id")
}
}
}
}
func TestClearForgetsEverythingButKeepsTheDaemonsLocks(t *testing.T) {
dir := store(t, map[string]string{"a": "a", "b": "b"}, "a", "b")
if err := os.WriteFile(filepath.Join(dir, "session_lock"), nil, 0o600); err != nil {
t.Fatal(err)
}
r, err := Clear()
if err != nil || r.Removed != 2 {
t.Fatalf("%+v, %v", r, err)
}
left, _ := os.ReadDir(dir)
var names []string
for _, f := range left {
names = append(names, f.Name())
}
if strings.Join(names, ",") != "line_cache,lock,session_lock" {
t.Fatalf("left: %v", names)
}
}
func copyBed(t *testing.T, xsel string) string {
t.Helper()
store(t, nil)
fakeProcess(t, nobody, "i3", "DISPLAY=:1")
bin := fakeBinaries(t, map[string]string{"systemctl": "true", "xsel": xsel,
"systemd-run": `echo "$*" > "$LOG"; for last; do :; done; cat "$last" > "$LOG.text"`})
t.Setenv("LOG", filepath.Join(bin, "log"))
readBackWait = 0
return bin
}
func TestCopyHandsTheTextToAnOwnerUnderTheAccountsServiceManagerAndReadsItBack(t *testing.T) {
bin := copyBed(t, `cat "$LOG.text"`)
r, err := Copy("secret-free text")
if err != nil || r.Bytes != 16 || !r.Verified || !strings.HasPrefix(r.Unit, "clipmenu-copy-") {
t.Fatalf("%+v, %v", r, err)
}
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
if !strings.Contains(string(asked), "--setenv=DISPLAY=:1 -- /bin/sh -c xsel --nodetach --input --clipboard") {
t.Fatalf("asked: %s", asked)
}
handed, _ := os.ReadFile(filepath.Join(bin, "log.text"))
if string(handed) != "secret-free text" {
t.Fatalf("handed over: %q", handed)
}
if _, err := Copy(""); err == nil {
t.Fatal("empty text was accepted")
}
}
func TestCopyFailsWhenTheClipboardHoldsSomethingElse(t *testing.T) {
copyBed(t, `printf 'old contents'`)
_, err := Copy("new text")
if err == nil || !strings.Contains(err.Error(), "does not hold") || strings.Contains(err.Error(), "old contents") {
t.Fatalf("a copy that did not land was reported: %v", err)
}
}
func TestCopyFailsSayingWhyWhenTheOwnerCannotReachTheDisplay(t *testing.T) {
copyBed(t, `echo "xsel: Can't open display: (null)" >&2; exit 1`)
_, err := Copy("new text")
if err == nil || !strings.Contains(err.Error(), "Can't open display") {
t.Fatalf("the reason was not given: %v", err)
}
}
func TestPasteSaysHowMuchTheClipboardHoldsNeverWhat(t *testing.T) {
fakeMachine(t)
if _, err := Paste(); !errors.Is(err, ErrNoSession) {
t.Fatal(err)
}
fakeProcess(t, nobody, "i3", "DISPLAY=:1")
fakeBinaries(t, map[string]string{"xsel": `[ "$*" = "--output --clipboard" ] && printf 'on the clipboard'`})
p, err := Paste()
raw, _ := json.Marshal(p)
if err != nil || p.Bytes != 16 || strings.Contains(string(raw), "on the clipboard") {
t.Fatalf("%s, %v", raw, err)
}
}
func TestWithoutAStoreTheChangesSayWhy(t *testing.T) {
fakeMachine(t)
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
if err := os.MkdirAll(runtime, 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("USER", "op")
if _, err := Clear(); err == nil || !strings.Contains(err.Error(), "clipmenud has not run") {
t.Fatal(err)
}
if h, err := History(5); err != nil || h.Total != 0 || h.Collecting {
t.Fatalf("an empty history: %+v, %v", h, err)
}
}
// clipmenud is a bash script: its process is named "bash" and only its command line names clipmenud
// (issue 860: collecting was false while the daemon ran).
func TestCollectingSeesClipmenudRunAsABashScript(t *testing.T) {
store(t, map[string]string{"first": "first"}, "first")
fakeProcess(t, nobody, "bash")
cmdline := filepath.Join(procRoot, strconv.Itoa(nobody), "cmdline")
if err := os.WriteFile(cmdline, []byte("bash\x00/usr/bin/clipmenud\x00"), 0o644); err != nil {
t.Fatal(err)
}
if h, err := History(10); err != nil || !h.Collecting {
t.Fatalf("clipmenud runs as a bash script: %+v %v", h, err)
}
// another bash script is not the daemon
os.WriteFile(cmdline, []byte("bash\x00/usr/bin/other\x00"), 0o644)
if h, _ := History(10); h.Collecting {
t.Fatalf("a bash script that is not clipmenud counted: %+v", h)
}
}