remotedesktop 0.3.0__tar.gz → 0.5.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.
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/PKG-INFO +11 -1
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/readme.md +10 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/__init__.py +1 -1
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/client.py +16 -1
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/server.py +12 -3
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/sharing.py +8 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_client_window.py +187 -164
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_server_window.py +15 -6
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_sharing.py +29 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_smoke.py +58 -58
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/.github/workflows/ci.yml +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/.gitignore +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/CLAUDE.md +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/LICENSE +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/pyproject.toml +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/run_claude.bat +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/run_client.bat +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/run_server.bat +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/scripts/make_coverage_badge.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/scripts/make_venv.bat +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/autostart.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/clipboard.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/config.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/db.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/discovery.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/input_injection.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/inventory.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/protocol.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/tls.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/viewer.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/src/remotedesktop/window_state.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/conftest.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_autostart.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_clipboard.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_config.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_discovery.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_input.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_input_injection.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_inventory.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_protocol.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_tls.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/tests/test_window_state.py +0 -0
- {remotedesktop-0.3.0 → remotedesktop-0.5.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remotedesktop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Remote desktop client/server for Windows computers on the same LAN, with autodiscovery. Provides screen, keyboard, mouse, and clipboard sharing without RDP or Microsoft authentication.
|
|
5
5
|
Author-email: James Abel <j@abel.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -115,3 +115,13 @@ uv run pytest # run the tests
|
|
|
115
115
|
Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
|
|
116
116
|
MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
|
|
117
117
|
From PowerShell, Qt uses the Windows schannel backend as intended.
|
|
118
|
+
|
|
119
|
+
### The `badges` branch
|
|
120
|
+
|
|
121
|
+
The coverage badge above is served from the `badges` branch
|
|
122
|
+
(`raw.githubusercontent.com/.../badges/coverage.svg`). CI regenerates the
|
|
123
|
+
SVG after each test run on `master` and force-pushes it there as a single
|
|
124
|
+
orphan commit. It lives on its own branch because `master` only accepts
|
|
125
|
+
pull requests (a repository ruleset), so CI cannot commit to it directly;
|
|
126
|
+
keeping the badge in the repo avoids depending on an external coverage
|
|
127
|
+
service. The branch is generated output — never branch from it or merge it.
|
|
@@ -96,3 +96,13 @@ uv run pytest # run the tests
|
|
|
96
96
|
Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
|
|
97
97
|
MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
|
|
98
98
|
From PowerShell, Qt uses the Windows schannel backend as intended.
|
|
99
|
+
|
|
100
|
+
### The `badges` branch
|
|
101
|
+
|
|
102
|
+
The coverage badge above is served from the `badges` branch
|
|
103
|
+
(`raw.githubusercontent.com/.../badges/coverage.svg`). CI regenerates the
|
|
104
|
+
SVG after each test run on `master` and force-pushes it there as a single
|
|
105
|
+
orphan commit. It lives on its own branch because `master` only accepts
|
|
106
|
+
pull requests (a repository ruleset), so CI cannot commit to it directly;
|
|
107
|
+
keeping the badge in the repo avoids depending on an external coverage
|
|
108
|
+
service. The branch is generated output — never branch from it or merge it.
|
|
@@ -83,7 +83,9 @@ class DiscoveryPanel(QWidget):
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class ClientWindow(QMainWindow):
|
|
86
|
-
def __init__(
|
|
86
|
+
def __init__(
|
|
87
|
+
self, *, connection: sqlite3.Connection | None = None, auto_scan: bool = True
|
|
88
|
+
) -> None:
|
|
87
89
|
super().__init__()
|
|
88
90
|
self.setWindowTitle("Remote Desktop Client")
|
|
89
91
|
# Tests inject a connection to a temp database; the app uses the default.
|
|
@@ -128,6 +130,9 @@ class ClientWindow(QMainWindow):
|
|
|
128
130
|
self._settings = Settings(self._db)
|
|
129
131
|
window_state.restore_geometry(self, self._settings, window_state.CLIENT_GEOMETRY_KEY)
|
|
130
132
|
self.log("Client started")
|
|
133
|
+
# Tests pass auto_scan=False so window tests never broadcast on the LAN.
|
|
134
|
+
if auto_scan:
|
|
135
|
+
self.discovery_panel.refresh()
|
|
131
136
|
|
|
132
137
|
def log(self, message: str) -> None:
|
|
133
138
|
self.connection_log.appendPlainText(f"{time.strftime('%H:%M:%S')} {message}")
|
|
@@ -180,6 +185,7 @@ class ClientWindow(QMainWindow):
|
|
|
180
185
|
self._client = client
|
|
181
186
|
client.status.connect(self.log)
|
|
182
187
|
client.connected.connect(self._on_connected)
|
|
188
|
+
client.approvalPending.connect(self._on_approval_pending)
|
|
183
189
|
client.denied.connect(self._on_denied)
|
|
184
190
|
client.disconnected.connect(self._on_disconnected)
|
|
185
191
|
client.frameReceived.connect(self._on_frame)
|
|
@@ -187,6 +193,15 @@ class ClientWindow(QMainWindow):
|
|
|
187
193
|
self.statusBar().showMessage(f"Connecting to {server.name} ({server.host}:{server.port}) …")
|
|
188
194
|
client.connect_to(server.host, server.port)
|
|
189
195
|
|
|
196
|
+
def _on_approval_pending(self) -> None:
|
|
197
|
+
self.viewer.clear(
|
|
198
|
+
f"Waiting for approval — someone at {self._server_name} "
|
|
199
|
+
"must allow this connection"
|
|
200
|
+
)
|
|
201
|
+
self.statusBar().showMessage(
|
|
202
|
+
f"Waiting for the user on {self._server_name} to approve this computer …"
|
|
203
|
+
)
|
|
204
|
+
|
|
190
205
|
def _on_connected(self, server_name: str) -> None:
|
|
191
206
|
self._server_name = server_name or self._server_name
|
|
192
207
|
self._connected = True
|
|
@@ -163,12 +163,21 @@ class ServerWindow(QMainWindow):
|
|
|
163
163
|
self.share_server.revoke_client(client_id)
|
|
164
164
|
|
|
165
165
|
def _ask_approval(self, client_id: str, client_name: str) -> bool:
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
box = QMessageBox(
|
|
167
|
+
QMessageBox.Icon.Question,
|
|
168
168
|
"Connection request",
|
|
169
169
|
f'Allow "{client_name}" to view this desktop?\n\nClient id: {client_id}',
|
|
170
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
171
|
+
self,
|
|
170
172
|
)
|
|
171
|
-
|
|
173
|
+
# The server usually isn't the foreground app when a request arrives,
|
|
174
|
+
# and Windows won't give focus to a background app's dialog — keep it
|
|
175
|
+
# on top so it can't sit unnoticed behind other windows.
|
|
176
|
+
box.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
|
|
177
|
+
box.show()
|
|
178
|
+
box.raise_()
|
|
179
|
+
box.activateWindow()
|
|
180
|
+
return box.exec() == QMessageBox.StandardButton.Yes
|
|
172
181
|
|
|
173
182
|
def closeEvent(self, event: QCloseEvent) -> None:
|
|
174
183
|
window_state.save_geometry(self, self._settings, window_state.SERVER_GEOMETRY_KEY)
|
|
@@ -256,6 +256,8 @@ class ShareServer(QObject):
|
|
|
256
256
|
)
|
|
257
257
|
else:
|
|
258
258
|
self.status.emit(f'"{client_name}" is new — asking for permission')
|
|
259
|
+
# Tell the client it's waiting on a human, not a stalled connection.
|
|
260
|
+
stream.send_json({"type": "pending"})
|
|
259
261
|
# The approval prompt is modal: a nested event loop runs while it is
|
|
260
262
|
# open, so this stream can disconnect (and _drop can run) meanwhile.
|
|
261
263
|
self._prompting.add(stream)
|
|
@@ -407,6 +409,7 @@ class ShareClient(QObject):
|
|
|
407
409
|
"""Connects to a ShareServer over TLS, authenticates, and emits frames."""
|
|
408
410
|
|
|
409
411
|
connected = Signal(str) # server name
|
|
412
|
+
approvalPending = Signal() # server is asking its user for permission
|
|
410
413
|
denied = Signal(str) # reason
|
|
411
414
|
disconnected = Signal()
|
|
412
415
|
frameReceived = Signal(QImage)
|
|
@@ -521,6 +524,11 @@ class ShareClient(QObject):
|
|
|
521
524
|
f'Server "{name}" accepted the connection — waiting for first frame'
|
|
522
525
|
)
|
|
523
526
|
self.connected.emit(name)
|
|
527
|
+
case "pending":
|
|
528
|
+
self.status.emit(
|
|
529
|
+
"Server is asking its user for permission — waiting for approval"
|
|
530
|
+
)
|
|
531
|
+
self.approvalPending.emit()
|
|
524
532
|
case "denied":
|
|
525
533
|
reason = str(message.get("reason", "denied"))
|
|
526
534
|
self.status.emit(f"Server denied the connection: {reason}")
|
|
@@ -1,164 +1,187 @@
|
|
|
1
|
-
from PySide6.QtCore import Qt
|
|
2
|
-
from PySide6.QtWidgets import QListWidgetItem, QMessageBox
|
|
3
|
-
|
|
4
|
-
from remotedesktop import client as client_module
|
|
5
|
-
from remotedesktop import db
|
|
6
|
-
from remotedesktop.client import ClientWindow, DiscoveryPanel
|
|
7
|
-
from remotedesktop.config import PairedClients
|
|
8
|
-
from remotedesktop.discovery import ServerInfo
|
|
9
|
-
from remotedesktop.sharing import ShareServer
|
|
10
|
-
|
|
11
|
-
from test_sharing import pump
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def make_window(tmp_path):
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assert
|
|
32
|
-
window.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assert peers
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
assert
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
window = make_window(tmp_path)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
pump
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
window.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
window.
|
|
117
|
-
pump(qapp, lambda:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
panel.serversFound.connect(found.append)
|
|
161
|
-
panel.
|
|
162
|
-
|
|
163
|
-
assert
|
|
164
|
-
|
|
1
|
+
from PySide6.QtCore import Qt
|
|
2
|
+
from PySide6.QtWidgets import QListWidgetItem, QMessageBox
|
|
3
|
+
|
|
4
|
+
from remotedesktop import client as client_module
|
|
5
|
+
from remotedesktop import db
|
|
6
|
+
from remotedesktop.client import ClientWindow, DiscoveryPanel
|
|
7
|
+
from remotedesktop.config import PairedClients
|
|
8
|
+
from remotedesktop.discovery import ServerInfo
|
|
9
|
+
from remotedesktop.sharing import ShareServer
|
|
10
|
+
|
|
11
|
+
from test_sharing import pump
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_window(tmp_path):
|
|
15
|
+
# auto_scan=False: window tests must never broadcast a discovery probe on the LAN.
|
|
16
|
+
return ClientWindow(connection=db.connect(tmp_path / "client.db"), auto_scan=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_share_server(credentials, tmp_path, *, approve=lambda *_: True):
|
|
20
|
+
server = ShareServer(
|
|
21
|
+
approve_client=approve,
|
|
22
|
+
credentials=credentials,
|
|
23
|
+
paired=PairedClients(db.connect(tmp_path / "server.db")),
|
|
24
|
+
)
|
|
25
|
+
assert server.listen(0)
|
|
26
|
+
return server
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_window_starts_disconnected(qapp, tmp_path):
|
|
30
|
+
window = make_window(tmp_path)
|
|
31
|
+
assert window.statusBar().currentMessage() == "Not connected"
|
|
32
|
+
assert "Client started" in window.connection_log.toPlainText()
|
|
33
|
+
window.close()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_discovered_servers_are_recorded(qapp, tmp_path):
|
|
37
|
+
window = make_window(tmp_path)
|
|
38
|
+
info = ServerInfo(name="box", host="10.0.0.7", port=1234)
|
|
39
|
+
window._record_discovered([info])
|
|
40
|
+
peers = window.inventory.peers()
|
|
41
|
+
assert len(peers) == 1
|
|
42
|
+
assert peers[0].state == "discovered"
|
|
43
|
+
window.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_connect_view_and_disconnect_full_flow(qapp, credentials, tmp_path):
|
|
47
|
+
server = make_share_server(credentials, tmp_path)
|
|
48
|
+
window = make_window(tmp_path)
|
|
49
|
+
info = ServerInfo(name="box", host="127.0.0.1", port=server.port)
|
|
50
|
+
try:
|
|
51
|
+
window._on_server_activated(info)
|
|
52
|
+
pump(qapp, lambda: window.viewer.has_frame)
|
|
53
|
+
assert window._connected
|
|
54
|
+
assert "Viewing" in window.statusBar().currentMessage()
|
|
55
|
+
key = f"127.0.0.1:{server.port}"
|
|
56
|
+
assert window.inventory._peers[key].state == "connected"
|
|
57
|
+
|
|
58
|
+
# Input from the viewer is forwarded while connected.
|
|
59
|
+
window._on_input_event({"action": "move", "x": 0.5, "y": 0.5})
|
|
60
|
+
|
|
61
|
+
# A re-scan that finds the connected server must not downgrade it.
|
|
62
|
+
window._record_discovered([info])
|
|
63
|
+
assert window.inventory._peers[key].state == "connected"
|
|
64
|
+
|
|
65
|
+
server.close()
|
|
66
|
+
pump(qapp, lambda: not window._connected)
|
|
67
|
+
assert window.inventory._peers[key].state == "disconnected"
|
|
68
|
+
finally:
|
|
69
|
+
window.close()
|
|
70
|
+
server.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_waiting_for_approval_is_shown_while_server_prompts(qapp, credentials, tmp_path):
|
|
74
|
+
window = make_window(tmp_path)
|
|
75
|
+
|
|
76
|
+
def approve(cid, name):
|
|
77
|
+
# While the server-side prompt is "open", the client window must show
|
|
78
|
+
# the waiting state (pump until the pending message arrives).
|
|
79
|
+
pump(
|
|
80
|
+
qapp,
|
|
81
|
+
lambda: "Waiting for the user on box" in window.statusBar().currentMessage(),
|
|
82
|
+
)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
server = make_share_server(credentials, tmp_path, approve=approve)
|
|
86
|
+
try:
|
|
87
|
+
window._on_server_activated(ServerInfo(name="box", host="127.0.0.1", port=server.port))
|
|
88
|
+
pump(qapp, lambda: window.viewer.has_frame)
|
|
89
|
+
assert "asking its user for permission" in window.connection_log.toPlainText()
|
|
90
|
+
finally:
|
|
91
|
+
window.close()
|
|
92
|
+
server.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_denied_connection_keeps_denied_state(qapp, credentials, tmp_path):
|
|
96
|
+
server = make_share_server(credentials, tmp_path, approve=lambda *_: False)
|
|
97
|
+
window = make_window(tmp_path)
|
|
98
|
+
try:
|
|
99
|
+
window._on_server_activated(ServerInfo(name="box", host="127.0.0.1", port=server.port))
|
|
100
|
+
pump(qapp, lambda: window._denied)
|
|
101
|
+
pump(qapp, lambda: "Denied" in window.statusBar().currentMessage())
|
|
102
|
+
key = f"127.0.0.1:{server.port}"
|
|
103
|
+
pump(qapp, lambda: window.inventory._peers[key].state == "denied")
|
|
104
|
+
# The trailing socket disconnect must not overwrite "denied".
|
|
105
|
+
pump(qapp, lambda: True, timeout=0.3)
|
|
106
|
+
assert window.inventory._peers[key].state == "denied"
|
|
107
|
+
finally:
|
|
108
|
+
window.close()
|
|
109
|
+
server.close()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_activating_a_second_server_replaces_the_connection(qapp, credentials, tmp_path):
|
|
113
|
+
server = make_share_server(credentials, tmp_path)
|
|
114
|
+
window = make_window(tmp_path)
|
|
115
|
+
try:
|
|
116
|
+
window._on_server_activated(ServerInfo(name="box", host="127.0.0.1", port=server.port))
|
|
117
|
+
pump(qapp, lambda: window._connected)
|
|
118
|
+
first_client = window._client
|
|
119
|
+
window._on_server_activated(ServerInfo(name="box", host="127.0.0.1", port=server.port))
|
|
120
|
+
assert window._client is not first_client
|
|
121
|
+
pump(qapp, lambda: window._connected)
|
|
122
|
+
finally:
|
|
123
|
+
window.close()
|
|
124
|
+
server.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_forget_server_disconnects_and_forgets(qapp, credentials, tmp_path, monkeypatch):
|
|
128
|
+
server = make_share_server(credentials, tmp_path)
|
|
129
|
+
window = make_window(tmp_path)
|
|
130
|
+
key = f"127.0.0.1:{server.port}"
|
|
131
|
+
try:
|
|
132
|
+
window._on_server_activated(ServerInfo(name="box", host="127.0.0.1", port=server.port))
|
|
133
|
+
pump(qapp, lambda: window._connected)
|
|
134
|
+
assert window._known_servers.get(key) is not None
|
|
135
|
+
|
|
136
|
+
monkeypatch.setattr(
|
|
137
|
+
QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.StandardButton.Yes)
|
|
138
|
+
)
|
|
139
|
+
window._forget_server(key)
|
|
140
|
+
pump(qapp, lambda: not window._connected)
|
|
141
|
+
assert window._known_servers.get(key) is None
|
|
142
|
+
assert window.inventory._peers[key].state == "forgotten"
|
|
143
|
+
|
|
144
|
+
# Answering "No" leaves a known server alone.
|
|
145
|
+
monkeypatch.setattr(
|
|
146
|
+
QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.StandardButton.No)
|
|
147
|
+
)
|
|
148
|
+
window._forget_server(key)
|
|
149
|
+
finally:
|
|
150
|
+
window.close()
|
|
151
|
+
server.close()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_discovery_panel_lists_scan_results(qapp, monkeypatch):
|
|
155
|
+
info = ServerInfo(name="box", host="10.0.0.7", port=1234)
|
|
156
|
+
monkeypatch.setattr(client_module, "discover_servers", lambda: [info])
|
|
157
|
+
panel = DiscoveryPanel()
|
|
158
|
+
found: list[list] = []
|
|
159
|
+
activated: list[ServerInfo] = []
|
|
160
|
+
panel.serversFound.connect(found.append)
|
|
161
|
+
panel.serverActivated.connect(activated.append)
|
|
162
|
+
panel.refresh()
|
|
163
|
+
assert not panel._refresh_button.isEnabled()
|
|
164
|
+
pump(qapp, lambda: found)
|
|
165
|
+
assert found == [[info]]
|
|
166
|
+
assert panel._refresh_button.isEnabled()
|
|
167
|
+
assert panel.server_list.count() == 1
|
|
168
|
+
|
|
169
|
+
item = panel.server_list.item(0)
|
|
170
|
+
assert isinstance(item, QListWidgetItem)
|
|
171
|
+
panel._on_item_activated(item)
|
|
172
|
+
assert activated == [info]
|
|
173
|
+
assert item.data(Qt.ItemDataRole.UserRole) == info
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_discovery_panel_survives_scan_failure(qapp, monkeypatch):
|
|
177
|
+
def boom():
|
|
178
|
+
raise RuntimeError("scan exploded")
|
|
179
|
+
|
|
180
|
+
monkeypatch.setattr(client_module, "discover_servers", boom)
|
|
181
|
+
panel = DiscoveryPanel()
|
|
182
|
+
found: list[list] = []
|
|
183
|
+
panel.serversFound.connect(found.append)
|
|
184
|
+
panel.refresh()
|
|
185
|
+
pump(qapp, lambda: found)
|
|
186
|
+
assert found == [[]]
|
|
187
|
+
assert panel._refresh_button.isEnabled()
|
|
@@ -85,10 +85,19 @@ def test_autostart_checkbox_toggles_registration(qapp, credentials, tmp_path):
|
|
|
85
85
|
window.close()
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def stub_approval_prompt(monkeypatch, button):
|
|
89
|
+
"""Answer _ask_approval's stay-on-top QMessageBox without showing any UI.
|
|
90
|
+
|
|
91
|
+
The approval prompt is an instance QMessageBox (show + exec), not
|
|
92
|
+
QMessageBox.question, so both methods are stubbed: show so nothing pops
|
|
93
|
+
up on screen, exec so the "user" answers immediately.
|
|
94
|
+
"""
|
|
95
|
+
monkeypatch.setattr(QMessageBox, "show", lambda self: None)
|
|
96
|
+
monkeypatch.setattr(QMessageBox, "exec", lambda self: button)
|
|
97
|
+
|
|
98
|
+
|
|
88
99
|
def test_approval_prompt_pairs_client_and_updates_summary(qapp, credentials, tmp_path, monkeypatch):
|
|
89
|
-
monkeypatch.
|
|
90
|
-
QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.StandardButton.Yes)
|
|
91
|
-
)
|
|
100
|
+
stub_approval_prompt(monkeypatch, QMessageBox.StandardButton.Yes)
|
|
92
101
|
window = make_window(credentials, tmp_path)
|
|
93
102
|
client = make_client(tmp_path)
|
|
94
103
|
names = []
|
|
@@ -105,9 +114,7 @@ def test_approval_prompt_pairs_client_and_updates_summary(qapp, credentials, tmp
|
|
|
105
114
|
|
|
106
115
|
|
|
107
116
|
def test_refused_approval_denies_client(qapp, credentials, tmp_path, monkeypatch):
|
|
108
|
-
monkeypatch.
|
|
109
|
-
QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.StandardButton.No)
|
|
110
|
-
)
|
|
117
|
+
stub_approval_prompt(monkeypatch, QMessageBox.StandardButton.No)
|
|
111
118
|
window = make_window(credentials, tmp_path)
|
|
112
119
|
client = make_client(tmp_path)
|
|
113
120
|
denials = []
|
|
@@ -122,6 +129,8 @@ def test_refused_approval_denies_client(qapp, credentials, tmp_path, monkeypatch
|
|
|
122
129
|
|
|
123
130
|
|
|
124
131
|
def test_revoke_via_window_disconnects_client(qapp, credentials, tmp_path, monkeypatch):
|
|
132
|
+
stub_approval_prompt(monkeypatch, QMessageBox.StandardButton.Yes)
|
|
133
|
+
# The revoke confirmation is still QMessageBox.question — patch it separately.
|
|
125
134
|
monkeypatch.setattr(
|
|
126
135
|
QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.StandardButton.Yes)
|
|
127
136
|
)
|
|
@@ -99,6 +99,35 @@ def test_reconnect_uses_token_without_prompting(qapp, credentials, tmp_path):
|
|
|
99
99
|
server.close()
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def test_approval_pending_is_signaled_only_when_prompting(qapp, credentials, tmp_path):
|
|
103
|
+
server = make_server(credentials, tmp_path, approve=lambda *_: True)
|
|
104
|
+
try:
|
|
105
|
+
# First connection: the server prompts, so the client hears "pending"
|
|
106
|
+
# before it is admitted.
|
|
107
|
+
client = make_client(tmp_path)
|
|
108
|
+
events: list[str] = []
|
|
109
|
+
client.approvalPending.connect(lambda: events.append("pending"))
|
|
110
|
+
client.connected.connect(lambda _name: events.append("connected"))
|
|
111
|
+
client.connect_to("127.0.0.1", server.port)
|
|
112
|
+
pump(qapp, lambda: "connected" in events)
|
|
113
|
+
assert events == ["pending", "connected"]
|
|
114
|
+
client.close()
|
|
115
|
+
pump(qapp, lambda: True, timeout=0.3)
|
|
116
|
+
|
|
117
|
+
# Token reconnect: no prompt on the server, so no pending signal.
|
|
118
|
+
client2 = make_client(tmp_path)
|
|
119
|
+
events2: list[str] = []
|
|
120
|
+
client2.approvalPending.connect(lambda: events2.append("pending"))
|
|
121
|
+
client2.connected.connect(lambda _name: events2.append("connected"))
|
|
122
|
+
client2.connect_to("127.0.0.1", server.port)
|
|
123
|
+
pump(qapp, lambda: "connected" in events2)
|
|
124
|
+
assert events2 == ["connected"]
|
|
125
|
+
finally:
|
|
126
|
+
client2.close()
|
|
127
|
+
client.close()
|
|
128
|
+
server.close()
|
|
129
|
+
|
|
130
|
+
|
|
102
131
|
def test_revoke_disconnects_and_requires_reapproval(qapp, credentials, tmp_path):
|
|
103
132
|
prompts = []
|
|
104
133
|
server = make_server(
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import socket
|
|
2
|
-
|
|
3
|
-
import remotedesktop
|
|
4
|
-
from remotedesktop import db
|
|
5
|
-
from remotedesktop.client import ClientWindow, DiscoveryPanel
|
|
6
|
-
from remotedesktop.config import PairedClients
|
|
7
|
-
from remotedesktop.discovery import ServerInfo, discover_servers
|
|
8
|
-
from remotedesktop.server import ServerWindow
|
|
9
|
-
from remotedesktop.viewer import ViewerWidget
|
|
10
|
-
|
|
11
|
-
from test_discovery import LOOPBACK, free_udp_port
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_version() -> None:
|
|
15
|
-
assert remotedesktop.__version__
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_client_window_hosts_viewer(qapp) -> None:
|
|
19
|
-
window = ClientWindow()
|
|
20
|
-
assert isinstance(window.viewer, ViewerWidget)
|
|
21
|
-
assert isinstance(window.discovery_panel, DiscoveryPanel)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_discovery_panel_lists_servers(qapp) -> None:
|
|
25
|
-
panel = DiscoveryPanel()
|
|
26
|
-
server = ServerInfo(name="testbox", host="192.168.1.7", port=12345)
|
|
27
|
-
panel._show_results([server])
|
|
28
|
-
assert panel.server_list.count() == 1
|
|
29
|
-
assert "testbox" in panel.server_list.item(0).text()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_server_window_is_discoverable(qapp, credentials, tmp_path) -> None:
|
|
33
|
-
port = free_udp_port()
|
|
34
|
-
window = ServerWindow(
|
|
35
|
-
discovery_port=port,
|
|
36
|
-
connect_port=0,
|
|
37
|
-
paired=PairedClients(db.connect(tmp_path / "server.db")),
|
|
38
|
-
credentials=credentials,
|
|
39
|
-
)
|
|
40
|
-
try:
|
|
41
|
-
servers = discover_servers(
|
|
42
|
-
timeout=2.0, discovery_port=port, broadcast_hosts=(LOOPBACK,)
|
|
43
|
-
)
|
|
44
|
-
assert [s.name for s in servers] == [socket.gethostname()]
|
|
45
|
-
assert servers[0].port == window.share_server.port
|
|
46
|
-
finally:
|
|
47
|
-
window.close()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_viewer_widget_shows_and_clears_frames(qapp) -> None:
|
|
51
|
-
from PySide6.QtGui import QImage
|
|
52
|
-
|
|
53
|
-
viewer = ViewerWidget()
|
|
54
|
-
assert not viewer.has_frame
|
|
55
|
-
viewer.show_frame(QImage(8, 8, QImage.Format.Format_RGB32))
|
|
56
|
-
assert viewer.has_frame
|
|
57
|
-
viewer.clear("gone")
|
|
58
|
-
assert not viewer.has_frame
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
import remotedesktop
|
|
4
|
+
from remotedesktop import db
|
|
5
|
+
from remotedesktop.client import ClientWindow, DiscoveryPanel
|
|
6
|
+
from remotedesktop.config import PairedClients
|
|
7
|
+
from remotedesktop.discovery import ServerInfo, discover_servers
|
|
8
|
+
from remotedesktop.server import ServerWindow
|
|
9
|
+
from remotedesktop.viewer import ViewerWidget
|
|
10
|
+
|
|
11
|
+
from test_discovery import LOOPBACK, free_udp_port
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_version() -> None:
|
|
15
|
+
assert remotedesktop.__version__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_client_window_hosts_viewer(qapp) -> None:
|
|
19
|
+
window = ClientWindow(auto_scan=False)
|
|
20
|
+
assert isinstance(window.viewer, ViewerWidget)
|
|
21
|
+
assert isinstance(window.discovery_panel, DiscoveryPanel)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_discovery_panel_lists_servers(qapp) -> None:
|
|
25
|
+
panel = DiscoveryPanel()
|
|
26
|
+
server = ServerInfo(name="testbox", host="192.168.1.7", port=12345)
|
|
27
|
+
panel._show_results([server])
|
|
28
|
+
assert panel.server_list.count() == 1
|
|
29
|
+
assert "testbox" in panel.server_list.item(0).text()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_server_window_is_discoverable(qapp, credentials, tmp_path) -> None:
|
|
33
|
+
port = free_udp_port()
|
|
34
|
+
window = ServerWindow(
|
|
35
|
+
discovery_port=port,
|
|
36
|
+
connect_port=0,
|
|
37
|
+
paired=PairedClients(db.connect(tmp_path / "server.db")),
|
|
38
|
+
credentials=credentials,
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
servers = discover_servers(
|
|
42
|
+
timeout=2.0, discovery_port=port, broadcast_hosts=(LOOPBACK,)
|
|
43
|
+
)
|
|
44
|
+
assert [s.name for s in servers] == [socket.gethostname()]
|
|
45
|
+
assert servers[0].port == window.share_server.port
|
|
46
|
+
finally:
|
|
47
|
+
window.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_viewer_widget_shows_and_clears_frames(qapp) -> None:
|
|
51
|
+
from PySide6.QtGui import QImage
|
|
52
|
+
|
|
53
|
+
viewer = ViewerWidget()
|
|
54
|
+
assert not viewer.has_frame
|
|
55
|
+
viewer.show_frame(QImage(8, 8, QImage.Format.Format_RGB32))
|
|
56
|
+
assert viewer.has_frame
|
|
57
|
+
viewer.clear("gone")
|
|
58
|
+
assert not viewer.has_frame
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|