mesh/merge-gate out of date: judged against trunk aee97bf8ad42, the trunk is now 7443938fef69; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk aee97bf8ad42, the trunk is now 7443938fef69; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
clipmenud is declared as the operator's user unit, enabled and running, with a launcher that finds the display. Copy reads the clipboard back and fails with the reason. History and paste answer counts, times and lengths, never content.
228 lines
7.4 KiB
Go
228 lines
7.4 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)
|
|
}
|
|
}
|