Skip to content

Commit 977d5e2

Browse files
committed
test: diagnostic mode interfaces and unit tests
1 parent 2120d51 commit 977d5e2

File tree

3 files changed

+542
-54
lines changed

3 files changed

+542
-54
lines changed

cmd/agent_smith/diagnostic.go

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package main
22

33
import (
44
"bufio"
5+
"context"
56
"crypto/tls"
67
"encoding/json"
78
"fmt"
9+
"io"
810
"net"
911
"os"
1012
"os/exec"
@@ -17,6 +19,40 @@ import (
1719
"github.com/RewstApp/agent-smith-go/internal/version"
1820
)
1921

22+
// tlsDialer abstracts TLS connectivity checks so tests can inject fakes.
23+
type tlsDialer interface {
24+
Dial(host, port string) bool
25+
}
26+
27+
type defaultTLSDialer struct{}
28+
29+
func (d *defaultTLSDialer) Dial(host, port string) bool {
30+
return testTLSConnection(host, port)
31+
}
32+
33+
// serviceStatusQuerier abstracts per-platform service status so tests can
34+
// inject fakes into scanAgentsFrom.
35+
type serviceStatusQuerier interface {
36+
QueryStatus(name string) (installed, running bool)
37+
}
38+
39+
type osServiceQuerier struct{}
40+
41+
func (q *osServiceQuerier) QueryStatus(name string) (bool, bool) {
42+
return queryServiceStatus(name)
43+
}
44+
45+
// logFileOpener abstracts os.Open so tests can inject an in-memory reader.
46+
type logFileOpener interface {
47+
Open(name string) (io.ReadCloser, error)
48+
}
49+
50+
type osLogFileOpener struct{}
51+
52+
func (o *osLogFileOpener) Open(name string) (io.ReadCloser, error) {
53+
return os.Open(name) // #nosec G304 - path comes from internal config
54+
}
55+
2056
type agentInfo struct {
2157
OrgId string
2258
ConfigFile string
@@ -27,12 +63,21 @@ type agentInfo struct {
2763
}
2864

2965
func runDiagnostic(params *diagnosticContext) {
30-
reader := bufio.NewReader(os.Stdin)
66+
runDiagnosticFull(context.Background(), params, os.Stdin, &defaultTLSDialer{}, &osLogFileOpener{}, getAgentDataRoot())
67+
}
68+
69+
// runDiagnosticWith is the testable entry point with all dependencies injected.
70+
func runDiagnosticWith(params *diagnosticContext, input io.Reader, dialer tlsDialer, opener logFileOpener) {
71+
runDiagnosticFull(context.Background(), params, input, dialer, opener, getAgentDataRoot())
72+
}
73+
74+
func runDiagnosticFull(ctx context.Context, params *diagnosticContext, input io.Reader, dialer tlsDialer, opener logFileOpener, agentRoot string) {
75+
reader := bufio.NewReader(input)
3176

3277
printHeader()
3378

3479
// Scan for installed agents
35-
agents := scanAgents()
80+
agents := scanAgentsFrom(agentRoot)
3681

3782
if len(agents) == 0 && params.OrgId == "" {
3883
fmt.Println("\n No installed agents found.")
@@ -77,13 +122,13 @@ func runDiagnostic(params *diagnosticContext) {
77122
case "2":
78123
runCommandTest()
79124
case "3":
80-
runConnectivityTest(target)
125+
runConnectivityTestWith(target, dialer)
81126
case "4":
82127
runTempDirTest(target)
83128
case "5":
84-
runLiveLogs(target)
129+
runLiveLogsWith(ctx, target, opener)
85130
case "6":
86-
runAllChecks(params, agents, target)
131+
runAllChecksWith(params, agents, target, dialer, opener)
87132
case "0", "q", "quit", "exit":
88133
fmt.Println("\n Exiting diagnostic mode.")
89134
return
@@ -145,9 +190,13 @@ func selectAgent(reader *bufio.Reader, agents []agentInfo) agentInfo {
145190
}
146191
}
147192

148-
// scanAgents discovers installed agents by scanning the data directory
193+
// scanAgents discovers installed agents by scanning the platform data directory.
149194
func scanAgents() []agentInfo {
150-
root := getAgentDataRoot()
195+
return scanAgentsFrom(getAgentDataRoot())
196+
}
197+
198+
// scanAgentsFrom is the testable core of scanAgents.
199+
func scanAgentsFrom(root string) []agentInfo {
151200
entries, err := os.ReadDir(root)
152201
if err != nil {
153202
return nil
@@ -258,7 +307,7 @@ func runCommandTest() {
258307

259308
// ── Check 3: MQTT/WebSocket connectivity ──
260309

261-
func runConnectivityTest(target agentInfo) {
310+
func runConnectivityTestWith(target agentInfo, dialer tlsDialer) {
262311
printSection("MQTT/WebSocket Connectivity")
263312

264313
if target.Device == nil {
@@ -277,7 +326,7 @@ func runConnectivityTest(target agentInfo) {
277326

278327
// Test MQTT port (8883)
279328
fmt.Printf(" Testing MQTT (TLS port 8883)... ")
280-
mqttOk := testTLSConnection(host, "8883")
329+
mqttOk := dialer.Dial(host, "8883")
281330
if mqttOk {
282331
fmt.Println("OK")
283332
} else {
@@ -287,7 +336,7 @@ func runConnectivityTest(target agentInfo) {
287336

288337
// Test WebSocket port (443)
289338
fmt.Printf(" Testing WebSocket (port 443)... ")
290-
wsOk := testTLSConnection(host, "443")
339+
wsOk := dialer.Dial(host, "443")
291340
if wsOk {
292341
fmt.Println("OK")
293342
} else {
@@ -374,76 +423,58 @@ func runTempDirTest(target agentInfo) {
374423

375424
// ── Check 5: Live log viewer ──
376425

377-
func runLiveLogs(target agentInfo) {
426+
func runLiveLogsWith(ctx context.Context, target agentInfo, opener logFileOpener) {
378427
printSection("Live Log Viewer")
379428

380429
logFile := target.LogFile
381430
fmt.Printf(" Log file: %s\n", logFile)
382431
fmt.Println(" Press Ctrl+C to stop watching.")
383432
fmt.Println()
384433

385-
file, err := os.Open(logFile)
434+
rc, err := opener.Open(logFile)
386435
if err != nil {
387436
printResult(false, fmt.Sprintf("Cannot open log file: %v", err))
388437
fmt.Println(" The agent may not have been started yet, or the log file path is incorrect.")
389438
return
390439
}
391-
defer func() { _ = file.Close() }()
440+
defer func() { _ = rc.Close() }()
392441

393-
// Seek to the last 4KB to show recent entries
394-
info, err := file.Stat()
395-
if err == nil && info.Size() > 4096 {
396-
_, _ = file.Seek(-4096, 2)
397-
// Discard partial line
398-
reader := bufio.NewReader(file)
399-
_, _ = reader.ReadString('\n')
400-
401-
fmt.Println(" ... (showing last entries)")
402-
fmt.Println()
403-
404-
// Print remaining buffered content
405-
for {
406-
line, err := reader.ReadString('\n')
407-
if line != "" {
408-
fmt.Print(" ", line)
409-
}
410-
if err != nil {
411-
break
412-
}
442+
// Read all buffered content line by line
443+
reader := bufio.NewReader(rc)
444+
for {
445+
line, err := reader.ReadString('\n')
446+
if line != "" {
447+
fmt.Print(" ", line)
413448
}
414-
} else {
415-
// Small file, read from beginning
416-
reader := bufio.NewReader(file)
417-
for {
418-
line, err := reader.ReadString('\n')
419-
if line != "" {
420-
fmt.Print(" ", line)
421-
}
422-
if err != nil {
423-
break
424-
}
449+
if err != nil {
450+
break
425451
}
426452
}
427453

428-
// Tail the file for new entries
454+
// Tail for new entries until context is cancelled
455+
buf := make([]byte, 4096)
429456
for {
430-
line := make([]byte, 4096)
431-
n, err := file.Read(line)
432-
if n > 0 {
433-
fmt.Print(" ", string(line[:n]))
434-
}
435-
if err != nil {
436-
time.Sleep(500 * time.Millisecond)
457+
select {
458+
case <-ctx.Done():
459+
return
460+
default:
461+
n, err := rc.Read(buf)
462+
if n > 0 {
463+
fmt.Print(" ", string(buf[:n]))
464+
}
465+
if err != nil {
466+
time.Sleep(500 * time.Millisecond)
467+
}
437468
}
438469
}
439470
}
440471

441472
// ── Check 6: Run all checks ──
442473

443-
func runAllChecks(params *diagnosticContext, agents []agentInfo, target agentInfo) {
474+
func runAllChecksWith(params *diagnosticContext, agents []agentInfo, target agentInfo, dialer tlsDialer, opener logFileOpener) {
444475
runCheckAgents(params, agents)
445476
runCommandTest()
446-
runConnectivityTest(target)
477+
runConnectivityTestWith(target, dialer)
447478
runTempDirTest(target)
448479
}
449480

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNewDiagnosticContext_Success(t *testing.T) {
8+
_, err := newDiagnosticContext(
9+
[]string{"--diagnostic"},
10+
&mockSystemInfoProvider{},
11+
&mockDomainInfoProvider{},
12+
&mockServiceManager{},
13+
&mockFileSystem{},
14+
)
15+
if err != nil {
16+
t.Fatalf("expected success, got error: %v", err)
17+
}
18+
}
19+
20+
func TestNewDiagnosticContext_WithOrgId(t *testing.T) {
21+
ctx, err := newDiagnosticContext(
22+
[]string{"--org-id", "test-org", "--diagnostic"},
23+
&mockSystemInfoProvider{},
24+
&mockDomainInfoProvider{},
25+
&mockServiceManager{},
26+
&mockFileSystem{},
27+
)
28+
if err != nil {
29+
t.Fatalf("expected success, got error: %v", err)
30+
}
31+
if ctx.OrgId != "test-org" {
32+
t.Errorf("expected OrgId 'test-org', got %q", ctx.OrgId)
33+
}
34+
}
35+
36+
func TestNewDiagnosticContext_MissingFlag(t *testing.T) {
37+
_, err := newDiagnosticContext(
38+
[]string{"--org-id", "test-org"},
39+
&mockSystemInfoProvider{},
40+
&mockDomainInfoProvider{},
41+
&mockServiceManager{},
42+
&mockFileSystem{},
43+
)
44+
if err == nil {
45+
t.Fatal("expected error for missing --diagnostic flag, got nil")
46+
}
47+
}
48+
49+
func TestNewDiagnosticContext_NoArgs(t *testing.T) {
50+
_, err := newDiagnosticContext(
51+
[]string{},
52+
&mockSystemInfoProvider{},
53+
&mockDomainInfoProvider{},
54+
&mockServiceManager{},
55+
&mockFileSystem{},
56+
)
57+
if err == nil {
58+
t.Fatal("expected error for empty args, got nil")
59+
}
60+
}
61+
62+
func TestNewDiagnosticContext_UnknownFlag(t *testing.T) {
63+
_, err := newDiagnosticContext(
64+
[]string{"--diagnostic", "--unknown-flag"},
65+
&mockSystemInfoProvider{},
66+
&mockDomainInfoProvider{},
67+
&mockServiceManager{},
68+
&mockFileSystem{},
69+
)
70+
if err == nil {
71+
t.Fatal("expected error for unknown flag, got nil")
72+
}
73+
}

0 commit comments

Comments
 (0)