324 lines
8.4 KiB
Go
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))
|
|
}
|
|
}
|