Files
2026-09-24 09:52:57 +01:00

324 lines
8.4 KiB
Go

package httpx
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
)
func baseConfig(extra ...string) config.Config {
env := []string{
"ADVERTISE_IP=192.0.2.10",
"MQTT_HOST=mqtt.example.invalid",
}
env = append(env, extra...)
cfg, err := config.Load(env)
if err != nil {
panic(err)
}
return cfg
}
func startServer(t *testing.T, fn func(context.Context, config.Config) error, cfg config.Config) context.CancelFunc {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
_ = fn(ctx, cfg)
}()
t.Cleanup(func() {
cancel()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("server did not stop")
}
})
time.Sleep(50 * time.Millisecond)
return cancel
}
func TestLookupHTTP10WithoutHost(t *testing.T) {
cfg := baseConfig("PORT_LOOKUP=18007")
startServer(t, ServeLookup, cfg)
conn, err := net.Dial("tcp", "127.0.0.1:18007")
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
body := `{"todo":"FindBest","service":"EcoMsgNew"}`
fmt.Fprintf(conn, "POST /lookup.do HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
t.Fatalf("read response: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
if resp.Header.Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("content-type = %q", resp.Header.Get("Content-Type"))
}
b, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
want := `{"result":"ok","ip":"192.0.2.10","port":5223}`
if string(b) != want {
t.Errorf("body = %q, want %q", string(b), want)
}
}
func TestLookupCompactNumericPort(t *testing.T) {
cfg := baseConfig(
"PORT_XMPP=18223",
"PORT_FIRMWARE=18005",
"PORT_LOOKUP=18007",
)
startServer(t, ServeLookup, cfg)
cases := []struct {
service string
want string
}{
{"EcoMsgNew", `{"result":"ok","ip":"192.0.2.10","port":18223}`},
{"EcoUpdate", `{"result":"ok","ip":"192.0.2.10","port":18005}`},
}
for _, tc := range cases {
conn, err := net.Dial("tcp", "127.0.0.1:18007")
if err != nil {
t.Fatalf("dial: %v", err)
}
body := fmt.Sprintf(`{"todo":"FindBest","service":"%s"}`, tc.service)
fmt.Fprintf(conn, "POST /lookup.do HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
conn.Close()
t.Fatalf("read response: %v", err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
conn.Close()
if string(b) != tc.want {
t.Errorf("service %s: body = %q, want %q", tc.service, string(b), tc.want)
}
}
}
func TestParallelLookupIndependentSockets(t *testing.T) {
cfg := baseConfig(
"PORT_XMPP=18223",
"PORT_FIRMWARE=18005",
"PORT_LOOKUP=18007",
)
startServer(t, ServeLookup, cfg)
ch := make(chan string, 2)
dialAndRead := func(service, want string) {
conn, err := net.Dial("tcp", "127.0.0.1:18007")
if err != nil {
ch <- fmt.Sprintf("dial %s: %v", service, err)
return
}
body := fmt.Sprintf(`{"todo":"FindBest","service":"%s"}`, service)
fmt.Fprintf(conn, "POST /lookup.do HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
conn.Close()
ch <- fmt.Sprintf("read %s: %v", service, err)
return
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
conn.Close()
if string(b) != want {
ch <- fmt.Sprintf("%s: got %q want %q", service, string(b), want)
return
}
ch <- ""
}
go dialAndRead("EcoMsgNew", `{"result":"ok","ip":"192.0.2.10","port":18223}`)
go dialAndRead("EcoUpdate", `{"result":"ok","ip":"192.0.2.10","port":18005}`)
for i := 0; i < 2; i++ {
if msg := <-ch; msg != "" {
t.Error(msg)
}
}
}
func TestLookupFailAndRST(t *testing.T) {
cfg := baseConfig("PORT_LOOKUP=18007")
startServer(t, ServeLookup, cfg)
conn, err := net.Dial("tcp", "127.0.0.1:18007")
if err != nil {
t.Fatalf("dial: %v", err)
}
body := `{"todo":"FindBest","service":"UnknownService"}`
fmt.Fprintf(conn, "POST /lookup.do HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
t.Fatalf("read response: %v", err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
conn.Close()
if string(b) != `{"result":"fail"}` {
t.Errorf("unknown service body = %q, want fail", string(b))
}
// Force an RST on a second connection by setting SO_LINGER to zero.
conn2, err := net.Dial("tcp", "127.0.0.1:18007")
if err != nil {
t.Fatalf("dial: %v", err)
}
body = `{"todo":"FindBest","service":"EcoMsgNew"}`
fmt.Fprintf(conn2, "POST /lookup.do HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
// Give the server time to process and close; then RST this side.
if tcp, ok := conn2.(*net.TCPConn); ok {
tcp.SetLinger(0)
}
conn2.Close()
time.Sleep(50 * time.Millisecond)
// The server must still answer a fresh request.
resp, err = http.Post("http://127.0.0.1:18007/lookup.do", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("post after rst: %v", err)
}
b, _ = io.ReadAll(resp.Body)
resp.Body.Close()
want := `{"result":"ok","ip":"192.0.2.10","port":5223}`
if string(b) != want {
t.Errorf("body after rst = %q, want %q", string(b), want)
}
}
func TestFirmware404ExactBytes(t *testing.T) {
cfg := baseConfig(
"PORT_FIRMWARE=18005",
"PORT_LOOKUP=18007",
)
startServer(t, ServeFirmware, cfg)
for _, path := range []string{
"/products/wukong/class/155/firmware/latest.json",
"/other",
} {
resp, err := http.Get("http://127.0.0.1:18005" + path)
if err != nil {
t.Fatalf("get %s: %v", path, err)
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("path %s status = %d, want 404", path, resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" {
t.Errorf("path %s content-type = %q", path, ct)
}
if cl := resp.Header.Get("Content-Length"); cl != "9" {
t.Errorf("path %s content-length = %q, want 9", path, cl)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if string(b) != "Not Found" {
t.Errorf("path %s body = %q, want %q", path, string(b), "Not Found")
}
}
}
func TestAdvertiseIPIsNotBindAddress(t *testing.T) {
cfg := baseConfig(
"BIND_ADDRESS=127.0.0.1",
"ADVERTISE_IP=192.0.2.10",
"PORT_LOOKUP=18007",
)
startServer(t, ServeLookup, cfg)
body := `{"todo":"FindBest","service":"EcoMsgNew"}`
resp, err := http.Post("http://127.0.0.1:18007/lookup.do", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
want := `{"result":"ok","ip":"192.0.2.10","port":5223}`
if string(b) != want {
t.Errorf("body = %q, want %q", string(b), want)
}
}
func TestHealthzSeparatePort(t *testing.T) {
cfg := baseConfig(
"PORT_FIRMWARE=18005",
"PORT_LOOKUP=18007",
"HEALTH_PORT=18080",
)
startServer(t, ServeFirmware, cfg)
startServer(t, ServeLookup, cfg)
startServer(t, ServeHealth, cfg)
resp, err := http.Get("http://127.0.0.1:18080/healthz")
if err != nil {
t.Fatalf("get healthz: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("healthz status = %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" {
t.Errorf("healthz content-type = %q", ct)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if string(b) != "ok\n" {
t.Errorf("healthz body = %q, want ok\\n", string(b))
}
for _, url := range []string{
"http://127.0.0.1:18005/healthz",
"http://127.0.0.1:18007/healthz",
} {
resp, err := http.Get(url)
if err != nil {
t.Fatalf("get %s: %v", url, err)
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("%s status = %d, want 404", url, resp.StatusCode)
}
resp.Body.Close()
}
}
func TestHealthzUnknownPath(t *testing.T) {
cfg := baseConfig("HEALTH_PORT=18080")
startServer(t, ServeHealth, cfg)
resp, err := http.Get("http://127.0.0.1:18080/other")
if err != nil {
t.Fatalf("get: %v", err)
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("status = %d, want 404", resp.StatusCode)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if len(b) != 0 {
t.Errorf("body = %q, want empty", string(b))
}
}