feat(cmd)_: `status-backend` (#5847)

* feat_: server supports for mobile api

fix(statusd)_: manually serve

* feat_: generate endpoints list from status/mobile.go

* chore_: mark public endpoint OpenAccounts as deprecated

* chore_: added status-backend makefile targets

* feat_: expose deprecated endpoints, with special header field

* docs(status-backend)_: added README
This commit is contained in:
Igor Sirotin 2024-09-27 16:02:18 +01:00 committed by GitHub
parent 42f715f123
commit fc36a7e980
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 723 additions and 26 deletions

View File

@ -170,6 +170,13 @@ spiff-workflow: build/bin/spiff-workflow
status-cli: ##@build Build status-cli to send messages
status-cli: build/bin/status-cli
status-backend: ##@build Build status-backend to run status-go as HTTP server
status-backend: build/bin/status-backend
run-status-backend: PORT ?= 0
run-status-backend: ##@run Start status-backend server listening to localhost:PORT
go run ./cmd/status-backend --address localhost:${PORT}
statusd-prune-docker-image: SHELL := /bin/sh
statusd-prune-docker-image: ##@statusd-prune Build statusd-prune docker image
@echo "Building docker image for ststusd-prune..."

View File

@ -0,0 +1,155 @@
# Description
Welcome to `status-backend`. This is a tool for debugging and testing `status-go`.
In contrast to existing `statusd` and `status-cli`, the `status-backend` exposes full status-go API through HTTP.
This allows to communicate with status-go through HTTP the same way as `status-desktop` and `status-mobile` do, including:
- create account
- restore account
- login
- logout
- start messenger
- start wallet
- subscribe to status-go signals
- etc.
# status-go API
## Public methods in `./mobile/status.go`
Only specific function signatures are currently supported:
- `func(string) string` - 1 argument, 1 return
- `func() string` - 0 argument, 1 return
### Unsupported methods
Attempt to call any other functions will return `501: Not Implemented` HTTP code.
For example, [`VerifyAccountPassword`](https://github.com/status-im/status-go/blob/669256095e16d953ca1af4954b90ca2ae65caa2f/mobile/status.go#L275-L277) has 3 arguments:
```go
func VerifyAccountPassword(keyStoreDir, address, password string) string {
return logAndCallString(verifyAccountPassword, keyStoreDir, address, password)
}
```
Later, as needed, a V2 of these functions will be introduced. V2 will have a single JSON argument composing all args in 1.
For example, https://github.com/status-im/status-go/pull/5865 fixes some of these.
### Deprecated methods
Deprecated methods will have `Deprecation: true` HTTP header.
## Signals in `./signal`
Each signal has [this structure](https://github.com/status-im/status-go/blob/c9b777a2186364b8f394ad65bdb18b128ceffa70/signal/signals.go#L30-L33):
```go
// Envelope is a general signal sent upward from node to RN app
type Envelope struct {
Type string `json:"type"`
Event interface{} `json:"event"`
}
```
List of possible events can be found in `./signal/event_*.go` files.
For example, `node.login` event is defined [here](https://github.com/status-im/status-go/blob/6bcf5f1289f9160168574290cbd6f90dede3f8f6/signal/events_node.go#L27-L28):
```go
const (
// EventLoggedIn is once node was injected with user account and ready to be used.
EventLoggedIn = "node.login"
)
```
And the structure of this event is [defined in the same file](https://github.com/status-im/status-go/blob/6bcf5f1289f9160168574290cbd6f90dede3f8f6/signal/events_node.go#L36-L42):
```go
// NodeLoginEvent returns the result of the login event
type NodeLoginEvent struct {
Error string `json:"error,omitempty"`
Settings *settings.Settings `json:"settings,omitempty"`
Account *multiaccounts.Account `json:"account,omitempty"`
EnsUsernames json.RawMessage `json:"ensUsernames,omitempty"`
}
```
So the signal for `node.login` event will look like this (with corresponding data):
```json
{
"type": "node.login",
"event": {
"error": "",
"settings": {},
"account": {},
"endUsernames": {}
}
}
```
## Services in `./services/**/api.go`
Services are registered in go-ethereum JSON-RPC server. To call such method, send request to `statusgo/CallRPC` endpoint.
For example:
```http request
### Send Contact Request
POST http://localhost:12345/statusgo/CallRPC
{
"jsonrpc": "2.0",
"method": "wakuext_sendContactRequest",
"params": [
{
"id": "0x048f0b885010783429c2298b916e24b3c01f165e55fe8f98fce63df0a55ade80089f512943d4fde5f8c7211f1a87b267a85cbcb3932eb2e4f88aa4ca3918f97541",
"message": "Hi, Alice!"
}
]
}
```
### Notes
1. In this case, there's no limitation to the number of arguments, comparing to `mobile/status.go`, so ll method are supported.
2. Deprecated methods won't have a corresponding `Deprecated: true`
# Usage
Start the app with the address to listen to:
```shell
status-backend --address localhost:12345
```
Or just use the root repo Makefile command:
```shell
make run-status-backend PORT=12345
```
Access the exposed API with any HTTP client you prefer:
- From your IDE:
- [JetBrains](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html)
- [VS Code](https://marketplace.visualstudio.com/items?itemName=humao.rest-client)
- From UI client:
- [Postman](https://learning.postman.com/docs/getting-started/first-steps/sending-the-first-request/)
- [Insomnia](https://docs.insomnia.rest/insomnia/send-your-first-request)
- From command line:
- [Curl](https://curl.se/docs/httpscripting.html)
- From your script:
- [Python](https://pypi.org/project/requests/)
- [Go](https://pkg.go.dev/net/http)
# Simple flows
In most cases to start testing you'll need some boilerplate. Below are the simple call flows for common cases.
## Create account and login
1. `InitializeApplication`
2. `CreateAccountAndLogin`
3. `wakuext_startMessenger`
4. `wallet_startWallet`
5. `settings_getSettings` (temporary workaround, otherwise settings don't get saved into DB)
## Login into account
1. `InitializeApplication`
2. `LoginAccount`
3. `wakuext_startMessenger`
4. `wallet_startWallet`

View File

@ -0,0 +1,48 @@
package main
import (
"flag"
stdlog "log"
"os"
"golang.org/x/crypto/ssh/terminal"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/cmd/statusd/server"
"github.com/status-im/status-go/logutils"
)
var (
address = flag.String("address", "", "host:port to listen")
logger = log.New("package", "status-go/cmd/status-backend")
)
func init() {
logSettings := logutils.LogSettings{
Enabled: true,
MobileSystem: false,
Level: "INFO",
}
colors := terminal.IsTerminal(int(os.Stdin.Fd()))
if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil {
stdlog.Fatalf("failed to initialize log: %v", err)
}
}
func main() {
flag.Parse()
srv := server.NewServer()
srv.Setup()
err := srv.Listen(*address)
if err != nil {
logger.Error("failed to start server", "error", err)
return
}
log.Info("server started", "address", srv.Address())
srv.RegisterMobileAPI()
srv.Serve()
}

View File

@ -178,6 +178,7 @@ func main() {
logger.Error("failed to start server", "error", err)
return
}
go srv.Serve()
log.Info("server started", "address", srv.Address())
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

View File

@ -0,0 +1,120 @@
// Code generated by parse-api/main.go. DO NOT EDIT.
// source: parse-api/main.go
package server
import statusgo "github.com/status-im/status-go/mobile"
var EndpointsWithRequest = map[string]func(string) string{
"/statusgo/InitializeApplication": statusgo.InitializeApplication,
"/statusgo/OpenAccounts": statusgo.OpenAccounts,
"/statusgo/ExtractGroupMembershipSignatures": statusgo.ExtractGroupMembershipSignatures,
"/statusgo/SignGroupMembership": statusgo.SignGroupMembership,
"/statusgo/ValidateNodeConfig": statusgo.ValidateNodeConfig,
"/statusgo/CallRPC": statusgo.CallRPC,
"/statusgo/CallPrivateRPC": statusgo.CallPrivateRPC,
"/statusgo/CreateAccountAndLogin": statusgo.CreateAccountAndLogin,
"/statusgo/LoginAccount": statusgo.LoginAccount,
"/statusgo/RestoreAccountAndLogin": statusgo.RestoreAccountAndLogin,
"/statusgo/InitKeystore": statusgo.InitKeystore,
"/statusgo/SignMessage": statusgo.SignMessage,
"/statusgo/HashTypedData": statusgo.HashTypedData,
"/statusgo/HashTypedDataV4": statusgo.HashTypedDataV4,
"/statusgo/Recover": statusgo.Recover,
"/statusgo/HashTransaction": statusgo.HashTransaction,
"/statusgo/HashMessage": statusgo.HashMessage,
"/statusgo/StartCPUProfile": statusgo.StartCPUProfile,
"/statusgo/WriteHeapProfile": statusgo.WriteHeapProfile,
"/statusgo/AddPeer": statusgo.AddPeer,
"/statusgo/SignHash": statusgo.SignHash,
"/statusgo/GenerateAlias": statusgo.GenerateAlias,
"/statusgo/IsAlias": statusgo.IsAlias,
"/statusgo/Identicon": statusgo.Identicon,
"/statusgo/EmojiHash": statusgo.EmojiHash,
"/statusgo/ColorHash": statusgo.ColorHash,
"/statusgo/ColorID": statusgo.ColorID,
"/statusgo/ValidateMnemonic": statusgo.ValidateMnemonic,
"/statusgo/DecompressPublicKey": statusgo.DecompressPublicKey,
"/statusgo/CompressPublicKey": statusgo.CompressPublicKey,
"/statusgo/SerializeLegacyKey": statusgo.SerializeLegacyKey,
"/statusgo/GetPasswordStrength": statusgo.GetPasswordStrength,
"/statusgo/GetPasswordStrengthScore": statusgo.GetPasswordStrengthScore,
"/statusgo/GetConnectionStringForBeingBootstrapped": statusgo.GetConnectionStringForBeingBootstrapped,
"/statusgo/GetConnectionStringForBootstrappingAnotherDevice": statusgo.GetConnectionStringForBootstrappingAnotherDevice,
"/statusgo/GetConnectionStringForExportingKeypairsKeystores": statusgo.GetConnectionStringForExportingKeypairsKeystores,
"/statusgo/ValidateConnectionString": statusgo.ValidateConnectionString,
"/statusgo/DecodeParameters": statusgo.DecodeParameters,
"/statusgo/HexToNumber": statusgo.HexToNumber,
"/statusgo/NumberToHex": statusgo.NumberToHex,
"/statusgo/Sha3": statusgo.Sha3,
"/statusgo/Utf8ToHex": statusgo.Utf8ToHex,
"/statusgo/HexToUtf8": statusgo.HexToUtf8,
"/statusgo/CheckAddressChecksum": statusgo.CheckAddressChecksum,
"/statusgo/IsAddress": statusgo.IsAddress,
"/statusgo/ToChecksumAddress": statusgo.ToChecksumAddress,
"/statusgo/DeserializeAndCompressKey": statusgo.DeserializeAndCompressKey,
"/statusgo/InitLogging": statusgo.InitLogging,
"/statusgo/ToggleCentralizedMetrics": statusgo.ToggleCentralizedMetrics,
"/statusgo/AddCentralizedMetric": statusgo.AddCentralizedMetric,
}
var EndpointsWithoutRequest = map[string]func() string{
"/statusgo/GetNodeConfig": statusgo.GetNodeConfig,
"/statusgo/ResetChainData": statusgo.ResetChainData,
"/statusgo/Logout": statusgo.Logout,
"/statusgo/StopCPUProfiling": statusgo.StopCPUProfiling,
"/statusgo/StartLocalNotifications": statusgo.StartLocalNotifications,
"/statusgo/StopLocalNotifications": statusgo.StopLocalNotifications,
"/statusgo/ExportNodeLogs": statusgo.ExportNodeLogs,
"/statusgo/ImageServerTLSCert": statusgo.ImageServerTLSCert,
"/statusgo/Fleets": statusgo.Fleets,
"/statusgo/LocalPairingPreflightOutboundCheck": statusgo.LocalPairingPreflightOutboundCheck,
"/statusgo/StartSearchForLocalPairingPeers": statusgo.StartSearchForLocalPairingPeers,
"/statusgo/GetRandomMnemonic": statusgo.GetRandomMnemonic,
"/statusgo/CentralizedMetricsInfo": statusgo.CentralizedMetricsInfo,
}
var EndpointsUnsupported = []string{
"/statusgo/VerifyAccountPassword",
"/statusgo/VerifyDatabasePassword",
"/statusgo/MigrateKeyStoreDir",
"/statusgo/Login",
"/statusgo/LoginWithConfig",
"/statusgo/SaveAccountAndLogin",
"/statusgo/DeleteMultiaccount",
"/statusgo/DeleteImportedKey",
"/statusgo/SaveAccountAndLoginWithKeycard",
"/statusgo/LoginWithKeycard",
"/statusgo/SignTypedData",
"/statusgo/SignTypedDataV4",
"/statusgo/SendTransactionWithChainID",
"/statusgo/SendTransaction",
"/statusgo/SendTransactionWithSignature",
"/statusgo/ConnectionChange",
"/statusgo/AppStateChange",
"/statusgo/SetMobileSignalHandler",
"/statusgo/SetSignalEventCallback",
"/statusgo/MultiformatSerializePublicKey",
"/statusgo/MultiformatDeserializePublicKey",
"/statusgo/ExportUnencryptedDatabase",
"/statusgo/ImportUnencryptedDatabase",
"/statusgo/ChangeDatabasePassword",
"/statusgo/ConvertToKeycardAccount",
"/statusgo/ConvertToRegularAccount",
"/statusgo/SwitchFleet",
"/statusgo/GenerateImages",
"/statusgo/InputConnectionStringForBootstrapping",
"/statusgo/InputConnectionStringForBootstrappingAnotherDevice",
"/statusgo/InputConnectionStringForImportingKeypairsKeystores",
"/statusgo/EncodeTransfer",
"/statusgo/EncodeFunctionCall",
}
var EndpointsDeprecated = map[string]struct{}{
"/statusgo/OpenAccounts": {},
"/statusgo/Login": {},
"/statusgo/LoginWithConfig": {},
"/statusgo/SaveAccountAndLogin": {},
"/statusgo/SaveAccountAndLoginWithKeycard": {},
"/statusgo/LoginWithKeycard": {},
}

View File

@ -0,0 +1,30 @@
// Code generated by parse-api/main.go. DO NOT EDIT.
// source: parse-api/main.go
package server
import statusgo "github.com/status-im/status-go/mobile"
var EndpointsWithRequest = map[string]func(string) string{
{{- range .FunctionsWithResp }}
"/{{ $.PackageName }}/{{ . }}": {{ $.PackageName }}.{{ . }},
{{- end }}
}
var EndpointsWithoutRequest = map[string]func() string{
{{- range .FunctionsNoArgs }}
"/{{ $.PackageName }}/{{ . }}": {{ $.PackageName }}.{{ . }},
{{- end }}
}
var EndpointsUnsupported = []string{
{{- range .UnsupportedEndpoints }}
"/{{ $.PackageName }}/{{ . }}",
{{- end }}
}
var EndpointsDeprecated = map[string]struct{}{
{{- range .DeprecatedEndpoints }}
"/{{ $.PackageName }}/{{ . }}": {},
{{- end }}
}

View File

@ -0,0 +1,167 @@
//go:generate go run main.go
/*
This script generates a Go file with a list of supported endpoints based on the public functions in `mobile/status.go`.
The output has 3 sections:
- Endpoints with a response of type `string`
- Endpoints with no arguments and a response of type `string`
- Unsupported endpoints: those have non-standard signatures
Deprecated functions are ignored.
*/
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
"text/template"
"unicode"
)
const (
inputFilePath = "../../../../mobile/status.go"
templateFilePath = "./endpoints_template.txt"
outputFilePath = "../endpoints.go"
)
var (
// Regular expressions extracted as global variables
publicFuncWithArgsPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\((\w|\s)+\)\s+string\s+\{$`)
publicFuncWithoutArgsPattern = regexp.MustCompile(`^func\s+([A-Z]\w*)\(\)\s+string\s+\{$`)
funcNamePattern = regexp.MustCompile(`^func\s+(\w*)\(`)
deprecatedRegex = regexp.MustCompile(`(?i)//\s*Deprecated`)
)
type TemplateData struct {
PackageName string
FunctionsWithResp []string
FunctionsNoArgs []string
UnsupportedEndpoints []string
DeprecatedEndpoints []string
}
func main() {
// Open the Go source file
file, err := os.Open(inputFilePath)
if err != nil {
fmt.Printf("Failed to open file: %s\n", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var publicFunctionsWithArgs []string
var publicFunctionsWithoutArgs []string
var unsupportedFunctions []string
var deprecatedFucntions []string
var isDeprecated bool
var packageName string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Detect package name
if strings.HasPrefix(line, "package ") {
packageName = strings.TrimPrefix(line, "package ")
}
// Check for deprecation comment
if isDeprecatedComment(line) {
isDeprecated = true
continue
}
functionName := extractFunctionName(line)
if functionName == "" {
continue
}
if !isPublicFunc(functionName) {
isDeprecated = false
continue
}
if isDeprecated {
isDeprecated = false
deprecatedFucntions = append(deprecatedFucntions, functionName)
}
switch {
case isPublicFunctionWithArgs(line):
publicFunctionsWithArgs = append(publicFunctionsWithArgs, functionName)
case isPublicFunctionWithoutArgs(line):
publicFunctionsWithoutArgs = append(publicFunctionsWithoutArgs, functionName)
default:
unsupportedFunctions = append(unsupportedFunctions, functionName)
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading file: %s\n", err)
return
}
// Prepare the template data
data := TemplateData{
PackageName: packageName,
FunctionsWithResp: publicFunctionsWithArgs,
FunctionsNoArgs: publicFunctionsWithoutArgs,
UnsupportedEndpoints: unsupportedFunctions,
DeprecatedEndpoints: deprecatedFucntions,
}
// Load and parse the template
tmpl, err := template.ParseFiles(templateFilePath)
if err != nil {
fmt.Printf("Failed to parse template file: %s\n", err)
return
}
// Create the output file
outputFile, err := os.Create(outputFilePath)
if err != nil {
fmt.Printf("Failed to create output file: %s\n", err)
return
}
defer outputFile.Close()
// Execute the template and write the result to the output file
err = tmpl.Execute(outputFile, data)
if err != nil {
fmt.Printf("Failed to execute template: %s\n", err)
return
}
fmt.Println("Generated endpoints file:", outputFilePath)
}
// Function to check if a line contains a public function with a response of string
func isPublicFunctionWithArgs(line string) bool {
return publicFuncWithArgsPattern.MatchString(line)
}
// Function to check if a line contains a public function with not arguments and a response of string
func isPublicFunctionWithoutArgs(line string) bool {
return publicFuncWithoutArgsPattern.MatchString(line)
}
// Function to extract the public function name from a line
func extractFunctionName(line string) string {
matches := funcNamePattern.FindStringSubmatch(line)
if len(matches) > 1 {
return matches[1]
}
return ""
}
// Function to check if a comment indicates a deprecated function
func isDeprecatedComment(line string) bool {
return deprecatedRegex.MatchString(line)
}
func isPublicFunc(name string) bool {
return name != "" && unicode.IsUpper(rune(name[0]))
}

View File

@ -2,21 +2,26 @@ package server
import (
"context"
"errors"
"io"
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/websocket"
"github.com/ethereum/go-ethereum/log"
"github.com/pkg/errors"
"github.com/status-im/status-go/signal"
)
type Server struct {
server *http.Server
listener net.Listener
mux *http.ServeMux
lock sync.Mutex
connections map[*websocket.Conn]struct{}
address string
@ -32,6 +37,14 @@ func (s *Server) Address() string {
return s.address
}
func (s *Server) Port() (int, error) {
_, portString, err := net.SplitHostPort(s.address)
if err != nil {
return 0, err
}
return strconv.Atoi(portString)
}
func (s *Server) Setup() {
signal.SetMobileSignalHandler(s.signalHandler)
}
@ -53,32 +66,37 @@ func (s *Server) Listen(address string) error {
return errors.New("server already started")
}
_, _, err := net.SplitHostPort(address)
if err != nil {
return errors.Wrap(err, "invalid address")
}
s.server = &http.Server{
Addr: address,
ReadHeaderTimeout: 5 * time.Second,
}
mux := http.NewServeMux()
mux.HandleFunc("/signals", s.signals)
s.server.Handler = mux
s.mux = http.NewServeMux()
s.mux.HandleFunc("/signals", s.signals)
s.server.Handler = s.mux
listener, err := net.Listen("tcp", address)
s.listener, err = net.Listen("tcp", address)
if err != nil {
return err
}
s.address = listener.Addr().String()
go func() {
err := s.server.Serve(listener)
if !errors.Is(err, http.ErrServerClosed) {
log.Error("signals server closed with error: %w", err)
}
}()
s.address = s.listener.Addr().String()
return nil
}
func (s *Server) Serve() {
err := s.server.Serve(s.listener)
if !errors.Is(err, http.ErrServerClosed) {
log.Error("signals server closed with error: %w", err)
}
}
func (s *Server) Stop(ctx context.Context) {
for connection := range s.connections {
err := connection.Close()
@ -115,3 +133,63 @@ func (s *Server) signals(w http.ResponseWriter, r *http.Request) {
s.connections[connection] = struct{}{}
}
func (s *Server) addEndpointWithResponse(name string, handler func(string) string) {
log.Debug("adding endpoint", "name", name)
s.mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
request, err := io.ReadAll(r.Body)
if err != nil {
log.Error("failed to read request: %w", err)
return
}
response := handler(string(request))
s.setHeaders(name, w)
_, err = w.Write([]byte(response))
if err != nil {
log.Error("failed to write response: %w", err)
}
})
}
func (s *Server) addEndpointNoRequest(name string, handler func() string) {
log.Debug("adding endpoint", "name", name)
s.mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
response := handler()
s.setHeaders(name, w)
_, err := w.Write([]byte(response))
if err != nil {
log.Error("failed to write response: %w", err)
}
})
}
func (s *Server) addUnsupportedEndpoint(name string) {
log.Debug("marking unsupported endpoint", "name", name)
s.mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
})
}
func (s *Server) RegisterMobileAPI() {
for name, endpoint := range EndpointsWithRequest {
s.addEndpointWithResponse(name, endpoint)
}
for name, endpoint := range EndpointsWithoutRequest {
s.addEndpointNoRequest(name, endpoint)
}
for _, name := range EndpointsUnsupported {
s.addUnsupportedEndpoint(name)
}
}
func (s *Server) setHeaders(name string, w http.ResponseWriter) {
if _, ok := EndpointsDeprecated[name]; ok {
w.Header().Set("Deprecation", "true")
}
w.Header().Set("Content-Type", "application/json")
}

View File

@ -1,9 +1,12 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"testing"
"time"
@ -16,24 +19,37 @@ import (
"github.com/status-im/status-go/signal"
)
func TestSignalsServer(t *testing.T) {
server := NewServer()
server.Setup()
err := server.Listen("localhost:0")
func setupServer(t *testing.T) (*Server, string) {
srv := NewServer()
srv.Setup()
err := srv.Listen("localhost:0")
require.NoError(t, err)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Stop(ctx)
}()
addr := server.Address()
serverURLString := fmt.Sprintf("ws://%s", addr)
addr := srv.Address()
// Check URL
serverURLString := fmt.Sprintf("http://%s", addr)
serverURL, err := url.Parse(serverURLString)
require.NoError(t, err)
require.NotNil(t, serverURL)
require.NotZero(t, serverURL.Port())
connection, _, err := websocket.DefaultDialer.Dial(serverURLString+"/signals", nil)
return srv, addr
}
func shutdownServer(srv *Server) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Stop(ctx)
}
func TestSignals(t *testing.T) {
srv, serverURLString := setupServer(t)
go srv.Serve()
defer shutdownServer(srv)
signalsURL := fmt.Sprintf("ws://%s/signals", serverURLString)
connection, _, err := websocket.DefaultDialer.Dial(signalsURL, nil)
require.NoError(t, err)
require.NotNil(t, connection)
defer func() {
@ -68,6 +84,80 @@ func TestSignalsServer(t *testing.T) {
require.Equal(t, sentEvent, receivedEvent)
}
func TestMobileAPI(t *testing.T) {
// Setup fake endpoints
endpointsWithResponse := EndpointsWithRequest
endpointsNoRequest := EndpointsWithoutRequest
endpointsUnsupported := EndpointsUnsupported
t.Cleanup(func() {
EndpointsWithRequest = endpointsWithResponse
EndpointsWithoutRequest = endpointsNoRequest
EndpointsUnsupported = endpointsUnsupported
})
endpointWithResponse := "/" + randomAlphabeticalString(t, 5)
endpointNoRequest := "/" + randomAlphabeticalString(t, 5)
endpointUnsupported := "/" + randomAlphabeticalString(t, 5)
request1 := randomAlphabeticalString(t, 5)
response1 := randomAlphabeticalString(t, 5)
response2 := randomAlphabeticalString(t, 5)
EndpointsWithRequest = map[string]func(string) string{
endpointWithResponse: func(request string) string {
require.Equal(t, request1, request)
return response1
},
}
EndpointsWithoutRequest = map[string]func() string{
endpointNoRequest: func() string {
return response2
},
}
EndpointsUnsupported = []string{endpointUnsupported}
// Setup server
srv, _ := setupServer(t)
defer shutdownServer(srv)
go srv.Serve()
srv.RegisterMobileAPI()
requestBody := []byte(request1)
bodyReader := bytes.NewReader(requestBody)
port, err := srv.Port()
require.NoError(t, err)
serverURL := fmt.Sprintf("http://127.0.0.1:%d", port)
// Test endpoints with response
resp, err := http.Post(serverURL+endpointWithResponse, "application/text", bodyReader)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusOK, resp.StatusCode)
responseBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, response1, string(responseBody))
// Test endpoints with no request
resp, err = http.Get(serverURL + endpointNoRequest)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusOK, resp.StatusCode)
responseBody, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, response2, string(responseBody))
// Test unsupported endpoint
resp, err = http.Get(serverURL + endpointUnsupported)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
}
func randomAlphabeticalString(t *testing.T, n int) string {
s, err := common.RandomAlphabeticalString(n)
require.NoError(t, err)

View File

@ -99,6 +99,7 @@ func initializeApplication(requestJSON string) string {
return string(data)
}
// Deprecated: Use InitializeApplication instead.
func OpenAccounts(datadir string) string {
return callWithResponse(openAccounts, datadir)
}