From 309d17ae5b2bc1a0492b1bd09e514c1de8cc5af5 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Tue, 17 Dec 2024 15:37:53 +0000 Subject: [PATCH] feat_: status-backend health endpoint (#6201) * feat_: status-backend health endpoint * test_: health check * test_: use health endpoint from python --- cmd/status-backend/README.md | 6 ++++ cmd/status-backend/server/api/health.go | 24 +++++++++++++ cmd/status-backend/server/server.go | 2 ++ tests-functional/README.MD | 19 +++++++++- tests-functional/clients/status_backend.py | 36 +++++++++++-------- .../tests/test_init_status_app.py | 5 ++- 6 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 cmd/status-backend/server/api/health.go diff --git a/cmd/status-backend/README.md b/cmd/status-backend/README.md index bc24c1d62..ec6254b95 100644 --- a/cmd/status-backend/README.md +++ b/cmd/status-backend/README.md @@ -245,6 +245,12 @@ Access the exposed API with any HTTP client you prefer: - [Python](https://pypi.org/project/requests/) - [Go](https://pkg.go.dev/net/http) +## `status-backend` API + +- `/health` +This is a basic health check endpoint. Response contains a single `version` property. +Returns HTTP code 200 if alive. + # 👌 Simple flows In most cases to start testing you'll need some boilerplate. Below are the simple call flows for common cases. diff --git a/cmd/status-backend/server/api/health.go b/cmd/status-backend/server/api/health.go new file mode 100644 index 000000000..3c6cb1c2a --- /dev/null +++ b/cmd/status-backend/server/api/health.go @@ -0,0 +1,24 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/status-im/status-go/internal/version" +) + +type HealthResponse struct { + Version string `json:"version,omitempty"` +} + +func Health(w http.ResponseWriter, r *http.Request) { + response := HealthResponse{ + Version: version.Version(), + } + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/cmd/status-backend/server/server.go b/cmd/status-backend/server/server.go index bfc05f85e..7bd2b7d99 100644 --- a/cmd/status-backend/server/server.go +++ b/cmd/status-backend/server/server.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" + "github.com/status-im/status-go/cmd/status-backend/server/api" "github.com/status-im/status-go/signal" ) @@ -93,6 +94,7 @@ func (s *Server) Listen(address string) error { } s.mux = http.NewServeMux() + s.mux.HandleFunc("/health", api.Health) s.mux.HandleFunc("/signals", s.signals) s.server.Handler = s.mux diff --git a/tests-functional/README.MD b/tests-functional/README.MD index be18550ee..f72c9336f 100644 --- a/tests-functional/README.MD +++ b/tests-functional/README.MD @@ -47,4 +47,21 @@ Functional tests for status-go - Every test has two types of verifications: - `verify_is_valid_json_rpc_response()` checks for status code 200, non-empty response, JSON-RPC structure, presence of the `result` field, and expected ID. - `jsonschema.validate()` is used to check that the response contains expected data, including required fields and types. Schemas are stored in `/schemas/wallet_MethodName` - - New schemas can be generated using `./tests-functional/utils/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method, should be used only on test creation phase, please search `how to create schema:` to see an example in a test \ No newline at end of file + - New schemas can be generated using `./tests-functional/utils/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method, should be used only on test creation phase, please search `how to create schema:` to see an example in a test + +# Known issues + +## Docker permission denied + +When running tests with auto-creating status-backend containers, you might face this: +```shell +sock.connect(self.unix_socket) +PermissionError: [Errno 13] Permission denied +``` + +Please follow this fix: https://github.com/docker/compose/issues/10299#issuecomment-1438247730 + +If you're on MacOS and `/var/run/docker.sock` doesn't exist, you need to create a symlink to the docker socket: +```shell +sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock +``` \ No newline at end of file diff --git a/tests-functional/clients/status_backend.py b/tests-functional/clients/status_backend.py index cc80fc4c7..c28f26daa 100644 --- a/tests-functional/clients/status_backend.py +++ b/tests-functional/clients/status_backend.py @@ -14,6 +14,8 @@ from datetime import datetime from conftest import option from resources.constants import user_1, DEFAULT_DISPLAY_NAME, USER_DIR +NANOSECONDS_PER_SECOND = 1_000_000_000 + class StatusBackend(RpcClient, SignalClient): @@ -29,6 +31,7 @@ class StatusBackend(RpcClient, SignalClient): url = f"http://127.0.0.1:{host_port}" option.status_backend_port_range.remove(host_port) + self.base_url = url self.api_url = f"{url}/statusgo" self.ws_url = f"{url}".replace("http", "ws") self.rpc_url = f"{url}/statusgo/CallRPC" @@ -36,7 +39,7 @@ class StatusBackend(RpcClient, SignalClient): RpcClient.__init__(self, self.rpc_url) SignalClient.__init__(self, self.ws_url, await_signals) - self._health_check() + self.wait_for_healthy() websocket_thread = threading.Thread(target=self._connect) websocket_thread.daemon = True @@ -72,7 +75,6 @@ class StatusBackend(RpcClient, SignalClient): } }, } - if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ: container_args["user"] = os.environ["FUNCTIONAL_TESTS_DOCKER_UID"] @@ -84,23 +86,29 @@ class StatusBackend(RpcClient, SignalClient): option.status_backend_containers.append(container.id) return container - def _health_check(self): + def wait_for_healthy(self, timeout=10): start_time = time.time() - while True: + while time.time() - start_time <= timeout: try: - self.api_valid_request(method="Fleets", data=[]) - break + self.health(enable_logging=False) + logging.info(f"StatusBackend is healthy after {time.time() - start_time} seconds") + return except Exception as e: - if time.time() - start_time > 20: - raise Exception(e) - time.sleep(1) + time.sleep(0.1) + raise TimeoutError( + f"StatusBackend was not healthy after {timeout} seconds") - def api_request(self, method, data, url=None): + def health(self, enable_logging=True): + return self.api_request("health", data=[], url=self.base_url, enable_logging=enable_logging) + + def api_request(self, method, data, url=None, enable_logging=True): url = url if url else self.api_url url = f"{url}/{method}" - logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}") + if enable_logging: + logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}") response = requests.post(url, json=data) - logging.info(f"Got response: {response.content}") + if enable_logging: + logging.info(f"Got response: {response.content}") return response def verify_is_valid_api_response(self, response): @@ -115,8 +123,8 @@ class StatusBackend(RpcClient, SignalClient): except KeyError: pass - def api_valid_request(self, method, data): - response = self.api_request(method, data) + def api_valid_request(self, method, data, url=None): + response = self.api_request(method, data, url) self.verify_is_valid_api_response(response) return response diff --git a/tests-functional/tests/test_init_status_app.py b/tests-functional/tests/test_init_status_app.py index 87d212a52..51aa35876 100644 --- a/tests-functional/tests/test_init_status_app.py +++ b/tests-functional/tests/test_init_status_app.py @@ -23,6 +23,10 @@ class TestInitialiseApp: backend_client.restore_account_and_login() assert backend_client is not None + backend_client.verify_json_schema( + backend_client.wait_for_login(), + "signal_node_login", + ) backend_client.verify_json_schema( backend_client.wait_for_signal(SignalType.MEDIASERVER_STARTED.value), "signal_mediaserver_started", @@ -35,7 +39,6 @@ class TestInitialiseApp: backend_client.wait_for_signal(SignalType.NODE_READY.value), "signal_node_ready", ) - backend_client.verify_json_schema(backend_client.wait_for_login(), "signal_node_login") @pytest.mark.rpc