Skip to content

Commit 4292258

Browse files
committed
add unit tests for go server
1 parent 0d85a3d commit 4292258

8 files changed

Lines changed: 501 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Set up Go
2121
uses: actions/setup-go@v5
2222
with:
23-
go-version: '1.21'
23+
go-version-file: go-server/go.mod
2424

2525
- name: Install dependencies
2626
run: npm ci
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Go Unit Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
types:
11+
- opened
12+
- synchronize
13+
- reopened
14+
15+
jobs:
16+
test:
17+
name: Run Go tests
18+
runs-on: ubuntu-latest
19+
20+
defaults:
21+
run:
22+
working-directory: go-server
23+
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version-file: go-server/go.mod
32+
33+
- name: Download dependencies
34+
run: go mod download
35+
36+
- name: Run unit tests
37+
run: go test ./...

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Go
1818
uses: actions/setup-go@v5
1919
with:
20-
go-version: '1.21'
20+
go-version-file: go-server/go.mod
2121

2222
- name: Install dependencies
2323
run: npm ci
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package config
2+
3+
import "testing"
4+
5+
func TestLoadDefaultsForLocalhost(t *testing.T) {
6+
t.Setenv("CCU_HOST", "")
7+
t.Setenv("WS_PORT", "")
8+
t.Setenv("RPC_PORT", "")
9+
t.Setenv("HMIP_PORT", "")
10+
t.Setenv("RPC_SERVER_PORT", "")
11+
t.Setenv("REGA_PORT", "")
12+
t.Setenv("DEBUG", "")
13+
t.Setenv("CALLBACK_HOST", "")
14+
15+
cfg := Load()
16+
17+
if cfg.CCUHost != "localhost" {
18+
t.Fatalf("expected CCUHost localhost, got %q", cfg.CCUHost)
19+
}
20+
if cfg.RegaPort != 8183 {
21+
t.Fatalf("expected RegaPort 8183 for localhost, got %d", cfg.RegaPort)
22+
}
23+
if cfg.WSPort != 8088 || cfg.RPCPort != 2001 || cfg.HmIPPort != 2010 || cfg.RPCServerPort != 9099 {
24+
t.Fatalf("unexpected default ports: ws=%d rpc=%d hmip=%d rpcServer=%d", cfg.WSPort, cfg.RPCPort, cfg.HmIPPort, cfg.RPCServerPort)
25+
}
26+
if cfg.Debug {
27+
t.Fatal("expected Debug=false by default")
28+
}
29+
if cfg.CallbackHost != "127.0.0.1" {
30+
t.Fatalf("expected default CallbackHost 127.0.0.1, got %q", cfg.CallbackHost)
31+
}
32+
}
33+
34+
func TestLoadDefaultsForNonLocalhost(t *testing.T) {
35+
t.Setenv("CCU_HOST", "ccu.local")
36+
t.Setenv("REGA_PORT", "")
37+
38+
cfg := Load()
39+
40+
if cfg.CCUHost != "ccu.local" {
41+
t.Fatalf("expected CCUHost ccu.local, got %q", cfg.CCUHost)
42+
}
43+
if cfg.RegaPort != 8181 {
44+
t.Fatalf("expected RegaPort 8181 for non-localhost, got %d", cfg.RegaPort)
45+
}
46+
}
47+
48+
func TestLoadEnvOverridesAndInvalidIntFallback(t *testing.T) {
49+
t.Setenv("CCU_HOST", "192.168.0.10")
50+
t.Setenv("REGA_PORT", "9000")
51+
t.Setenv("WS_PORT", "not-a-number")
52+
t.Setenv("RPC_PORT", "2200")
53+
t.Setenv("HMIP_PORT", "2210")
54+
t.Setenv("RPC_SERVER_PORT", "9191")
55+
t.Setenv("DEBUG", "true")
56+
t.Setenv("CALLBACK_HOST", "10.0.0.5")
57+
58+
cfg := Load()
59+
60+
if cfg.WSPort != 8088 {
61+
t.Fatalf("expected invalid WS_PORT to fall back to 8088, got %d", cfg.WSPort)
62+
}
63+
if cfg.RPCPort != 2200 || cfg.HmIPPort != 2210 || cfg.RPCServerPort != 9191 {
64+
t.Fatalf("unexpected overridden ports: rpc=%d hmip=%d rpcServer=%d", cfg.RPCPort, cfg.HmIPPort, cfg.RPCServerPort)
65+
}
66+
if cfg.RegaPort != 9000 {
67+
t.Fatalf("expected explicit REGA_PORT 9000, got %d", cfg.RegaPort)
68+
}
69+
if !cfg.Debug {
70+
t.Fatal("expected Debug=true")
71+
}
72+
if cfg.CallbackHost != "10.0.0.5" {
73+
t.Fatalf("expected CallbackHost 10.0.0.5, got %q", cfg.CallbackHost)
74+
}
75+
}

go-server/pkg/rega/rega_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package rega
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
11+
"ccu-addon-mui-server/pkg/config"
12+
)
13+
14+
func TestSanitizeRegaValue(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
input string
18+
want string
19+
}{
20+
{name: "boolean true", input: "true", want: "true"},
21+
{name: "boolean false", input: "false", want: "false"},
22+
{name: "integer", input: "-12", want: "-12"},
23+
{name: "float", input: "12.5", want: "12.5"},
24+
{name: "string quoted", input: "hello", want: "\"hello\""},
25+
{name: "string escaped", input: "a\\b\"c", want: "\"a\\\\b\\\"c\""},
26+
}
27+
28+
for _, tt := range tests {
29+
t.Run(tt.name, func(t *testing.T) {
30+
if got := sanitizeRegaValue(tt.input); got != tt.want {
31+
t.Fatalf("sanitizeRegaValue(%q) = %q, want %q", tt.input, got, tt.want)
32+
}
33+
})
34+
}
35+
}
36+
37+
func TestSetDatapointRejectsInvalidIdentifiers(t *testing.T) {
38+
client := &Client{}
39+
40+
_, err := client.SetDatapoint("HmIP-RF", "abc\";DROP", "STATE", "1")
41+
if err == nil {
42+
t.Fatal("expected error for invalid identifier, got nil")
43+
}
44+
if !strings.Contains(err.Error(), "invalid identifier") {
45+
t.Fatalf("expected invalid identifier error, got %v", err)
46+
}
47+
}
48+
49+
func TestExecuteStripsXMLWrapper(t *testing.T) {
50+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
if r.Method != http.MethodPost {
52+
t.Fatalf("expected POST, got %s", r.Method)
53+
}
54+
w.WriteHeader(http.StatusOK)
55+
_, _ = io.WriteString(w, "payload before xml<xml><anything/></xml>\n")
56+
}))
57+
defer ts.Close()
58+
59+
client := &Client{
60+
cfg: &config.Config{},
61+
httpClient: ts.Client(),
62+
baseURL: ts.URL,
63+
}
64+
65+
got, err := client.Execute("Write(\"x\")")
66+
if err != nil {
67+
t.Fatalf("Execute returned error: %v", err)
68+
}
69+
if got != "payload before xml" {
70+
t.Fatalf("expected wrapper to be removed, got %q", got)
71+
}
72+
}
73+
74+
func TestExecuteUsesBasicAuthWhenConfigured(t *testing.T) {
75+
const user = "alice"
76+
const pass = "secret"
77+
78+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
79+
u, p, ok := r.BasicAuth()
80+
if !ok {
81+
t.Fatal("expected basic auth header")
82+
}
83+
if u != user || p != pass {
84+
t.Fatalf("unexpected credentials user=%q pass=%q", u, p)
85+
}
86+
w.WriteHeader(http.StatusOK)
87+
_, _ = io.WriteString(w, "ok")
88+
}))
89+
defer ts.Close()
90+
91+
client := &Client{
92+
cfg: &config.Config{CCUUser: user, CCUPass: pass},
93+
httpClient: ts.Client(),
94+
baseURL: ts.URL,
95+
}
96+
97+
got, err := client.Execute("Write(\"x\")")
98+
if err != nil {
99+
t.Fatalf("Execute returned error: %v", err)
100+
}
101+
if got != "ok" {
102+
t.Fatalf("expected ok, got %q", got)
103+
}
104+
}
105+
106+
func TestExecuteReturnsStatusError(t *testing.T) {
107+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108+
w.WriteHeader(http.StatusBadGateway)
109+
}))
110+
defer ts.Close()
111+
112+
client := &Client{
113+
cfg: &config.Config{},
114+
httpClient: ts.Client(),
115+
baseURL: ts.URL,
116+
}
117+
118+
_, err := client.Execute("Write(\"x\")")
119+
if err == nil {
120+
t.Fatal("expected status error, got nil")
121+
}
122+
if !strings.Contains(err.Error(), fmt.Sprintf("status %d", http.StatusBadGateway)) {
123+
t.Fatalf("expected status code in error, got %v", err)
124+
}
125+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package subscriptions
2+
3+
import (
4+
"sort"
5+
"testing"
6+
7+
"ccu-addon-mui-server/pkg/types"
8+
)
9+
10+
func TestSubscribeAndShouldReceiveEvent(t *testing.T) {
11+
mgr := NewManager()
12+
mgr.Subscribe("device-1", []string{"A:B:C:1", "A:B:C:2", "A:B:C:2"})
13+
14+
if !mgr.ShouldReceiveEvent("device-1", &types.CCUEvent{Event: types.Event{Channel: "A:B:C:1"}}) {
15+
t.Fatal("expected subscribed channel to receive event")
16+
}
17+
if mgr.ShouldReceiveEvent("device-1", &types.CCUEvent{Event: types.Event{Channel: "A:B:C:9"}}) {
18+
t.Fatal("expected non-subscribed channel not to receive event")
19+
}
20+
if mgr.ShouldReceiveEvent("unknown-device", &types.CCUEvent{Event: types.Event{Channel: "A:B:C:1"}}) {
21+
t.Fatal("expected unknown device not to receive event")
22+
}
23+
24+
channels := mgr.GetSubscriptions("device-1")
25+
sort.Strings(channels)
26+
if len(channels) != 2 {
27+
t.Fatalf("expected duplicate channels to be de-duplicated to 2, got %d", len(channels))
28+
}
29+
if channels[0] != "A:B:C:1" || channels[1] != "A:B:C:2" {
30+
t.Fatalf("unexpected subscriptions: %v", channels)
31+
}
32+
}
33+
34+
func TestUnsubscribeAndStats(t *testing.T) {
35+
mgr := NewManager()
36+
mgr.Subscribe("device-1", []string{"X:1", "X:2"})
37+
mgr.Subscribe("device-2", []string{"Y:1"})
38+
39+
stats := mgr.GetStats()
40+
if stats.Devices != 2 || stats.TotalChannels != 3 {
41+
t.Fatalf("unexpected stats before unsubscribe: %+v", stats)
42+
}
43+
44+
mgr.Unsubscribe("device-1")
45+
46+
stats = mgr.GetStats()
47+
if stats.Devices != 1 || stats.TotalChannels != 1 {
48+
t.Fatalf("unexpected stats after unsubscribe: %+v", stats)
49+
}
50+
51+
if got := mgr.GetSubscriptions("device-1"); len(got) != 0 {
52+
t.Fatalf("expected no subscriptions for unsubscribed device, got %v", got)
53+
}
54+
}

0 commit comments

Comments
 (0)