mesh/merge-gate head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — pass: builds clipmenu → g14, shanks; no bus step; every machine composes with the change…
mesh/repo-check head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — fail: its merge-check.sh failed: --- FAIL: TestTheLauncherWaitsQuietlyWhileAnotherClipme…
mesh/delivery superseded: a newer head of the same pull request
247 lines
8.2 KiB
Go
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)
|
|
}
|
|
}
|