urirun-service-android-node 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- urirun_service_android_node-0.1.0/PKG-INFO +135 -0
- urirun_service_android_node-0.1.0/README.md +120 -0
- urirun_service_android_node-0.1.0/pyproject.toml +42 -0
- urirun_service_android_node-0.1.0/setup.cfg +4 -0
- urirun_service_android_node-0.1.0/tests/test_android_node_service.py +355 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node/__init__.py +3 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node/core.py +1010 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node/page.py +485 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node/service.manifest.json +40 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/PKG-INFO +135 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/SOURCES.txt +13 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/dependency_links.txt +1 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/entry_points.txt +6 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/requires.txt +5 -0
- urirun_service_android_node-0.1.0/urirun_service_android_node.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urirun-service-android-node
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Android setup and webpage-node relay service for urirun on port 8195
|
|
5
|
+
Author: if-uri
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://connect.ifuri.com/services/android-node
|
|
8
|
+
Project-URL: Repository, https://github.com/if-uri/urirun-service-android-node
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: urirun[host]>=0.4.88
|
|
12
|
+
Requires-Dist: qrcode[pil]>=7
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
15
|
+
|
|
16
|
+
# urirun-service-android-node
|
|
17
|
+
|
|
18
|
+
Service on port `8195` for connecting Android devices and browser pages to a urirun host.
|
|
19
|
+
|
|
20
|
+
Two separate roles:
|
|
21
|
+
|
|
22
|
+
- **Android setup** — QR page, ADB instructions, Termux bootstrap, APK download.
|
|
23
|
+
- **Webpage node relay** — any browser page that opens the service URL registers as a `webpage` node and exposes `/health`, `/routes`, `/run` through the relay.
|
|
24
|
+
|
|
25
|
+
## Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
urirun-android-node serve --host 0.0.0.0 --port 8195
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open the printed URL on a phone or another browser. To replace an old process on the same port:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
urirun-android-node restart --host 0.0.0.0 --port 8195 --force-replace
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## APK download
|
|
38
|
+
|
|
39
|
+
The setup page (Path C) shows available APKs with a QR code for direct phone download.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
GET /apk/ — JSON list of available APKs
|
|
43
|
+
GET /apk/<file> — download APK
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Current APK:
|
|
47
|
+
|
|
48
|
+
| File | Arch | Android | Size |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `urirunnode-0.3.0-arm64-v8a-debug.apk` | arm64-v8a (64-bit) | 12+ (API 31) | 18 MB |
|
|
51
|
+
|
|
52
|
+
The service checks these directories (first match wins per filename):
|
|
53
|
+
|
|
54
|
+
1. `URIRUN_ANDROID_NODE_APK_DIRS` or `URIRUN_ANDROID_NODE_APK_DIR` (env override)
|
|
55
|
+
2. `urirun-service-android-node/apk/` ← stable distribution location
|
|
56
|
+
3. `urirun-android-node-app/bin/` ← Buildozer output
|
|
57
|
+
4. `~/.urirun/android-node/apk/`
|
|
58
|
+
|
|
59
|
+
To build and publish a new APK:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd /home/tom/github/if-uri/urirun-android-node-app
|
|
63
|
+
URIRUN_ANDROID_BUILD_SKIP_IMAGE=1 make docker-apk
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`docker-apk` copies the result to `urirun-service-android-node/apk/` automatically.
|
|
67
|
+
The page reloads the list dynamically — no service restart needed.
|
|
68
|
+
|
|
69
|
+
## Browser plugins
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
GET /plugins — JSON list of available plugins
|
|
73
|
+
GET /plugins/chrome.zip — Chrome extension zip
|
|
74
|
+
GET /plugins/firefox.zip — Firefox extension zip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Plugins are served when their source directories exist next to this repo:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd /home/tom/github/if-uri/chrome-plugin && make package
|
|
81
|
+
# Chrome: chrome://extensions → Developer mode → Load unpacked
|
|
82
|
+
|
|
83
|
+
cd /home/tom/github/if-uri/firefox-plugin && make package
|
|
84
|
+
# Firefox: about:debugging → Load Temporary Add-on → manifest.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Webpage node relay
|
|
88
|
+
|
|
89
|
+
Opening the setup page on a phone/browser auto-registers it as a `webpage` node.
|
|
90
|
+
The host can reach it immediately through the relay endpoint.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
GET /api/webpage-node/list
|
|
94
|
+
GET /api/webpage-node/relay/<id>/health
|
|
95
|
+
GET /api/webpage-node/relay/<id>/routes
|
|
96
|
+
POST /api/webpage-node/relay/<id>/run
|
|
97
|
+
POST /api/webpage-node/register
|
|
98
|
+
POST /api/webpage-node/forget
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Routes exposed per device (replace `{id}` with the assigned device id):
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
webpage://{id}/page/query/info
|
|
105
|
+
webpage://{id}/page/query/text
|
|
106
|
+
webpage://{id}/page/query/devices
|
|
107
|
+
webpage://{id}/page/command/navigate
|
|
108
|
+
webpage://{id}/page/command/eval
|
|
109
|
+
webpage://{id}/camera/query/status
|
|
110
|
+
webpage://{id}/camera/command/start
|
|
111
|
+
webpage://{id}/sensor/query/capabilities
|
|
112
|
+
webpage://{id}/user/query/activity
|
|
113
|
+
webpage://{id}/iframe/command/open
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Legacy `/api/web-node/...` endpoints are accepted as aliases.
|
|
117
|
+
|
|
118
|
+
## All endpoints
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
GET / setup page (HTML)
|
|
122
|
+
GET /qr.png QR code for this page (or ?url=... for arbitrary URL)
|
|
123
|
+
GET /bootstrap.sh Termux bootstrap script
|
|
124
|
+
GET /apk/ APK list (JSON)
|
|
125
|
+
GET /apk/<file> APK download
|
|
126
|
+
GET /plugins browser plugin list (JSON)
|
|
127
|
+
GET /plugins/chrome.zip Chrome extension
|
|
128
|
+
GET /plugins/firefox.zip Firefox extension
|
|
129
|
+
GET /api/status ADB devices + service info
|
|
130
|
+
POST /api/connect register an Android node by IP:port
|
|
131
|
+
GET /api/webpage-node/list list connected webpage nodes
|
|
132
|
+
GET /api/webpage-node/relay/<id>/health
|
|
133
|
+
GET /api/webpage-node/relay/<id>/routes
|
|
134
|
+
POST /api/webpage-node/relay/<id>/run
|
|
135
|
+
```
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# urirun-service-android-node
|
|
2
|
+
|
|
3
|
+
Service on port `8195` for connecting Android devices and browser pages to a urirun host.
|
|
4
|
+
|
|
5
|
+
Two separate roles:
|
|
6
|
+
|
|
7
|
+
- **Android setup** — QR page, ADB instructions, Termux bootstrap, APK download.
|
|
8
|
+
- **Webpage node relay** — any browser page that opens the service URL registers as a `webpage` node and exposes `/health`, `/routes`, `/run` through the relay.
|
|
9
|
+
|
|
10
|
+
## Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
urirun-android-node serve --host 0.0.0.0 --port 8195
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open the printed URL on a phone or another browser. To replace an old process on the same port:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
urirun-android-node restart --host 0.0.0.0 --port 8195 --force-replace
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## APK download
|
|
23
|
+
|
|
24
|
+
The setup page (Path C) shows available APKs with a QR code for direct phone download.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
GET /apk/ — JSON list of available APKs
|
|
28
|
+
GET /apk/<file> — download APK
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Current APK:
|
|
32
|
+
|
|
33
|
+
| File | Arch | Android | Size |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `urirunnode-0.3.0-arm64-v8a-debug.apk` | arm64-v8a (64-bit) | 12+ (API 31) | 18 MB |
|
|
36
|
+
|
|
37
|
+
The service checks these directories (first match wins per filename):
|
|
38
|
+
|
|
39
|
+
1. `URIRUN_ANDROID_NODE_APK_DIRS` or `URIRUN_ANDROID_NODE_APK_DIR` (env override)
|
|
40
|
+
2. `urirun-service-android-node/apk/` ← stable distribution location
|
|
41
|
+
3. `urirun-android-node-app/bin/` ← Buildozer output
|
|
42
|
+
4. `~/.urirun/android-node/apk/`
|
|
43
|
+
|
|
44
|
+
To build and publish a new APK:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd /home/tom/github/if-uri/urirun-android-node-app
|
|
48
|
+
URIRUN_ANDROID_BUILD_SKIP_IMAGE=1 make docker-apk
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`docker-apk` copies the result to `urirun-service-android-node/apk/` automatically.
|
|
52
|
+
The page reloads the list dynamically — no service restart needed.
|
|
53
|
+
|
|
54
|
+
## Browser plugins
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
GET /plugins — JSON list of available plugins
|
|
58
|
+
GET /plugins/chrome.zip — Chrome extension zip
|
|
59
|
+
GET /plugins/firefox.zip — Firefox extension zip
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Plugins are served when their source directories exist next to this repo:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd /home/tom/github/if-uri/chrome-plugin && make package
|
|
66
|
+
# Chrome: chrome://extensions → Developer mode → Load unpacked
|
|
67
|
+
|
|
68
|
+
cd /home/tom/github/if-uri/firefox-plugin && make package
|
|
69
|
+
# Firefox: about:debugging → Load Temporary Add-on → manifest.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Webpage node relay
|
|
73
|
+
|
|
74
|
+
Opening the setup page on a phone/browser auto-registers it as a `webpage` node.
|
|
75
|
+
The host can reach it immediately through the relay endpoint.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
GET /api/webpage-node/list
|
|
79
|
+
GET /api/webpage-node/relay/<id>/health
|
|
80
|
+
GET /api/webpage-node/relay/<id>/routes
|
|
81
|
+
POST /api/webpage-node/relay/<id>/run
|
|
82
|
+
POST /api/webpage-node/register
|
|
83
|
+
POST /api/webpage-node/forget
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Routes exposed per device (replace `{id}` with the assigned device id):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
webpage://{id}/page/query/info
|
|
90
|
+
webpage://{id}/page/query/text
|
|
91
|
+
webpage://{id}/page/query/devices
|
|
92
|
+
webpage://{id}/page/command/navigate
|
|
93
|
+
webpage://{id}/page/command/eval
|
|
94
|
+
webpage://{id}/camera/query/status
|
|
95
|
+
webpage://{id}/camera/command/start
|
|
96
|
+
webpage://{id}/sensor/query/capabilities
|
|
97
|
+
webpage://{id}/user/query/activity
|
|
98
|
+
webpage://{id}/iframe/command/open
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Legacy `/api/web-node/...` endpoints are accepted as aliases.
|
|
102
|
+
|
|
103
|
+
## All endpoints
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
GET / setup page (HTML)
|
|
107
|
+
GET /qr.png QR code for this page (or ?url=... for arbitrary URL)
|
|
108
|
+
GET /bootstrap.sh Termux bootstrap script
|
|
109
|
+
GET /apk/ APK list (JSON)
|
|
110
|
+
GET /apk/<file> APK download
|
|
111
|
+
GET /plugins browser plugin list (JSON)
|
|
112
|
+
GET /plugins/chrome.zip Chrome extension
|
|
113
|
+
GET /plugins/firefox.zip Firefox extension
|
|
114
|
+
GET /api/status ADB devices + service info
|
|
115
|
+
POST /api/connect register an Android node by IP:port
|
|
116
|
+
GET /api/webpage-node/list list connected webpage nodes
|
|
117
|
+
GET /api/webpage-node/relay/<id>/health
|
|
118
|
+
GET /api/webpage-node/relay/<id>/routes
|
|
119
|
+
POST /api/webpage-node/relay/<id>/run
|
|
120
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "urirun-service-android-node"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Android setup and webpage-node relay service for urirun on port 8195"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "if-uri" }]
|
|
13
|
+
dependencies = ["urirun[host]>=0.4.88", "qrcode[pil]>=7"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = ["pytest>=8"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://connect.ifuri.com/services/android-node"
|
|
20
|
+
Repository = "https://github.com/if-uri/urirun-service-android-node"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
urirun-service-android-node = "urirun_service_android_node.core:main"
|
|
24
|
+
urirun-android-node = "urirun_service_android_node.core:main"
|
|
25
|
+
|
|
26
|
+
[project.entry-points."urirun.services"]
|
|
27
|
+
android-node = "urirun_service_android_node.core:urirun_service"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
include = ["urirun_service_android_node*"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
urirun_service_android_node = ["service.manifest.json", "templates/*.html"]
|
|
35
|
+
|
|
36
|
+
[tool.wellmanifest]
|
|
37
|
+
standard = "0.20.35"
|
|
38
|
+
revision = "cfaa0bf0ea6b0e7349fed0bb62b5ce15792d687d"
|
|
39
|
+
gate = "project/governance-check.sh"
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
addopts = "-p wellmanifest_governance"
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Author: Tom Sapletta · https://tom.sapletta.com
|
|
2
|
+
# Part of the ifURI solution.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import pathlib
|
|
9
|
+
import threading
|
|
10
|
+
from http.server import ThreadingHTTPServer
|
|
11
|
+
from urllib.request import urlopen
|
|
12
|
+
from urllib.error import URLError
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
import urirun_service_android_node.core as core
|
|
17
|
+
from urirun_service_android_node import DEFAULT_PORT, SERVICE_ID, service_manifest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_service_manifest_declares_port() -> None:
|
|
21
|
+
m = service_manifest()
|
|
22
|
+
assert m["id"] == SERVICE_ID
|
|
23
|
+
assert m["id"] == "android-node"
|
|
24
|
+
assert m["defaultPort"] == 8195
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_service_manifest_has_required_fields() -> None:
|
|
28
|
+
m = service_manifest()
|
|
29
|
+
assert m["kind"] == "service"
|
|
30
|
+
assert "/api/status" in m["http"]["api"]
|
|
31
|
+
assert "/api/host-services" in m["http"]["api"]
|
|
32
|
+
assert "/bootstrap.sh" in m["http"]["api"]
|
|
33
|
+
assert "/apk/" in m["http"]["api"]
|
|
34
|
+
assert "/plugins/chrome.zip" in m["http"]["api"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_lan_ip_returns_string() -> None:
|
|
38
|
+
ip = core._lan_ip()
|
|
39
|
+
assert isinstance(ip, str)
|
|
40
|
+
assert "." in ip # IPv4 dotted notation
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_adb_devices_no_adb(monkeypatch) -> None:
|
|
44
|
+
monkeypatch.setattr(core.shutil, "which", lambda _: None)
|
|
45
|
+
devices = core._adb_devices()
|
|
46
|
+
assert devices == []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_qr_bytes_writes_and_reads(monkeypatch, tmp_path) -> None:
|
|
50
|
+
written: list = []
|
|
51
|
+
|
|
52
|
+
def fake_write(url, path):
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
path.write_bytes(b"PNG_FAKE")
|
|
55
|
+
written.append(url)
|
|
56
|
+
|
|
57
|
+
monkeypatch.setattr(core, "_write_qr_png", fake_write)
|
|
58
|
+
monkeypatch.setattr(core, "_QR_CACHE", tmp_path / "qr.png")
|
|
59
|
+
result = core._qr_bytes("http://192.168.1.1:8195/")
|
|
60
|
+
assert result == b"PNG_FAKE"
|
|
61
|
+
assert written == ["http://192.168.1.1:8195/"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _start_test_server(tmp_path) -> tuple[ThreadingHTTPServer, int, str]:
|
|
65
|
+
"""Start the service on a free port, return (server, port, base_url)."""
|
|
66
|
+
# Write a fake bootstrap script
|
|
67
|
+
bs = tmp_path / "bootstrap.sh"
|
|
68
|
+
bs.write_text("#!/bin/bash\necho hello\n")
|
|
69
|
+
server = core.serve(host="127.0.0.1", port=0,
|
|
70
|
+
bootstrap_script=str(bs))
|
|
71
|
+
port = server.server_address[1]
|
|
72
|
+
t = threading.Thread(target=server.serve_forever, daemon=True)
|
|
73
|
+
t.start()
|
|
74
|
+
return server, port, f"http://127.0.0.1:{port}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_api_status_endpoint(monkeypatch, tmp_path) -> None:
|
|
78
|
+
monkeypatch.setattr(core.shutil, "which", lambda _: None) # no adb
|
|
79
|
+
server, port, base = _start_test_server(tmp_path)
|
|
80
|
+
try:
|
|
81
|
+
with urlopen(f"{base}/api/status") as resp:
|
|
82
|
+
data = json.loads(resp.read())
|
|
83
|
+
assert data["ok"] is True
|
|
84
|
+
assert data["adb"] is False
|
|
85
|
+
assert isinstance(data["devices"], list)
|
|
86
|
+
finally:
|
|
87
|
+
server.shutdown()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_bootstrap_sh_endpoint(tmp_path) -> None:
|
|
91
|
+
server, port, base = _start_test_server(tmp_path)
|
|
92
|
+
try:
|
|
93
|
+
with urlopen(f"{base}/bootstrap.sh") as resp:
|
|
94
|
+
body = resp.read().decode()
|
|
95
|
+
assert "hello" in body
|
|
96
|
+
finally:
|
|
97
|
+
server.shutdown()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_qr_png_endpoint(monkeypatch, tmp_path) -> None:
|
|
101
|
+
monkeypatch.setattr(core, "_QR_CACHE", tmp_path / "qr.png")
|
|
102
|
+
|
|
103
|
+
def fake_write(url, path):
|
|
104
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
path.write_bytes(b"\x89PNG_FAKE_DATA")
|
|
106
|
+
|
|
107
|
+
monkeypatch.setattr(core, "_write_qr_png", fake_write)
|
|
108
|
+
server, port, base = _start_test_server(tmp_path)
|
|
109
|
+
try:
|
|
110
|
+
with urlopen(f"{base}/qr.png") as resp:
|
|
111
|
+
assert resp.headers["Content-Type"] == "image/png"
|
|
112
|
+
body = resp.read()
|
|
113
|
+
assert body == b"\x89PNG_FAKE_DATA"
|
|
114
|
+
finally:
|
|
115
|
+
server.shutdown()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_root_page_html(monkeypatch, tmp_path) -> None:
|
|
119
|
+
server, port, base = _start_test_server(tmp_path)
|
|
120
|
+
try:
|
|
121
|
+
with urlopen(f"{base}/") as resp:
|
|
122
|
+
body = resp.read().decode()
|
|
123
|
+
assert "urirun Android Node" in body
|
|
124
|
+
assert "bootstrap.sh" in body
|
|
125
|
+
assert "Path C — Install APK" in body
|
|
126
|
+
assert "Path E — Host services" in body
|
|
127
|
+
assert "/api/host-services" in body
|
|
128
|
+
finally:
|
|
129
|
+
server.shutdown()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_host_services_default_to_same_lan_host_and_dashboard_port(monkeypatch) -> None:
|
|
133
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PUBLIC_URL", raising=False)
|
|
134
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_BASE", raising=False)
|
|
135
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PUBLIC_HOST", raising=False)
|
|
136
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PORT", raising=False)
|
|
137
|
+
monkeypatch.setattr(core, "_host_phone_scanner_services", lambda _base: [])
|
|
138
|
+
data = core.host_services("http://192.168.188.212:8195/")
|
|
139
|
+
assert data["dashboardBase"] == "http://192.168.188.212:8194"
|
|
140
|
+
scanner = data["services"][0]
|
|
141
|
+
assert scanner["id"] == "service:phone-scanner"
|
|
142
|
+
assert scanner["target"] == "service:phone-scanner"
|
|
143
|
+
assert scanner["url"].startswith("http://192.168.188.212:8194/scanner?")
|
|
144
|
+
assert "autostart=1" in scanner["url"]
|
|
145
|
+
assert "minScore=45" in scanner["url"]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_host_services_honor_dashboard_public_url(monkeypatch) -> None:
|
|
149
|
+
monkeypatch.setenv("URIRUN_DASHBOARD_PUBLIC_URL", "https://dash.example.test:9443/base/")
|
|
150
|
+
monkeypatch.setattr(core, "_host_phone_scanner_services", lambda _base: [])
|
|
151
|
+
data = core.host_services("http://192.168.188.212:8195/")
|
|
152
|
+
assert data["dashboardBase"] == "https://dash.example.test:9443/base"
|
|
153
|
+
assert data["services"][0]["url"].startswith("https://dash.example.test:9443/base/scanner?")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_host_services_prefers_host_dashboard_summary(monkeypatch) -> None:
|
|
157
|
+
monkeypatch.setenv("URIRUN_DASHBOARD_PUBLIC_URL", "http://dash.example.test:8194")
|
|
158
|
+
|
|
159
|
+
def fake_summary(base, path, timeout=1.5):
|
|
160
|
+
assert base == "http://dash.example.test:8194"
|
|
161
|
+
assert path == "/api/summary"
|
|
162
|
+
return {"services": [
|
|
163
|
+
{"id": "service:phone-scanner", "url": "https://scanner.example.test:9443/scanner?count=2",
|
|
164
|
+
"label": "Host registry scanner", "routes": ["scanner://page/camera/command/autonomous"]},
|
|
165
|
+
]}
|
|
166
|
+
|
|
167
|
+
monkeypatch.setattr(core, "_host_dashboard_json", fake_summary)
|
|
168
|
+
data = core.host_services("http://192.168.188.212:8195/")
|
|
169
|
+
scanner = data["services"][0]
|
|
170
|
+
assert scanner["url"] == "https://scanner.example.test:9443/scanner?count=2"
|
|
171
|
+
assert scanner["label"] == "Host registry scanner"
|
|
172
|
+
assert scanner["source"] == "host-dashboard-summary"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_host_services_endpoint(monkeypatch, tmp_path) -> None:
|
|
176
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PUBLIC_URL", raising=False)
|
|
177
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_BASE", raising=False)
|
|
178
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PUBLIC_HOST", raising=False)
|
|
179
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_SCHEME", raising=False)
|
|
180
|
+
monkeypatch.delenv("URIRUN_DASHBOARD_PORT", raising=False)
|
|
181
|
+
monkeypatch.setattr(core, "_host_phone_scanner_services", lambda _base: [])
|
|
182
|
+
core._WEB_NODES.clear()
|
|
183
|
+
server, port, base = _start_test_server(tmp_path)
|
|
184
|
+
try:
|
|
185
|
+
with urlopen(f"{base}/api/host-services") as resp:
|
|
186
|
+
data = json.loads(resp.read())
|
|
187
|
+
assert data["ok"] is True
|
|
188
|
+
assert data["services"][0]["id"] == "service:phone-scanner"
|
|
189
|
+
assert f"http://127.0.0.1:8194/scanner?" in data["services"][0]["url"]
|
|
190
|
+
finally:
|
|
191
|
+
server.shutdown()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_apk_list_reports_dirs_when_empty(monkeypatch, tmp_path) -> None:
|
|
195
|
+
monkeypatch.setattr(core, "_apk_dirs", lambda: [tmp_path / "apk"])
|
|
196
|
+
assert core._list_apks() == []
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_apk_endpoint_serves_found_apk(monkeypatch, tmp_path) -> None:
|
|
200
|
+
apk_dir = tmp_path / "bin"
|
|
201
|
+
apk_dir.mkdir()
|
|
202
|
+
apk = apk_dir / "urirun test.apk"
|
|
203
|
+
apk.write_bytes(b"APK")
|
|
204
|
+
monkeypatch.setattr(core, "_apk_dirs", lambda: [apk_dir])
|
|
205
|
+
server, port, base = _start_test_server(tmp_path)
|
|
206
|
+
try:
|
|
207
|
+
with urlopen(f"{base}/apk/") as resp:
|
|
208
|
+
listing = json.loads(resp.read())
|
|
209
|
+
assert listing["count"] == 1
|
|
210
|
+
assert listing["apks"][0]["name"] == "urirun test.apk"
|
|
211
|
+
assert listing["apks"][0]["url"] == "/apk/urirun%20test.apk"
|
|
212
|
+
|
|
213
|
+
with urlopen(f"{base}/apk/urirun%20test.apk") as resp:
|
|
214
|
+
assert resp.headers["Content-Type"] == "application/vnd.android.package-archive"
|
|
215
|
+
assert resp.read() == b"APK"
|
|
216
|
+
finally:
|
|
217
|
+
server.shutdown()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_main_manifest_prints_json(capsys) -> None:
|
|
221
|
+
from urirun_service_android_node.core import main
|
|
222
|
+
rc = main(["manifest"])
|
|
223
|
+
assert rc == 0
|
|
224
|
+
out = capsys.readouterr().out
|
|
225
|
+
data = json.loads(out)
|
|
226
|
+
assert data["id"] == "android-node"
|
|
227
|
+
assert data["defaultPort"] == 8195
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_web_node_register_and_list() -> None:
|
|
231
|
+
core._WEB_NODES.clear()
|
|
232
|
+
r = core.web_node_register(
|
|
233
|
+
{"userAgent": "Mozilla/5.0 (Linux; Android 8.1; Nexus 7)"},
|
|
234
|
+
service_url="http://192.168.1.1:8195/",
|
|
235
|
+
client_ip="192.168.1.55")
|
|
236
|
+
assert r["ok"] is True
|
|
237
|
+
assert r["platform"] == "android"
|
|
238
|
+
assert r["nodeUrl"].endswith(f"/api/webpage-node/relay/{r['id']}")
|
|
239
|
+
assert r["relayUrl"] == r["nodeUrl"]
|
|
240
|
+
assert r["clientIp"] == "192.168.1.55"
|
|
241
|
+
assert r["clientUrl"] == "http://192.168.1.55"
|
|
242
|
+
assert f"webpage://{r['id']}/page/query/info" in r["routes"]
|
|
243
|
+
listing = core.web_node_list()
|
|
244
|
+
assert listing["count"] == 1
|
|
245
|
+
assert listing["devices"][0]["online"] is True
|
|
246
|
+
assert listing["devices"][0]["clientIp"] == "192.168.1.55"
|
|
247
|
+
assert listing["devices"][0]["relayUrl"].endswith(f"/api/webpage-node/relay/{r['id']}")
|
|
248
|
+
assert listing["devices"][0]["capabilities"] == ["webpage", "dom", "camera", "sensor", "iframe", "user-activity"]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_web_node_action_queue_roundtrip() -> None:
|
|
252
|
+
core._WEB_NODES.clear()
|
|
253
|
+
dev = core.web_node_register({"userAgent": "Chrome"}, service_url="http://h:8195/")["id"]
|
|
254
|
+
core.web_node_enqueue(dev, {"id": "a1", "type": "navigate", "url": "https://example.com"})
|
|
255
|
+
polled = core.web_node_poll(dev)
|
|
256
|
+
assert polled["actions"] == [{"id": "a1", "type": "navigate", "url": "https://example.com"}]
|
|
257
|
+
# queue drains after poll
|
|
258
|
+
assert core.web_node_poll(dev)["actions"] == []
|
|
259
|
+
# result is accepted
|
|
260
|
+
assert core.web_node_result(dev, {"id": dev, "actionId": "a1", "result": {"ok": 1}})["ok"] is True
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_web_node_poll_unknown_device() -> None:
|
|
264
|
+
core._WEB_NODES.clear()
|
|
265
|
+
assert core.web_node_poll("nope")["ok"] is False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_page_embeds_web_node_client() -> None:
|
|
269
|
+
from urirun_service_android_node.page import SETUP_HTML
|
|
270
|
+
assert "/api/webpage-node/register" in SETUP_HTML
|
|
271
|
+
assert "urirun-webnode-id" in SETUP_HTML
|
|
272
|
+
assert "webpage-route-list" in SETUP_HTML
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_webpage_relay_health_routes_and_timeout() -> None:
|
|
276
|
+
core._WEB_NODES.clear()
|
|
277
|
+
dev = core.web_node_register({"userAgent": "Chrome"}, service_url="http://h:8195/")["id"]
|
|
278
|
+
|
|
279
|
+
health = core.web_node_health(dev, service_url="http://h:8195/")
|
|
280
|
+
assert health["ok"] is True
|
|
281
|
+
assert health["nodeType"] == "webpage"
|
|
282
|
+
assert health["routeCount"] == len(core.WEBPAGE_ROUTES)
|
|
283
|
+
|
|
284
|
+
routes = core.web_node_routes(dev)
|
|
285
|
+
assert routes["ok"] is True
|
|
286
|
+
assert f"webpage://{dev}/camera/command/start" in {route["uri"] for route in routes["routes"]}
|
|
287
|
+
|
|
288
|
+
result = core.web_node_run(dev, {
|
|
289
|
+
"uri": f"webpage://{dev}/page/query/info",
|
|
290
|
+
"payload": {},
|
|
291
|
+
}, timeout=0.01)
|
|
292
|
+
assert result["ok"] is False
|
|
293
|
+
assert result["queued"] is True
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_web_node_forget_by_id_and_name() -> None:
|
|
297
|
+
core._WEB_NODES.clear()
|
|
298
|
+
reg = core.web_node_register({"userAgent": "Chrome"}, service_url="http://h:8195/")
|
|
299
|
+
dev_id = reg["id"]
|
|
300
|
+
# forget by id
|
|
301
|
+
assert core.web_node_forget(dev_id)["forgot"] == dev_id
|
|
302
|
+
assert core.web_node_list()["count"] == 0
|
|
303
|
+
# forget by name
|
|
304
|
+
reg2 = core.web_node_register({"userAgent": "Android"}, service_url="http://h:8195/")
|
|
305
|
+
assert core.web_node_forget(reg2["name"])["forgot"] == reg2["id"]
|
|
306
|
+
assert core.web_node_list()["count"] == 0
|
|
307
|
+
# forgetting an unknown id is a graceful no-op
|
|
308
|
+
assert core.web_node_forget("nope")["ok"] is True
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_relay_bare_url_returns_node_health(tmp_path):
|
|
312
|
+
"""The bare relay URL (nodeUrl with no /health|/routes|/run suffix) is the node root and
|
|
313
|
+
must return health, not 404 — host mesh and users hit <nodeUrl> directly."""
|
|
314
|
+
core._WEB_NODES.clear()
|
|
315
|
+
server, port, base = _start_test_server(tmp_path)
|
|
316
|
+
try:
|
|
317
|
+
import urllib.request
|
|
318
|
+
# register a webpage node on this live test server
|
|
319
|
+
req = urllib.request.Request(f"{base}/api/webpage-node/register",
|
|
320
|
+
data=b'{"userAgent":"Android Chrome"}',
|
|
321
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
322
|
+
reg = json.loads(urllib.request.urlopen(req, timeout=3).read())
|
|
323
|
+
dev = reg["id"]
|
|
324
|
+
# bare relay URL → health (was 404 before the fix)
|
|
325
|
+
bare = json.loads(urllib.request.urlopen(f"{base}/api/webpage-node/relay/{dev}", timeout=3).read())
|
|
326
|
+
assert bare["ok"] is True
|
|
327
|
+
assert bare["kind"] == "node"
|
|
328
|
+
assert bare["nodeType"] == "webpage"
|
|
329
|
+
# /health still works too
|
|
330
|
+
health = json.loads(urllib.request.urlopen(f"{base}/api/webpage-node/relay/{dev}/health", timeout=3).read())
|
|
331
|
+
assert health["ok"] is True
|
|
332
|
+
finally:
|
|
333
|
+
server.shutdown()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_relay_bare_url_post_dispatches_run(tmp_path):
|
|
337
|
+
"""POST to the bare relay URL (the node's url, no /run suffix) must dispatch a URI via run,
|
|
338
|
+
not 404 — the host mesh POSTs {uri, payload} to <node_url> directly."""
|
|
339
|
+
core._WEB_NODES.clear()
|
|
340
|
+
server, port, base = _start_test_server(tmp_path)
|
|
341
|
+
try:
|
|
342
|
+
import urllib.request
|
|
343
|
+
reg = urllib.request.Request(f"{base}/api/webpage-node/register",
|
|
344
|
+
data=b'{"userAgent":"Android"}',
|
|
345
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
346
|
+
dev = json.loads(urllib.request.urlopen(reg, timeout=3).read())["id"]
|
|
347
|
+
# POST bare relay url with a uri; no page poller → run times out (queued) but NOT 404.
|
|
348
|
+
body = json.dumps({"uri": f"webpage://{dev}/page/query/info", "payload": {}}).encode()
|
|
349
|
+
req = urllib.request.Request(f"{base}/api/webpage-node/relay/{dev}", data=body,
|
|
350
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
351
|
+
r = json.loads(urllib.request.urlopen(req, timeout=12).read())
|
|
352
|
+
assert "not found" not in str(r.get("error", ""))
|
|
353
|
+
assert r.get("queued") is True or r.get("ok") is True # routed to run
|
|
354
|
+
finally:
|
|
355
|
+
server.shutdown()
|