summaryrefslogtreecommitdiff
path: root/internal/clipboard/clipboard_test.go
blob: c4468f1d6acbc000e84d322c99534e503bebd9fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package clipboard

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// ---- extract ---------------------------------------------------------------

// TestExtract_basic verifies the simplest possible "user:pass" input.
func TestExtract_basic(t *testing.T) {
	user, password, censored, err := extract("user:pass")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user != "user" {
		t.Errorf("user: got %q, want %q", user, "user")
	}
	if password != "pass" {
		t.Errorf("password: got %q, want %q", password, "pass")
	}
	if censored != "user:CENSORED" {
		t.Errorf("censored: got %q, want %q", censored, "user:CENSORED")
	}
}

// TestExtract_with_other_text verifies that surrounding text is preserved and
// only the secret portion is replaced.
func TestExtract_with_other_text(t *testing.T) {
	user, password, censored, err := extract("login user:secret123 notes")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user != "user" {
		t.Errorf("user: got %q, want %q", user, "user")
	}
	if password != "secret123" {
		t.Errorf("password: got %q, want %q", password, "secret123")
	}
	if censored != "login user:CENSORED notes" {
		t.Errorf("censored: got %q, want %q", censored, "login user:CENSORED notes")
	}
}

// TestExtract_multiple_separate_tokens verifies that matchRe picks the first
// token and censorRe replaces all tokens when multiple user:pass pairs exist.
func TestExtract_multiple_separate_tokens(t *testing.T) {
	user, password, censored, err := extract("a:1 b:2")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user != "a" {
		t.Errorf("user: got %q, want %q", user, "a")
	}
	if password != "1" {
		t.Errorf("password: got %q, want %q", password, "1")
	}
	// Both tokens must be censored.
	if censored != "a:CENSORED b:CENSORED" {
		t.Errorf("censored: got %q, want %q", censored, "a:CENSORED b:CENSORED")
	}
}

// TestExtract_multiple_colons verifies the greedy behaviour of \S+:\S+ when
// there are multiple colons in a single whitespace-delimited token.
//
// With input "user:pass:extra", the regex (\S+):(\S+) is greedy on both sides.
// The engine backtracks to find the last colon that still satisfies the pattern,
// so group 1 = "user:pass" and group 2 = "extra".  This matches Ruby's behaviour.
func TestExtract_multiple_colons(t *testing.T) {
	user, password, censored, err := extract("user:pass:extra")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if user != "user:pass" {
		t.Errorf("user: got %q, want %q", user, "user:pass")
	}
	if password != "extra" {
		t.Errorf("password: got %q, want %q", password, "extra")
	}
	if censored != "user:pass:CENSORED" {
		t.Errorf("censored: got %q, want %q", censored, "user:pass:CENSORED")
	}
}

// TestExtract_trailing_colon verifies that a trailing colon with no password
// (e.g. "user:") does not match because \S+ requires at least one character.
func TestExtract_trailing_colon(t *testing.T) {
	_, _, _, err := extract("user:")
	if err == nil {
		t.Fatal("expected error for 'user:' (empty password), got nil")
	}
}

// TestExtract_leading_colon verifies that a leading colon (empty user) does
// not match because \S+ before the colon requires at least one character.
func TestExtract_leading_colon(t *testing.T) {
	_, _, _, err := extract(":pass")
	if err == nil {
		t.Fatal("expected error for ':pass' (empty user), got nil")
	}
}

// TestExtract_no_match verifies that an error is returned when no colon token exists.
func TestExtract_no_match(t *testing.T) {
	_, _, _, err := extract("no colon here")
	if err == nil {
		t.Fatal("expected error for input without colon, got nil")
	}
}

// TestExtract_empty verifies that an empty string returns an error.
func TestExtract_empty(t *testing.T) {
	_, _, _, err := extract("")
	if err == nil {
		t.Fatal("expected error for empty input, got nil")
	}
}

// TestExtract_whitespace_only verifies that whitespace-only input returns an error.
func TestExtract_whitespace_only(t *testing.T) {
	_, _, _, err := extract("   ")
	if err == nil {
		t.Fatal("expected error for whitespace-only input, got nil")
	}
}

// ---- New -------------------------------------------------------------------

// TestNew_darwin verifies that New returns the macOS command when UNAME=Darwin.
func TestNew_darwin(t *testing.T) {
	t.Setenv("UNAME", "Darwin")
	c := New("gpaste-client", "pbcopy")
	if c.cmd != "pbcopy" {
		t.Errorf("cmd: got %q, want %q", c.cmd, "pbcopy")
	}
}

// TestNew_linux verifies that New returns the gnome command when UNAME is unset.
func TestNew_linux(t *testing.T) {
	t.Setenv("UNAME", "")
	c := New("gpaste-client", "pbcopy")
	if c.cmd != "gpaste-client" {
		t.Errorf("cmd: got %q, want %q", c.cmd, "gpaste-client")
	}
}

// ---- Paste -----------------------------------------------------------------

// TestPaste_empty_cmd verifies that Paste returns an error when no command is configured.
func TestPaste_empty_cmd(t *testing.T) {
	c := &Clipboard{cmd: ""}
	err := c.Paste(context.Background(), "user:pass")
	if err == nil {
		t.Fatal("expected error for empty cmd, got nil")
	}
}

// TestPaste_password_reaches_stdin verifies that only the password — not the
// full data string — is written to the clipboard command's stdin.
// A shell script captures stdin to a temp file so we can assert its content.
func TestPaste_password_reaches_stdin(t *testing.T) {
	outFile := filepath.Join(t.TempDir(), "clipboard.txt")
	script := filepath.Join(t.TempDir(), "capture.sh")
	scriptContent := "#!/bin/sh\ncat > " + outFile + "\n"
	if err := os.WriteFile(script, []byte(scriptContent), 0o755); err != nil {
		t.Fatalf("WriteFile script: %v", err)
	}

	c := &Clipboard{cmd: script}
	if err := c.Paste(context.Background(), "user:s3cr3t"); err != nil {
		t.Fatalf("Paste: %v", err)
	}

	// The clipboard command runs in a detached goroutine. Poll for the output
	// file with a short sleep between attempts — in practice the shell script
	// exits within a few milliseconds.
	for range 100 {
		if _, err := os.Stat(outFile); err == nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	got, err := os.ReadFile(outFile)
	if err != nil {
		t.Fatalf("ReadFile: %v", err)
	}
	if string(got) != "s3cr3t" {
		t.Errorf("clipboard content: got %q, want %q", string(got), "s3cr3t")
	}
}

// TestPaste_bad_command verifies that Paste returns an error when the clipboard
// command does not exist.
func TestPaste_bad_command(t *testing.T) {
	c := &Clipboard{cmd: "/nonexistent/command"}
	err := c.Paste(context.Background(), "user:pass")
	if err == nil {
		t.Fatal("expected error for nonexistent command, got nil")
	}
}