Skip to content

Commit eec3301

Browse files
committed
⭐️ native windows service tests
1 parent 3ee1fe8 commit eec3301

5 files changed

Lines changed: 246 additions & 0 deletions

File tree

.gitattributes

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Prevent line ending conversion for all files (preserve LF)
2+
* text=auto eol=lf
3+
4+
# Ensure shell scripts use LF
5+
*.sh text eol=lf
6+
7+
# Ensure test data files preserve original line endings
8+
*.toml text eol=lf
9+
*.json text eol=lf
10+
*.yaml text eol=lf
11+
*.yml text eol=lf
12+
*.txt text eol=lf
13+
14+
# Binary files
15+
*.png binary
16+
*.jpg binary
17+
*.gif binary
18+
*.ico binary
19+
*.pdf binary
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Windows Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- "providers/os/**"
7+
- ".github/workflows/pr-test-windows.yml"
8+
pull_request:
9+
paths:
10+
- "providers/os/**"
11+
- ".github/workflows/pr-test-windows.yml"
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
windows-test:
18+
runs-on: windows-latest
19+
timeout-minutes: 30
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
24+
- name: Import environment variables from file
25+
shell: bash
26+
run: cat ".github/env" >> $GITHUB_ENV
27+
28+
- name: Install Go
29+
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
30+
with:
31+
go-version: ">=${{ env.golang-version }}"
32+
cache: false
33+
34+
- name: Run Windows-specific tests
35+
shell: pwsh
36+
run: |
37+
# Run tests for packages with Windows-specific code
38+
# The build tag ensures only Windows tests are compiled and run
39+
go test -v -tags windows ./providers/os/resources/services/... 2>&1 | Tee-Object -FilePath windows-test-output.txt
40+
41+
- name: Upload test output
42+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
43+
if: success() || failure()
44+
with:
45+
name: windows-test-output
46+
path: windows-test-output.txt

providers/os/connection/mock/mock.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ func (c *Connection) RunCommand(command string) (*shared.Command, error) {
206206
}
207207

208208
func (c *Connection) FileInfo(path string) (shared.FileInfoDetails, error) {
209+
// Normalize path separators to forward slashes for cross-platform compatibility
210+
path = filepath.ToSlash(path)
211+
209212
found, ok := c.data.Files[path]
210213
if !ok {
211214
return shared.FileInfoDetails{}, errors.New("file not found: " + path)
@@ -246,6 +249,10 @@ func (c *Connection) Open(name string) (afero.File, error) {
246249
c.mutex.Lock()
247250
defer c.mutex.Unlock()
248251

252+
// Normalize path separators to forward slashes for cross-platform compatibility
253+
// Mock data uses forward slashes, but Windows filepath.Join uses backslashes
254+
name = filepath.ToSlash(name)
255+
249256
data, ok := c.data.Files[name]
250257
if !ok || data.Enoent {
251258
return nil, os.ErrNotExist
@@ -264,6 +271,8 @@ func (c *Connection) OpenFile(name string, flag int, perm os.FileMode) (afero.Fi
264271
func (c *Connection) Remove(name string) error {
265272
c.mutex.Lock()
266273
defer c.mutex.Unlock()
274+
// Normalize path separators to forward slashes for cross-platform compatibility
275+
name = filepath.ToSlash(name)
267276
delete(c.data.Files, name)
268277
return nil
269278
}
@@ -275,6 +284,11 @@ func (c *Connection) RemoveAll(path string) error {
275284
func (c *Connection) Rename(oldname, newname string) error {
276285
c.mutex.Lock()
277286
defer c.mutex.Unlock()
287+
288+
// Normalize path separators to forward slashes for cross-platform compatibility
289+
oldname = filepath.ToSlash(oldname)
290+
newname = filepath.ToSlash(newname)
291+
278292
if oldname == newname {
279293
return nil
280294
}
@@ -291,6 +305,10 @@ func (c *Connection) Rename(oldname, newname string) error {
291305
func (c *Connection) Stat(name string) (os.FileInfo, error) {
292306
c.mutex.Lock()
293307
defer c.mutex.Unlock()
308+
309+
// Normalize path separators to forward slashes for cross-platform compatibility
310+
name = filepath.ToSlash(name)
311+
294312
data, ok := c.data.Files[name]
295313
if !ok {
296314
return nil, os.ErrNotExist
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
//go:build windows
5+
6+
package services
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/sys/windows/svc"
14+
)
15+
16+
func TestGetNativeWindowsServices(t *testing.T) {
17+
services, err := GetNativeWindowsServices()
18+
require.NoError(t, err)
19+
assert.NotEmpty(t, services, "expected at least one service")
20+
21+
// Verify service structure is populated correctly
22+
for _, s := range services {
23+
assert.NotEmpty(t, s.Name, "service name should not be empty")
24+
assert.Equal(t, "windows", s.Type, "service type should be 'windows'")
25+
assert.True(t, s.Installed, "service should be marked as installed")
26+
// State should be one of the known states
27+
assert.Contains(t, []State{
28+
ServiceStopped,
29+
ServiceStartPending,
30+
ServiceStopPending,
31+
ServiceRunning,
32+
ServiceContinuePending,
33+
ServicePausePending,
34+
ServicePaused,
35+
ServiceUnknown,
36+
}, s.State, "service state should be a known state")
37+
}
38+
}
39+
40+
func TestGetNativeWindowsServices_WellKnownServices(t *testing.T) {
41+
services, err := GetNativeWindowsServices()
42+
require.NoError(t, err)
43+
44+
// These services should exist on all Windows systems
45+
wellKnownServices := []string{
46+
"Winmgmt", // Windows Management Instrumentation
47+
"EventLog", // Windows Event Log
48+
"PlugPlay", // Plug and Play
49+
"RpcSs", // Remote Procedure Call (RPC)
50+
"Schedule", // Task Scheduler
51+
"SENS", // System Event Notification Service
52+
"Spooler", // Print Spooler (may be disabled but should exist)
53+
"W32Time", // Windows Time
54+
"Dhcp", // DHCP Client
55+
"Dnscache", // DNS Client
56+
"LanmanServer", // Server
57+
"LanmanWorkstation", // Workstation
58+
}
59+
60+
serviceMap := make(map[string]*Service)
61+
for _, s := range services {
62+
serviceMap[s.Name] = s
63+
}
64+
65+
foundCount := 0
66+
for _, name := range wellKnownServices {
67+
if _, ok := serviceMap[name]; ok {
68+
foundCount++
69+
}
70+
}
71+
72+
// At least half of the well-known services should be present
73+
// (some may be disabled/removed on minimal installations)
74+
assert.GreaterOrEqual(t, foundCount, len(wellKnownServices)/2,
75+
"expected at least %d well-known services, found %d", len(wellKnownServices)/2, foundCount)
76+
}
77+
78+
func TestGetNativeWindowsServices_RunningServices(t *testing.T) {
79+
services, err := GetNativeWindowsServices()
80+
require.NoError(t, err)
81+
82+
// At least some services should be running on any Windows system
83+
runningCount := 0
84+
for _, s := range services {
85+
if s.Running {
86+
runningCount++
87+
// Running services should have Running state
88+
assert.Equal(t, ServiceRunning, s.State,
89+
"service %s is marked running but state is %s", s.Name, s.State)
90+
}
91+
}
92+
93+
assert.Greater(t, runningCount, 0, "expected at least one running service")
94+
}
95+
96+
func TestMapState(t *testing.T) {
97+
tests := []struct {
98+
input svc.State
99+
expected State
100+
}{
101+
{svc.Stopped, ServiceStopped},
102+
{svc.StartPending, ServiceStartPending},
103+
{svc.StopPending, ServiceStopPending},
104+
{svc.Running, ServiceRunning},
105+
{svc.ContinuePending, ServiceContinuePending},
106+
{svc.PausePending, ServicePausePending},
107+
{svc.Paused, ServicePaused},
108+
{svc.State(99), ServiceUnknown}, // Unknown state
109+
}
110+
111+
for _, tc := range tests {
112+
t.Run(string(tc.expected), func(t *testing.T) {
113+
assert.Equal(t, tc.expected, mapState(tc.input))
114+
})
115+
}
116+
}
117+
118+
func TestIsEnabled(t *testing.T) {
119+
tests := []struct {
120+
name string
121+
startType uint32
122+
expected bool
123+
}{
124+
{"Boot", 0, true},
125+
{"System", 1, true},
126+
{"Automatic", 2, true},
127+
{"Manual", 3, true},
128+
{"Disabled", 4, false},
129+
{"Unknown high value", 5, false},
130+
{"Unknown higher value", 100, false},
131+
}
132+
133+
for _, tc := range tests {
134+
t.Run(tc.name, func(t *testing.T) {
135+
assert.Equal(t, tc.expected, isEnabled(tc.startType))
136+
})
137+
}
138+
}
139+
140+
func TestIsRunning(t *testing.T) {
141+
tests := []struct {
142+
state svc.State
143+
expected bool
144+
}{
145+
{svc.Stopped, false},
146+
{svc.StartPending, false},
147+
{svc.StopPending, false},
148+
{svc.Running, true},
149+
{svc.ContinuePending, false},
150+
{svc.PausePending, false},
151+
{svc.Paused, false},
152+
}
153+
154+
for _, tc := range tests {
155+
t.Run(string(mapState(tc.state)), func(t *testing.T) {
156+
assert.Equal(t, tc.expected, isRunning(tc.state))
157+
})
158+
}
159+
}

providers/os/resources/services/systemd_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package services
55

66
import (
7+
"runtime"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
@@ -111,6 +112,9 @@ func TestParseServiceSystemDUnitFilesPhoton(t *testing.T) {
111112
}
112113

113114
func TestSystemdFS(t *testing.T) {
115+
if runtime.GOOS == "windows" {
116+
t.Skip("skipping on Windows: testdata contains symlinks with absolute Unix paths")
117+
}
114118
s := SystemdFSServiceManager{
115119
Fs: mountedfs.NewMountedFs("testdata/systemd"),
116120
}

0 commit comments

Comments
 (0)