Skip to content

Commit 99a5a84

Browse files
committed
test: expand Windows API key helper and process management tests
- Add comprehensive Windows-specific tests for API key helper functionality - Test job object creation and validation of process termination on job close - Verify error handling for invalid and non-existent process IDs - Ensure API key extraction works for various Windows command scenarios - Test timeout behavior and process tree termination using job objects - Check error handling for failed commands and empty output cases - Validate that sensitive stderr output is not leaked in error messages - Confirm correct handling of context cancellation and concurrent invocations Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
1 parent 03a377e commit 99a5a84

File tree

1 file changed

+370
-0
lines changed

1 file changed

+370
-0
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
//go:build windows
2+
3+
package util
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"strings"
9+
"testing"
10+
"time"
11+
"unsafe"
12+
13+
"golang.org/x/sys/windows"
14+
)
15+
16+
func TestCreateKillOnCloseJob(t *testing.T) {
17+
job, err := createKillOnCloseJob()
18+
if err != nil {
19+
t.Fatalf("createKillOnCloseJob() error = %v, want nil", err)
20+
}
21+
defer func() {
22+
_ = windows.CloseHandle(job)
23+
}()
24+
25+
// Verify that the job handle is valid
26+
if job == 0 {
27+
t.Error("createKillOnCloseJob() returned invalid handle")
28+
}
29+
30+
// Verify that KILL_ON_JOB_CLOSE flag is set
31+
var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
32+
var returnLength uint32
33+
err = windows.QueryInformationJobObject(
34+
job,
35+
windows.JobObjectExtendedLimitInformation,
36+
uintptr(unsafe.Pointer(&info)),
37+
uint32(unsafe.Sizeof(info)),
38+
&returnLength,
39+
)
40+
if err != nil {
41+
t.Fatalf("QueryInformationJobObject() error = %v", err)
42+
}
43+
44+
if info.BasicLimitInformation.LimitFlags&windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE == 0 {
45+
t.Error("KILL_ON_JOB_CLOSE flag is not set")
46+
}
47+
}
48+
49+
func TestAssignProcessToJob_InvalidPID(t *testing.T) {
50+
job, err := createKillOnCloseJob()
51+
if err != nil {
52+
t.Fatalf("createKillOnCloseJob() error = %v", err)
53+
}
54+
defer func() {
55+
_ = windows.CloseHandle(job)
56+
}()
57+
58+
tests := []struct {
59+
name string
60+
pid int
61+
}{
62+
{
63+
name: "negative PID",
64+
pid: -1,
65+
},
66+
{
67+
name: "PID exceeds max",
68+
pid: 0x80000000,
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
_, err := assignProcessToJob(job, tt.pid)
75+
if err == nil {
76+
t.Error("assignProcessToJob() should return error for invalid PID")
77+
}
78+
if !strings.Contains(err.Error(), "invalid process ID") {
79+
t.Errorf("error should mention invalid PID, got: %v", err)
80+
}
81+
})
82+
}
83+
}
84+
85+
func TestAssignProcessToJob_NonExistentPID(t *testing.T) {
86+
job, err := createKillOnCloseJob()
87+
if err != nil {
88+
t.Fatalf("createKillOnCloseJob() error = %v", err)
89+
}
90+
defer func() {
91+
_ = windows.CloseHandle(job)
92+
}()
93+
94+
// Use a PID that likely doesn't exist (but is valid range)
95+
nonExistentPID := 99999
96+
97+
_, err = assignProcessToJob(job, nonExistentPID)
98+
if err == nil {
99+
t.Error("assignProcessToJob() should return error for non-existent PID")
100+
}
101+
}
102+
103+
func TestGetAPIKeyFromHelper_Windows_Success(t *testing.T) {
104+
tests := []struct {
105+
name string
106+
command string
107+
expected string
108+
}{
109+
{
110+
name: "simple echo command",
111+
command: "echo test-api-key",
112+
expected: "test-api-key",
113+
},
114+
{
115+
name: "command with whitespace",
116+
command: "echo test-key-with-spaces ",
117+
expected: "test-key-with-spaces",
118+
},
119+
{
120+
name: "powershell command",
121+
command: `powershell -Command "Write-Output 'ps-key'"`,
122+
expected: "ps-key",
123+
},
124+
{
125+
name: "set and echo variable",
126+
command: "set KEY=win-key && echo %KEY%",
127+
expected: "win-key",
128+
},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
result, err := GetAPIKeyFromHelper(context.Background(), tt.command)
134+
if err != nil {
135+
t.Fatalf("GetAPIKeyFromHelper() error = %v, want nil", err)
136+
}
137+
if result != tt.expected {
138+
t.Errorf("GetAPIKeyFromHelper() = %q, want %q", result, tt.expected)
139+
}
140+
})
141+
}
142+
}
143+
144+
func TestGetAPIKeyFromHelper_Windows_Timeout(t *testing.T) {
145+
// Use timeout command (Windows specific)
146+
// This will sleep for 15 seconds, which is longer than HelperTimeout (10s)
147+
command := "timeout /t 15 /nobreak >nul"
148+
149+
start := time.Now()
150+
_, err := GetAPIKeyFromHelper(context.Background(), command)
151+
duration := time.Since(start)
152+
153+
if err == nil {
154+
t.Fatal("GetAPIKeyFromHelper() should return timeout error")
155+
}
156+
157+
if !strings.Contains(err.Error(), "timeout") {
158+
t.Errorf("error message should mention timeout, got: %v", err)
159+
}
160+
161+
// Verify it actually timed out around the expected timeout duration
162+
// Allow up to 2 seconds margin
163+
if duration < HelperTimeout || duration > HelperTimeout+2*time.Second {
164+
t.Errorf("timeout duration = %v, want around %v", duration, HelperTimeout)
165+
}
166+
}
167+
168+
func TestGetAPIKeyFromHelper_Windows_KillProcessTree(t *testing.T) {
169+
// Test that the Job Object kills the entire process tree
170+
// Create a command that spawns child processes
171+
command := `cmd /c "timeout /t 15 /nobreak >nul & timeout /t 15 /nobreak >nul"`
172+
173+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
174+
defer cancel()
175+
176+
start := time.Now()
177+
_, err := GetAPIKeyFromHelper(ctx, command)
178+
duration := time.Since(start)
179+
180+
if err == nil {
181+
t.Fatal("GetAPIKeyFromHelper() should return timeout error")
182+
}
183+
184+
// Should timeout quickly (around 2 seconds, not 15)
185+
if duration > 3*time.Second {
186+
t.Errorf("timeout took too long: %v, expected around 2s", duration)
187+
}
188+
}
189+
190+
func TestGetAPIKeyFromHelper_Windows_CommandFailure(t *testing.T) {
191+
tests := []struct {
192+
name string
193+
command string
194+
wantErr string
195+
}{
196+
{
197+
name: "non-existent command",
198+
command: "nonexistentcommand12345",
199+
wantErr: "failed",
200+
},
201+
{
202+
name: "command with exit code 1",
203+
command: "exit 1",
204+
wantErr: "failed",
205+
},
206+
{
207+
name: "invalid syntax",
208+
command: "echo %UNDEFINED_VAR && exit 1",
209+
wantErr: "failed",
210+
},
211+
}
212+
213+
for _, tt := range tests {
214+
t.Run(tt.name, func(t *testing.T) {
215+
_, err := GetAPIKeyFromHelper(context.Background(), tt.command)
216+
if err == nil {
217+
t.Fatal("GetAPIKeyFromHelper() should return error for failed command")
218+
}
219+
if !strings.Contains(err.Error(), tt.wantErr) {
220+
t.Errorf("error should contain %q, got: %v", tt.wantErr, err)
221+
}
222+
})
223+
}
224+
}
225+
226+
func TestGetAPIKeyFromHelper_Windows_EmptyOutput(t *testing.T) {
227+
tests := []struct {
228+
name string
229+
command string
230+
}{
231+
{
232+
name: "command with no output",
233+
command: "rem no output",
234+
},
235+
{
236+
name: "command outputting only whitespace",
237+
command: "echo ",
238+
},
239+
}
240+
241+
for _, tt := range tests {
242+
t.Run(tt.name, func(t *testing.T) {
243+
_, err := GetAPIKeyFromHelper(context.Background(), tt.command)
244+
if err == nil {
245+
t.Fatal("GetAPIKeyFromHelper() with empty output should return error")
246+
}
247+
if !strings.Contains(err.Error(), "empty output") {
248+
t.Errorf("error message should mention empty output, got: %v", err)
249+
}
250+
})
251+
}
252+
}
253+
254+
func TestGetAPIKeyFromHelper_Windows_SecurityStderr(t *testing.T) {
255+
// Command that outputs to stderr (sensitive info should not be leaked in error)
256+
command := "echo secret-data 1>&2 && exit 1"
257+
258+
_, err := GetAPIKeyFromHelper(context.Background(), command)
259+
if err == nil {
260+
t.Fatal("GetAPIKeyFromHelper() should return error when command fails")
261+
}
262+
263+
// The error message should NOT contain the stderr output (security consideration)
264+
if strings.Contains(err.Error(), "secret-data") {
265+
t.Error("error message should not leak stderr content (security issue)")
266+
}
267+
}
268+
269+
func TestGetAPIKeyFromHelper_Windows_ComplexCommands(t *testing.T) {
270+
tests := []struct {
271+
name string
272+
command string
273+
expected string
274+
}{
275+
{
276+
name: "piped commands",
277+
command: "echo my-api-key | findstr api",
278+
expected: "my-api-key",
279+
},
280+
{
281+
name: "command with variable substitution",
282+
command: "set KEY=test-123 && echo %KEY%",
283+
expected: "test-123",
284+
},
285+
{
286+
name: "for loop",
287+
command: `for /F %i in ('echo nested-key') do @echo %i`,
288+
expected: "nested-key",
289+
},
290+
}
291+
292+
for _, tt := range tests {
293+
t.Run(tt.name, func(t *testing.T) {
294+
result, err := GetAPIKeyFromHelper(context.Background(), tt.command)
295+
if err != nil {
296+
t.Fatalf("GetAPIKeyFromHelper() error = %v, want nil", err)
297+
}
298+
if result != tt.expected {
299+
t.Errorf("GetAPIKeyFromHelper() = %q, want %q", result, tt.expected)
300+
}
301+
})
302+
}
303+
}
304+
305+
func TestGetAPIKeyFromHelper_Windows_ContextCancellation(t *testing.T) {
306+
ctx, cancel := context.WithCancel(context.Background())
307+
308+
// Start a long-running command
309+
done := make(chan error, 1)
310+
go func() {
311+
_, err := GetAPIKeyFromHelper(ctx, "timeout /t 30 /nobreak >nul")
312+
done <- err
313+
}()
314+
315+
// Cancel after a short delay
316+
time.Sleep(500 * time.Millisecond)
317+
cancel()
318+
319+
// Wait for the command to be cancelled
320+
select {
321+
case err := <-done:
322+
if err == nil {
323+
t.Error("GetAPIKeyFromHelper() should return error on context cancellation")
324+
}
325+
if !strings.Contains(err.Error(), "timeout") {
326+
t.Errorf("error should mention timeout, got: %v", err)
327+
}
328+
case <-time.After(5 * time.Second):
329+
t.Error("GetAPIKeyFromHelper() took too long to respond to cancellation")
330+
}
331+
}
332+
333+
func TestGetAPIKeyFromHelper_Windows_MultipleInvocations(t *testing.T) {
334+
results := make(chan string, 3)
335+
errors := make(chan error, 3)
336+
337+
for i := 0; i < 3; i++ {
338+
go func(n int) {
339+
result, err := GetAPIKeyFromHelper(
340+
context.Background(),
341+
fmt.Sprintf("echo test-key-%d", n),
342+
)
343+
if err != nil {
344+
errors <- err
345+
} else {
346+
results <- result
347+
}
348+
}(i)
349+
}
350+
351+
// Collect results
352+
successCount := 0
353+
for i := 0; i < 3; i++ {
354+
select {
355+
case result := <-results:
356+
if !strings.HasPrefix(result, "test-key-") {
357+
t.Errorf("unexpected result: %s", result)
358+
}
359+
successCount++
360+
case err := <-errors:
361+
t.Errorf("unexpected error: %v", err)
362+
case <-time.After(5 * time.Second):
363+
t.Error("timeout waiting for results")
364+
}
365+
}
366+
367+
if successCount != 3 {
368+
t.Errorf("expected 3 successful invocations, got %d", successCount)
369+
}
370+
}

0 commit comments

Comments
 (0)