py2hackCraft2 1.1.45__tar.gz → 1.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py2hackCraft2
3
- Version: 1.1.45
3
+ Version: 1.2.1
4
4
  Summary: Python client library for hackCraft2
5
5
  Home-page: https://github.com/0x48lab/hackCraft2-python
6
6
  Author: masafumi_t
@@ -65,6 +65,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
65
65
  # その他の操作...
66
66
  ```
67
67
 
68
+ ### セキュア(wss)接続(Colab / リモート)
69
+
70
+ Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
71
+
72
+ ```python
73
+ # 公開サーバーへ wss 接続
74
+ player.login("your-tunnel.example.com", secure=True)
75
+
76
+ # 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
77
+ player.login("self-hosted.example.com", secure=True, verify_ssl=False)
78
+ ```
79
+
80
+ ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
81
+
68
82
  ## ライセンス
69
83
 
70
84
  MIT License
@@ -38,6 +38,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
38
38
  # その他の操作...
39
39
  ```
40
40
 
41
+ ### セキュア(wss)接続(Colab / リモート)
42
+
43
+ Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
44
+
45
+ ```python
46
+ # 公開サーバーへ wss 接続
47
+ player.login("your-tunnel.example.com", secure=True)
48
+
49
+ # 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
50
+ player.login("self-hosted.example.com", secure=True, verify_ssl=False)
51
+ ```
52
+
53
+ ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
54
+
41
55
  ## ライセンス
42
56
 
43
57
  MIT License
@@ -3,9 +3,45 @@ import threading
3
3
  import time
4
4
  import json
5
5
  import logging
6
+ import ssl
7
+ import functools
6
8
  from dataclasses import dataclass
7
9
  from typing import Callable, Any, List, Optional, NamedTuple
8
10
 
11
+ # 標準のTLS(セキュアWebSocket)ポート
12
+ DEFAULT_SECURE_PORT = 443
13
+
14
+ # 接続確立を待つ上限秒数(到達不能・TLS不一致時に無限待機しないため)
15
+ CONNECT_TIMEOUT_SECONDS = 15
16
+
17
+
18
+ class ConnectionTimeoutError(Exception):
19
+ """接続確立が制限時間内に完了しなかったことを示す例外"""
20
+ pass
21
+
22
+
23
+ def build_ws_url(host: str, port: Optional[int], secure: bool) -> str:
24
+ """接続先 WebSocket URL を組み立てる(副作用なし)。
25
+
26
+ - secure=True: ``wss://{host}:{port or 443}/ws``
27
+ - secure=False: ``ws://{host}:{port}/ws``(従来どおり)
28
+ """
29
+ scheme = "wss" if secure else "ws"
30
+ if secure and port is None:
31
+ port = DEFAULT_SECURE_PORT
32
+ return "%s://%s:%d/ws" % (scheme, host, port)
33
+
34
+
35
+ def build_sslopt(secure: bool, verify_ssl: bool) -> Optional[dict]:
36
+ """run_forever に渡す sslopt を組み立てる(副作用なし)。
37
+
38
+ セキュア接続で証明書検証を無効化する場合のみ ``{"cert_reqs": ssl.CERT_NONE}``。
39
+ それ以外(非セキュア / 既定検証)は None(websocket-client の既定挙動に委ねる)。
40
+ """
41
+ if secure and not verify_ssl:
42
+ return {"cert_reqs": ssl.CERT_NONE}
43
+ return None
44
+
9
45
  def str_to_bool(s):
10
46
  """
11
47
  文字列をブール値に変換する
@@ -44,10 +80,11 @@ class _WebSocketClient:
44
80
  self.response_event = threading.Event()
45
81
  self.callbacks = {}
46
82
 
47
- def connect(self, host: str, port: int):
83
+ def connect(self, host: str, port: Optional[int] = None,
84
+ secure: bool = False, verify_ssl: bool = True):
48
85
  self.host = host
49
86
  self.port = port
50
- self.url = "ws://%s:%d/ws" % (host, port)
87
+ self.url = build_ws_url(host, port, secure)
51
88
  logging.debug("connecting '%s'" % (self.url))
52
89
  self.connected = False
53
90
  self.ws = websocket.WebSocketApp(self.url,
@@ -55,7 +92,10 @@ class _WebSocketClient:
55
92
  on_error=self._on_error,
56
93
  on_close=self._on_close)
57
94
  self.ws.on_open = self._on_open
58
- self.thread = threading.Thread(target=self.ws.run_forever)
95
+ # セキュア接続で証明書検証を無効化する場合のみ sslopt を渡す(None なら既定挙動)
96
+ run_forever = functools.partial(self.ws.run_forever,
97
+ sslopt=build_sslopt(secure, verify_ssl))
98
+ self.thread = threading.Thread(target=run_forever)
59
99
  self.thread.daemon = True
60
100
  self._run_forever()
61
101
 
@@ -118,8 +158,16 @@ class _WebSocketClient:
118
158
  def _run_forever(self):
119
159
  self.thread.start()
120
160
 
121
- def _wait_for_connection(self):
161
+ def _wait_for_connection(self, timeout: float = CONNECT_TIMEOUT_SECONDS):
162
+ # 一定時間内に接続が確立しない場合は例外を送出し、無限待機を避ける
163
+ deadline = time.monotonic() + timeout
122
164
  while not self.connected:
165
+ if time.monotonic() >= deadline:
166
+ raise ConnectionTimeoutError(
167
+ "接続を確立できませんでした(%s に %d 秒以内に接続できませんでした)。"
168
+ "ホスト/ポート、secure 指定、TLS証明書の検証設定を確認してください。"
169
+ % (getattr(self, "url", "?"), int(timeout))
170
+ )
123
171
  time.sleep(0.1)
124
172
 
125
173
 
@@ -193,12 +241,16 @@ class Location:
193
241
  - "~": 相対座標 (例: ~10, ~0, ~-5)
194
242
 
195
243
  - "^": ローカル座標 (例: ^0, ^5, ^0)
244
+ yaw (float): 水平方向の向き(デフォルトは0)
245
+ pitch (float): 垂直方向の向き(デフォルトは0)
196
246
  """
197
247
  x: int
198
248
  y: int
199
249
  z: int
200
250
  world: str = "world"
201
251
  cord: str = "" # デフォルトは絶対座標
252
+ yaw: float = 0
253
+ pitch: float = 0
202
254
 
203
255
  class LocationFactory:
204
256
  """
@@ -444,9 +496,18 @@ class Player:
444
496
  def __init__(self, player: str):
445
497
  self.name = player
446
498
 
447
- def login(self, host: str, port: int) -> 'Player':
499
+ def login(self, host: str, port: Optional[int] = None, *,
500
+ secure: bool = False, verify_ssl: bool = True) -> 'Player':
501
+ """サーバーにログインする。
502
+
503
+ Args:
504
+ host: サーバーのホスト名 / IP / 公開トンネルのドメイン
505
+ port: ポート番号。secure=True で省略した場合は標準TLSポート(443)を使用
506
+ secure: True で wss(TLS暗号化)接続。Colab 等から公開サーバーへ接続する場合に使用
507
+ verify_ssl: secure=True 時のTLS証明書検証。自己署名/自前トンネル向けに False で無効化可能
508
+ """
448
509
  self.client = _WebSocketClient()
449
- self.client.connect(host, port)
510
+ self.client.connect(host, port, secure=secure, verify_ssl=verify_ssl)
450
511
  self.client.send(json.dumps({
451
512
  "type": "login",
452
513
  "data": {
@@ -898,11 +959,11 @@ class Entity:
898
959
 
899
960
  def pop(self) -> bool:
900
961
  """
901
- 自分の位置を保存した位置に戻す
962
+ 自分の位置を保存した位置に戻す(ブロックがあっても移動可能、向きも復元)
902
963
  """
903
964
  if(len(self.positions) > 0):
904
965
  pos = self.positions.pop()
905
- self.teleport(pos)
966
+ self.client.send_call(self.uuid, "forceTeleport", [pos.x, pos.y, pos.z, pos.cord, pos.yaw, pos.pitch])
906
967
  return True
907
968
  else:
908
969
  return False
@@ -2207,6 +2268,197 @@ class Entity:
2207
2268
 
2208
2269
  # ===== Livestock Methods =====
2209
2270
 
2271
+ def get_biome(self) -> str:
2272
+ """
2273
+ 自分の現在位置のバイオームを取得する
2274
+
2275
+ Returns:
2276
+ str: バイオーム名(例: "PLAINS", "DESERT", "FOREST")
2277
+ """
2278
+ self.client.send_call(self.uuid, "getBiome")
2279
+ return self.client.result
2280
+
2281
+ def get_biome_at(self, loc: Location) -> str:
2282
+ """
2283
+ 指定された座標のバイオームを取得する
2284
+
2285
+ Args:
2286
+ loc (Location): 座標情報(LocationFactory.absolute/relative/localで生成)
2287
+
2288
+ Returns:
2289
+ str: バイオーム名(例: "PLAINS", "DESERT", "FOREST")
2290
+ """
2291
+ self.client.send_call(self.uuid, "getBiomeAt", [loc.x, loc.y, loc.z, loc.cord])
2292
+ return self.client.result
2293
+
2294
+ def get_time(self) -> int:
2295
+ """
2296
+ ワールドの時刻を取得する
2297
+
2298
+ Returns:
2299
+ int: ワールドの時刻(0-24000のtick値。0=朝6時, 6000=正午, 12000=夕方, 18000=真夜中)
2300
+ """
2301
+ self.client.send_call(self.uuid, "getTime")
2302
+ return int(self.client.result)
2303
+
2304
+ def get_weather(self) -> str:
2305
+ """
2306
+ ワールドの天気を取得する
2307
+
2308
+ Returns:
2309
+ str: 天気("clear", "rain", "thunder"のいずれか)
2310
+ """
2311
+ self.client.send_call(self.uuid, "getWeather")
2312
+ return self.client.result
2313
+
2314
+ def get_server_time(self) -> int:
2315
+ """
2316
+ サーバーの現在時刻をUnixタイムスタンプ(ミリ秒)で取得する
2317
+ イベントログの取得開始時刻の記録に使用する
2318
+
2319
+ Returns:
2320
+ int: サーバーの現在時刻(Unixタイムスタンプ、ミリ秒)
2321
+
2322
+ Example:
2323
+ .. code-block:: python
2324
+
2325
+ start = entity.get_server_time()
2326
+ # ... 何か作業 ...
2327
+ log = entity.get_event_log(since=start)
2328
+ """
2329
+ self.client.send_call(self.uuid, "getServerTime")
2330
+ return int(self.client.result)
2331
+
2332
+ def get_event_log(self, event_types: list = None, limit: int = 200) -> list:
2333
+ """
2334
+ ワールドのイベントログを取得する(差分取得)
2335
+ 呼び出すたびに、前回取得した位置以降の新しいイベントだけを返す
2336
+ サーバー側でエンティティごとにカーソルを管理しているため、sinceの指定は不要
2337
+
2338
+ Args:
2339
+ event_types (list, optional): 取得するイベントの種類(例: ["fish", "block_break"])
2340
+ limit (int): 最大取得件数(デフォルト: 200、最大: 200)
2341
+
2342
+ Returns:
2343
+ list: 新しいイベントのリスト。各イベントは辞書形式
2344
+ [{"type": "fish", "player": "taro", "item": "cod", "timestamp": 1714020000000}, ...]
2345
+
2346
+ Example:
2347
+ .. code-block:: python
2348
+
2349
+ new_events = entity.get_event_log(["fish"])
2350
+ for event in new_events:
2351
+ print(f"{event['player']}が{event['item']}を釣りました")
2352
+ """
2353
+ types_str = ",".join(event_types) if event_types else None
2354
+ self.client.send_call(self.uuid, "getEventLog", [types_str, limit])
2355
+ result = json.loads(self.client.result)
2356
+ return result.get("events", [])
2357
+
2358
+ def get_event_log_status(self) -> dict:
2359
+ """
2360
+ イベントログの状態を取得する
2361
+
2362
+ Returns:
2363
+ dict: ログの状態情報
2364
+ {"eventCount": 1234, "maxEvents": 50000, "serverTime": 1714020000000}
2365
+ """
2366
+ self.client.send_call(self.uuid, "getEventLogStatus")
2367
+ return json.loads(self.client.result)
2368
+
2369
+ def scan_blocks(self, volume: 'Volume') -> list:
2370
+ """
2371
+ 指定範囲のブロックを一括取得する(最大32×32×32)
2372
+
2373
+ Args:
2374
+ volume (Volume): スキャンする範囲
2375
+
2376
+ Returns:
2377
+ list: ブロック情報のリスト
2378
+ [{"name": "stone", "x": 100, "y": 64, "z": -200, ...}, ...]
2379
+
2380
+ Example:
2381
+ .. code-block:: python
2382
+
2383
+ area = Volume.relative(-5, -5, -5, 5, 5, 5)
2384
+ blocks = entity.scan_blocks(area)
2385
+ print(f"{len(blocks)}ブロックをスキャンしました")
2386
+ """
2387
+ x1, y1, z1, cord = volume.pos1
2388
+ x2, y2, z2, _ = volume.pos2
2389
+ self.client.send_call(self.uuid, "scanBlocks", [x1, y1, z1, x2, y2, z2, cord])
2390
+ return json.loads(self.client.result)
2391
+
2392
+ def reset_event_cursor(self):
2393
+ """
2394
+ イベントログのカーソルをリセットする
2395
+ 次回のget_event_log()は現在時刻以降のイベントから返すようになる
2396
+
2397
+ Example:
2398
+ .. code-block:: python
2399
+
2400
+ entity.reset_event_cursor()
2401
+ # ここから新しいイベントだけを取得する
2402
+ events = entity.get_event_log(["fish"])
2403
+ """
2404
+ self.client.send_call(self.uuid, "resetEventCursor")
2405
+
2406
+ def watch_events(self, event_types: list = None, interval: int = 1, callback: Callable = None) -> list:
2407
+ """
2408
+ リアルタイムでイベントを監視する
2409
+ 1秒ごとに新しいイベントを取得し、callbackが設定されていれば呼び出す
2410
+ Ctrl+C(KeyboardInterrupt)で停止し、全イベントを返す
2411
+ サーバー側でカーソルを管理するため、差分だけが自動的に返される
2412
+
2413
+ Args:
2414
+ event_types (list, optional): 監視するイベントの種類(例: ["fish", "block_break"])
2415
+ interval (int): ポーリング間隔(秒)。デフォルト: 1
2416
+ callback (Callable, optional): 新しいイベントが来たときに呼ばれる関数
2417
+ callback(new_events, all_events) の形式で呼ばれる
2418
+ new_events: 今回新しく取得したイベントのリスト
2419
+ all_events: これまでの全イベントのリスト
2420
+
2421
+ Returns:
2422
+ list: 監視中に収集した全イベントのリスト
2423
+
2424
+ Example:
2425
+ .. code-block:: python
2426
+
2427
+ events = entity.watch_events(["fish"])
2428
+
2429
+ .. code-block:: python
2430
+
2431
+ from IPython.display import clear_output
2432
+ import matplotlib.pyplot as plt
2433
+ import pandas as pd
2434
+
2435
+ def update_graph(new_events, all_events):
2436
+ clear_output(wait=True)
2437
+ df = pd.DataFrame(all_events)
2438
+ df["item"].value_counts().plot(kind="bar")
2439
+ plt.title(f"釣り結果({len(df)}回)")
2440
+ plt.show()
2441
+
2442
+ events = entity.watch_events(["fish"], callback=update_graph)
2443
+ """
2444
+ self.reset_event_cursor()
2445
+ all_events = []
2446
+
2447
+ try:
2448
+ while True:
2449
+ new_events = self.get_event_log(event_types=event_types)
2450
+ if new_events:
2451
+ all_events.extend(new_events)
2452
+ if callback:
2453
+ callback(new_events, all_events)
2454
+ time.sleep(interval)
2455
+ except KeyboardInterrupt:
2456
+ pass
2457
+
2458
+ return all_events
2459
+
2460
+ # ===== Livestock Methods =====
2461
+
2210
2462
  def livestock_count_nearby(self, animal_type: str = "ALL", radius: float = 50.0) -> int:
2211
2463
  """
2212
2464
  近くの動物を数える
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py2hackCraft2
3
- Version: 1.1.45
3
+ Version: 1.2.1
4
4
  Summary: Python client library for hackCraft2
5
5
  Home-page: https://github.com/0x48lab/hackCraft2-python
6
6
  Author: masafumi_t
@@ -65,6 +65,20 @@ entity.set_event_area(Volume.local(10, 10, 10, -10, -10, -10))
65
65
  # その他の操作...
66
66
  ```
67
67
 
68
+ ### セキュア(wss)接続(Colab / リモート)
69
+
70
+ Google Colab などから、HTTPS 公開トンネル(Cloudflare 等)越しに動いているサーバーへ接続する場合は `secure=True` を指定します。`port` を省略すると標準TLSポート(443)を使用します。
71
+
72
+ ```python
73
+ # 公開サーバーへ wss 接続
74
+ player.login("your-tunnel.example.com", secure=True)
75
+
76
+ # 自己署名証明書や自前トンネルで証明書検証を無効化する場合(自己責任)
77
+ player.login("self-hosted.example.com", secure=True, verify_ssl=False)
78
+ ```
79
+
80
+ ローカルサーバーへは従来どおり `player.login("localhost", 25570)` で接続できます(後方互換)。
81
+
68
82
  ## ライセンス
69
83
 
70
84
  MIT License
@@ -8,4 +8,9 @@ py2hackCraft2.egg-info/PKG-INFO
8
8
  py2hackCraft2.egg-info/SOURCES.txt
9
9
  py2hackCraft2.egg-info/dependency_links.txt
10
10
  py2hackCraft2.egg-info/requires.txt
11
- py2hackCraft2.egg-info/top_level.txt
11
+ py2hackCraft2.egg-info/top_level.txt
12
+ tests/__init__.py
13
+ tests/test_backward_compat.py
14
+ tests/test_connection_timeout.py
15
+ tests/test_connection_url.py
16
+ tests/test_secure_login.py
@@ -0,0 +1,8 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.pytest.ini_options]
6
+ testpaths = ["tests"]
7
+ python_files = ["test_*.py"]
8
+
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="py2hackCraft2",
5
- version="v1.1.45",
5
+ version="v1.2.1",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "websocket-client>=1.6.0",
@@ -24,4 +24,4 @@ setup(
24
24
  "Operating System :: OS Independent",
25
25
  ],
26
26
  python_requires=">=3.7",
27
- )
27
+ )
File without changes
@@ -0,0 +1,59 @@
1
+ """US2: 後方互換の回帰テスト。
2
+
3
+ 既存の login(host, port)(非secure)が従来どおり ws:// URL を構築し、
4
+ sslopt が None(証明書設定の影響なし)であることを検証する。
5
+ """
6
+ import json
7
+
8
+ import pytest
9
+
10
+ import py2hackCraft.modules as modules
11
+ from py2hackCraft.modules import Player
12
+
13
+
14
+ class _FakeWS:
15
+ last_url = None
16
+ last_sslopt = "UNSET"
17
+
18
+ def __init__(self, url, on_message=None, on_error=None, on_close=None):
19
+ _FakeWS.last_url = url
20
+ self.url = url
21
+ self.on_open = None
22
+ self._on_message = on_message
23
+
24
+ def run_forever(self, sslopt=None, **kwargs):
25
+ _FakeWS.last_sslopt = sslopt
26
+ if self.on_open:
27
+ self.on_open(self)
28
+
29
+ def send(self, message):
30
+ self._on_message(self, json.dumps({
31
+ "type": "logged",
32
+ "data": {"playerUUID": "uuid-xyz", "world": "world"},
33
+ }))
34
+
35
+ def close(self):
36
+ pass
37
+
38
+
39
+ @pytest.fixture
40
+ def patched(monkeypatch):
41
+ _FakeWS.last_url = None
42
+ _FakeWS.last_sslopt = "UNSET"
43
+ monkeypatch.setattr(modules.websocket, "WebSocketApp", _FakeWS)
44
+
45
+ def sync_run_forever(self):
46
+ self.thread.start()
47
+ self.thread.join()
48
+ monkeypatch.setattr(modules._WebSocketClient, "_run_forever", sync_run_forever)
49
+ return _FakeWS
50
+
51
+
52
+ def test_legacy_login_builds_plain_ws_url(patched):
53
+ # 既存の呼び出し方法(位置引数 host, port)が従来どおり動作すること
54
+ player = Player("tester")
55
+ player.login("localhost", 25570)
56
+ assert patched.last_url == "ws://localhost:25570/ws"
57
+ # 非secure では sslopt は None(verify_ssl フラグの影響を受けない)
58
+ assert patched.last_sslopt is None
59
+ assert player.uuid == "uuid-xyz"
@@ -0,0 +1,29 @@
1
+ """接続確立タイムアウトの単体テスト(FR-005: 無限待機しない)。
2
+
3
+ 実接続せず、connected が True にならない状況で _wait_for_connection が
4
+ 短時間で ConnectionTimeoutError を送出することを検証する。
5
+ """
6
+ import time
7
+
8
+ import pytest
9
+
10
+ from py2hackCraft.modules import _WebSocketClient, ConnectionTimeoutError
11
+
12
+
13
+ def test_wait_for_connection_times_out_when_never_connected():
14
+ client = _WebSocketClient()
15
+ client.url = "wss://unreachable.example.com:443/ws"
16
+ # connected は False のまま(接続成立しないケース)
17
+ start = time.monotonic()
18
+ with pytest.raises(ConnectionTimeoutError):
19
+ client._wait_for_connection(timeout=0.3)
20
+ elapsed = time.monotonic() - start
21
+ # 上限時間付近で速やかに失敗すること(無限待機しない)
22
+ assert elapsed < 5
23
+
24
+
25
+ def test_wait_for_connection_returns_immediately_when_connected():
26
+ client = _WebSocketClient()
27
+ client.connected = True
28
+ # 既に接続済みなら即座に戻る(例外なし)
29
+ client._wait_for_connection(timeout=0.3)
@@ -0,0 +1,34 @@
1
+ """build_ws_url / build_sslopt の純粋ロジック単体テスト(contracts/login-api.md 準拠)。
2
+
3
+ 実 WebSocket 接続を行わず、URL と TLS オプションの組み立てのみを検証する。
4
+ """
5
+ import ssl
6
+
7
+ from py2hackCraft.modules import build_ws_url, build_sslopt
8
+
9
+
10
+ class TestBuildWsUrl:
11
+ def test_local_non_secure(self):
12
+ assert build_ws_url("localhost", 25570, False) == "ws://localhost:25570/ws"
13
+
14
+ def test_secure_default_tls_port(self):
15
+ # secure かつ port 省略(None) → 標準TLSポート 443
16
+ assert build_ws_url("example.com", None, True) == "wss://example.com:443/ws"
17
+
18
+ def test_secure_custom_port(self):
19
+ assert build_ws_url("example.com", 8443, True) == "wss://example.com:8443/ws"
20
+
21
+
22
+ class TestBuildSslopt:
23
+ def test_non_secure_returns_none(self):
24
+ assert build_sslopt(False, True) is None
25
+
26
+ def test_secure_verify_returns_none(self):
27
+ # 既定検証は websocket-client の既定挙動に委ねる → None
28
+ assert build_sslopt(True, True) is None
29
+
30
+ def test_secure_no_verify_disables_cert_check(self):
31
+ assert build_sslopt(True, False) == {"cert_reqs": ssl.CERT_NONE}
32
+
33
+ def test_non_secure_ignores_verify_flag(self):
34
+ assert build_sslopt(False, False) is None
@@ -0,0 +1,72 @@
1
+ """US1: secure(wss) ログインの統合テスト。
2
+
3
+ 実サーバーには接続せず、websocket.WebSocketApp と run_forever を monkeypatch して、
4
+ login(secure=True) が wss URL を構築し sslopt を run_forever に渡すことを検証する。
5
+ """
6
+ import json
7
+ import ssl
8
+
9
+ import pytest
10
+
11
+ import py2hackCraft.modules as modules
12
+ from py2hackCraft.modules import Player
13
+
14
+
15
+ class _FakeWS:
16
+ """WebSocketApp の差し替え。run_forever の sslopt を記録し、接続成立させる。"""
17
+
18
+ last_url = None
19
+ last_sslopt = "UNSET"
20
+
21
+ def __init__(self, url, on_message=None, on_error=None, on_close=None):
22
+ _FakeWS.last_url = url
23
+ self.url = url
24
+ self.on_open = None
25
+ self._on_message = on_message
26
+
27
+ def run_forever(self, sslopt=None, **kwargs):
28
+ # connect() が functools.partial(run_forever, sslopt=...) で渡す sslopt を記録
29
+ _FakeWS.last_sslopt = sslopt
30
+ # 接続成立を通知(on_open → _WebSocketClient.connected = True)
31
+ if self.on_open:
32
+ self.on_open(self)
33
+
34
+ def send(self, message):
35
+ # login 送信に対して logged 応答を即返してフローを進める
36
+ self._on_message(self, json.dumps({
37
+ "type": "logged",
38
+ "data": {"playerUUID": "uuid-123", "world": "world"},
39
+ }))
40
+
41
+ def close(self):
42
+ pass
43
+
44
+
45
+ @pytest.fixture
46
+ def patched(monkeypatch):
47
+ _FakeWS.last_url = None
48
+ _FakeWS.last_sslopt = "UNSET"
49
+ monkeypatch.setattr(modules.websocket, "WebSocketApp", _FakeWS)
50
+
51
+ # スレッド起動を同期実行に置き換え(partial(run_forever, sslopt=...) をその場で実行)
52
+ def sync_run_forever(self):
53
+ self.thread.start()
54
+ self.thread.join()
55
+ monkeypatch.setattr(modules._WebSocketClient, "_run_forever", sync_run_forever)
56
+ return _FakeWS
57
+
58
+
59
+ def test_secure_login_builds_wss_url_and_passes_sslopt(patched):
60
+ player = Player("tester")
61
+ player.login("example.com", secure=True)
62
+ assert patched.last_url == "wss://example.com:443/ws"
63
+ # verify_ssl 既定(True) → sslopt は None(既定検証)
64
+ assert patched.last_sslopt is None
65
+ assert player.uuid == "uuid-123"
66
+
67
+
68
+ def test_secure_login_verify_ssl_false_disables_cert_check(patched):
69
+ player = Player("tester")
70
+ player.login("example.com", secure=True, verify_ssl=False)
71
+ assert patched.last_url == "wss://example.com:443/ws"
72
+ assert patched.last_sslopt == {"cert_reqs": ssl.CERT_NONE}
@@ -1,3 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=42", "wheel"]
3
- build-backend = "setuptools.build_meta"
File without changes