package nvda2cthulhu import ( "context" "fmt" "net" "net/http" "net/http/httptest" "os" "strings" "sync" "testing" "time" "github.com/gorilla/websocket" ) type recordingHandler struct { mutex sync.Mutex spoken []string interrupt []bool braille []string canceled int } func (handler *recordingHandler) SpeakText(text string, interrupt bool) bool { handler.mutex.Lock() defer handler.mutex.Unlock() handler.spoken = append(handler.spoken, text) handler.interrupt = append(handler.interrupt, interrupt) return true } func (handler *recordingHandler) BrailleMessage(text string) bool { handler.mutex.Lock() defer handler.mutex.Unlock() handler.braille = append(handler.braille, text) return true } func (handler *recordingHandler) CancelSpeech() bool { handler.mutex.Lock() defer handler.mutex.Unlock() handler.canceled++ return true } func TestAllowedOriginAcceptsNativeAndLoopbackClients(t *testing.T) { for _, test := range []struct { origin string want bool }{ {"", true}, {"http://127.0.0.1:3457", true}, {"http://localhost:3457", true}, {"https://[::1]:3457", true}, {"https://example.com", false}, {"null", false}, } { request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:3457/", nil) if test.origin != "" { request.Header.Set("Origin", test.origin) } if got := allowedOrigin(request); got != test.want { t.Errorf("allowedOrigin(%q) = %v, want %v", test.origin, got, test.want) } } } func liveSocketTest(t *testing.T) { t.Helper() handler := &recordingHandler{} server := New(nil, handler) httpTest := httptest.NewServer(http.HandlerFunc(server.handleHTTP)) defer httpTest.Close() url := "ws" + strings.TrimPrefix(httpTest.URL, "http") connection, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { t.Fatalf("dial WebSocket: %v", err) } defer connection.Close() if err := connection.WriteMessage(websocket.TextMessage, []byte("Accessibility mod loaded.")); err != nil { t.Fatalf("write speech text: %v", err) } if err := connection.WriteMessage(websocket.BinaryMessage, []byte{0x81, 0xab, 'B', 'r', 'a', 'i', 'l', 'l', 'e', 'T', 'e', 'x', 't', 0xa2, 'O', 'K'}); err != nil { t.Fatalf("write braille text: %v", err) } if err := connection.WriteMessage(websocket.TextMessage, []byte("CancelSpeech")); err != nil { t.Fatalf("write cancellation: %v", err) } waitForHandlerCalls(t, handler) handler.mutex.Lock() defer handler.mutex.Unlock() if len(handler.spoken) != 1 || handler.spoken[0] != "Accessibility mod loaded." || len(handler.interrupt) != 1 || !handler.interrupt[0] { t.Fatalf("speech calls = %v, interrupts = %v", handler.spoken, handler.interrupt) } if len(handler.braille) != 1 || handler.braille[0] != "OK" { t.Fatalf("braille calls = %v", handler.braille) } if handler.canceled != 1 { t.Fatalf("cancellations = %d, want 1", handler.canceled) } } func TestServerHandlesWebSocketRequests(t *testing.T) { if os.Getenv("CTHULHU_LIVE_SOCKET_TEST") != "1" { t.Skip("set CTHULHU_LIVE_SOCKET_TEST=1 to allow a local TCP listener") } liveSocketTest(t) } func TestStartStopClosesActiveConnection(t *testing.T) { if os.Getenv("CTHULHU_LIVE_SOCKET_TEST") != "1" { t.Skip("set CTHULHU_LIVE_SOCKET_TEST=1 to allow a local TCP listener") } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("find port: %v", err) } port := listener.Addr().(*net.TCPAddr).Port if err := listener.Close(); err != nil { t.Fatalf("release port: %v", err) } t.Setenv(hostEnv, fmt.Sprintf("http://127.0.0.1:%d", port)) server := New(nil, &recordingHandler{}) if err := server.Start(context.Background()); err != nil { t.Fatalf("start server: %v", err) } connection, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://127.0.0.1:%d", port), nil) if err != nil { t.Fatalf("dial server: %v", err) } defer connection.Close() if err := server.Stop(context.Background()); err != nil { t.Fatalf("stop server: %v", err) } if _, _, err := connection.ReadMessage(); err == nil { t.Fatal("active connection remained open after Stop") } } func waitForHandlerCalls(t *testing.T, handler *recordingHandler) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { handler.mutex.Lock() done := len(handler.spoken) == 1 && len(handler.braille) == 1 && handler.canceled == 1 handler.mutex.Unlock() if done { return } time.Sleep(10 * time.Millisecond) } } func TestBindAddressUsesPythonDefault(t *testing.T) { t.Setenv(hostEnv, "http://127.0.0.1") if address := bindAddress(); address != "127.0.0.1:3457" { t.Fatalf("default address = %q", address) } } func TestStopWithoutStartSucceeds(t *testing.T) { server := New(nil, nil) if err := server.Stop(context.Background()); err != nil { t.Fatalf("stop unstarted server: %v", err) } }