RemoteRF 2.2.3__tar.gz → 2.2.5__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.
Files changed (95) hide show
  1. {remoterf-2.2.3/src/remoterf.egg-info → remoterf-2.2.5}/PKG-INFO +1 -1
  2. {remoterf-2.2.3 → remoterf-2.2.5}/pyproject.toml +1 -1
  3. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/app.py +14 -3
  4. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/homes.py +12 -19
  5. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/remoterf_cli.py +22 -19
  6. {remoterf-2.2.3 → remoterf-2.2.5/src/remoterf.egg-info}/PKG-INFO +1 -1
  7. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_cli_login_fresh.py +68 -27
  8. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_homes.py +3 -3
  9. {remoterf-2.2.3 → remoterf-2.2.5}/LICENSE +0 -0
  10. {remoterf-2.2.3 → remoterf-2.2.5}/README.md +0 -0
  11. {remoterf-2.2.3 → remoterf-2.2.5}/setup.cfg +0 -0
  12. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/__init__.py +0 -0
  13. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/__init__.py +0 -0
  14. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/__init__.py +0 -0
  15. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
  16. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
  17. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  18. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/grpc.proto +0 -0
  19. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
  20. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
  21. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/grpc/v2_codec.py +0 -0
  22. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/tos_notice.txt +0 -0
  23. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/__init__.py +0 -0
  24. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/ansi_codes.py +0 -0
  25. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/api_token.py +0 -0
  26. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/banner.py +0 -0
  27. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/list_string.py +0 -0
  28. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/common/utils/process_arg.py +0 -0
  29. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/config/__init__.py +0 -0
  30. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/config/cert_fetcher.py +0 -0
  31. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/config/config.py +0 -0
  32. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/__init__.py +0 -0
  33. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/acc_login.py +0 -0
  34. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/banner_note.txt +0 -0
  35. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/direct_path.py +0 -0
  36. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/dynamic_v2_transport.py +0 -0
  37. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/grpc_acc.py +0 -0
  38. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/grpc_client.py +0 -0
  39. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/secure_channel.py +0 -0
  40. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/core/v2_errors.py +0 -0
  41. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/__init__.py +0 -0
  42. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/backend.py +0 -0
  43. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/direct.py +0 -0
  44. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/http.py +0 -0
  45. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/deployment/state.py +0 -0
  46. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/__init__.py +0 -0
  47. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/_virtual.py +0 -0
  48. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
  49. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
  50. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/dynamic_device.py +0 -0
  51. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/dynamic_v2.py +0 -0
  52. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/hackrf/__init__.py +0 -0
  53. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
  54. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
  55. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  56. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
  57. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
  58. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/support/__init__.py +0 -0
  59. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
  60. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/support/uhd.py +0 -0
  61. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
  62. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/support/uhd_v2.py +0 -0
  63. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
  64. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  65. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/usrp/__init__.py +0 -0
  66. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
  67. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
  68. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf/version.py +0 -0
  69. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.egg-info/SOURCES.txt +0 -0
  70. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.egg-info/dependency_links.txt +0 -0
  71. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.egg-info/entry_points.txt +0 -0
  72. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.egg-info/requires.txt +0 -0
  73. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.egg-info/top_level.txt +0 -0
  74. {remoterf-2.2.3 → remoterf-2.2.5}/src/remoterf.py +0 -0
  75. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_banner.py +0 -0
  76. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_config_address.py +0 -0
  77. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_deployment_accounts.py +0 -0
  78. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_direct_path_fixed.py +0 -0
  79. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_direct_path_partner.py +0 -0
  80. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_direct_path_race.py +0 -0
  81. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_dynamic_device_codegen.py +0 -0
  82. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_dynamic_v2.py +0 -0
  83. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_federated_devices.py +0 -0
  84. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_fetch_idl_fallback.py +0 -0
  85. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_finish_prompt.py +0 -0
  86. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_gate_i_direct_path.py +0 -0
  87. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_grpc_client_target.py +0 -0
  88. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_native_account_credentials.py +0 -0
  89. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_native_devices.py +0 -0
  90. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_partner_dynamic_v2.py +0 -0
  91. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_pluto_virtual.py +0 -0
  92. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_process_arg.py +0 -0
  93. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_remoterf_cli.py +0 -0
  94. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_ti_mmwave.py +0 -0
  95. {remoterf-2.2.3 → remoterf-2.2.5}/tests/test_upgrade_rescue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoterf"
7
- version = "2.2.3"
7
+ version = "2.2.5"
8
8
  description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -116,7 +116,7 @@ def _password_confirmation(*, enforce_policy=False):
116
116
 
117
117
  # The code typed at `remoterf -r`, used once for this registration.
118
118
  pending_enrollment_code = None
119
- # A username already given (found on Global): the login asks only the password.
119
+ # A username already given (`name@lab` at `-l`): the login asks only the password.
120
120
  pending_username = None
121
121
  # The saved home this shell is attached to (None on a bare direct profile).
122
122
  home_name = None
@@ -209,6 +209,9 @@ def _account_command(command):
209
209
  account.email = _ask('Email') if policy['email_required'] else ''
210
210
  if not account.create_user():
211
211
  raise AccountBackendError('Registration failed')
212
+ if account.is_https_home:
213
+ from ..deployment import homes
214
+ printf(f"Log in from anywhere as {account.username}@{homes.GLOBAL_ACCOUNTS}", Sty.GRAY)
212
215
  except BaseException:
213
216
  # The code was fine; only the details failed. Keep it for the retry.
214
217
  pending_enrollment_code = account.enrollment_code or None
@@ -252,8 +255,13 @@ def _account_command(command):
252
255
  if command in ('l', 'login'):
253
256
  printf(f"Login to {_home_name()}", (Sty.BOLD, Sty.BLUE))
254
257
  global pending_username
255
- account.username = pending_username or _ask('Username or Email' if account.is_https_home else 'Username')
258
+ from ..deployment import homes
259
+ typed = pending_username or _ask('Username or Email' if account.is_https_home else 'Username')
256
260
  pending_username = None
261
+ # `alice@ucla` typed at ucla is `alice` there.
262
+ name, where = homes.split_account(typed)
263
+ here = homes.GLOBAL_ACCOUNTS if account.is_https_home else home_name
264
+ account.username = name if where == here else typed
257
265
  account.password = _ask('Password (Hidden)', hidden=True)
258
266
  ok = bool(account.login_user(remember=not account.is_https_home))
259
267
  if ok:
@@ -1620,9 +1628,12 @@ def run(backend=None, *, register=False, enrollment_code=None, server_label=None
1620
1628
  # Continue below the banner rather than wiping the screen: with a
1621
1629
  # stored login the wipe came a split second after everything appeared.
1622
1630
  session_line()
1631
+ from ..deployment import homes
1632
+ # The prompt is the account's full name: alice@ucla, alice@global.
1633
+ where = homes.GLOBAL_ACCOUNTS if account.is_https_home else (home_name or 'remoterf')
1623
1634
  while True:
1624
1635
  try:
1625
- inpu = session.prompt(stylize(f'{account.username}@remoterf: ', Sty.BOLD))
1636
+ inpu = session.prompt(stylize(f'{account.username}@{where}: ', Sty.BOLD))
1626
1637
  if inpu in ('register', 'verify', 'login', 'forgot-password', 'reset-password'):
1627
1638
  authenticated = _account_command(inpu)
1628
1639
  if inpu == 'reset-password' and not welcome(show_banner=False):
@@ -268,25 +268,18 @@ def connect(name: str) -> dict:
268
268
  return route
269
269
 
270
270
 
271
- # Global's own account home, by its name on Global's broker.
272
- GLOBAL_HOME = "home"
273
-
274
-
275
- def lookup_username(global_origin: str, username: str) -> str | None:
276
- """The deployment that holds this username on Global, or None."""
277
- from .http import AccountBackendError, JsonTransport
278
-
279
- transport = JsonTransport(global_origin)
280
- try:
281
- return transport.request(
282
- "POST", "/v2/broker/usernames/lookup", data={"username": username}
283
- )["deployment"]
284
- except AccountBackendError as exc:
285
- if exc.code == "not_found":
286
- return None
287
- raise
288
- finally:
289
- transport.close()
271
+ # An account is `name@<deployment>`; Global's own accounts are `name@global`.
272
+ GLOBAL_ACCOUNTS = "global"
273
+
274
+
275
+ def split_account(text: str) -> tuple[str, str | None]:
276
+ """`alice@ucla` -> ("alice", "ucla"). A bare name, or an email (a dot
277
+ after the @; deployment names never have one), names no deployment."""
278
+ text = text.strip()
279
+ name, at, where = text.rpartition("@")
280
+ if not at or not name or not where or "." in where:
281
+ return text, None
282
+ return name, where.lower()
290
283
 
291
284
 
292
285
  def register_home(host: str) -> tuple[str, dict, dict]:
@@ -150,7 +150,7 @@ def print_help() -> None:
150
150
  printf(" remoterf -h | --help", Sty.CYAN, " Show this help", Sty.DEFAULT)
151
151
  print()
152
152
  printf("Commands:", (Sty.BOLD, Sty.MAGENTA))
153
- printf(" remoterf -l | --login [target]", Sty.CYAN, " Login: last used, 'global', or a home name", Sty.DEFAULT)
153
+ printf(" remoterf -l | --login [target]", Sty.CYAN, " Login: last used, 'global', a home name, or an account (username@lab)", Sty.DEFAULT)
154
154
  printf(" remoterf -r | --register [global]", Sty.CYAN, " Register with an enrollment code (or a RemoteRF Global account)", Sty.DEFAULT)
155
155
  printf(" remoterf homes", Sty.CYAN, " List saved deployment homes", Sty.DEFAULT)
156
156
  printf(" remoterf -v | --version", Sty.CYAN, " Print version", Sty.DEFAULT)
@@ -171,6 +171,7 @@ def print_help() -> None:
171
171
  printf(" remoterf --login", Sty.GREEN)
172
172
  printf(" remoterf --login ucla", Sty.GREEN)
173
173
  printf(" remoterf --login global", Sty.GREEN)
174
+ printf(" remoterf --login jdoe@ucla", Sty.GREEN)
174
175
  printf(" remoterf homes", Sty.GREEN)
175
176
  printf(" remoterf --version", Sty.GREEN)
176
177
  printf(" remoterf --config --addr 203.0.113.10:12321", Sty.GREEN)
@@ -295,7 +296,8 @@ def _reason(exc: BaseException) -> str:
295
296
  return str(exc)
296
297
 
297
298
 
298
- def _use_home(name: str, *, register: bool, enrollment_code: str | None = None) -> int:
299
+ def _use_home(name: str, *, register: bool, enrollment_code: str | None = None,
300
+ username: str | None = None) -> int:
299
301
  from remoterf.deployment import homes
300
302
 
301
303
  try:
@@ -311,6 +313,7 @@ def _use_home(name: str, *, register: bool, enrollment_code: str | None = None)
311
313
  server_label=f'{name} ({_route_label(route)})',
312
314
  home=name,
313
315
  route={**route, 'home': home},
316
+ username=username,
314
317
  )
315
318
 
316
319
 
@@ -384,6 +387,8 @@ def _login(target: str | None) -> int:
384
387
  if not require_tos_for_login():
385
388
  return 1
386
389
  homes.migrate_legacy_target()
390
+ if target is not None and '@' in target:
391
+ return _login_as(target)
387
392
  if target is None:
388
393
  last = homes.last_target()
389
394
  saved = sorted(homes.homes())
@@ -412,30 +417,28 @@ def _login(target: str | None) -> int:
412
417
 
413
418
 
414
419
  def _login_by_username() -> int:
415
- """A fresh install: Global knows which lab holds a username, so the
416
- account is found by name. Enter registers, as `-l` here always did."""
417
- from remoterf.deployment import homes
418
-
420
+ """A fresh install: an account says where it lives (`alice@ucla`), so
421
+ asking for it is enough. Enter registers, as `-l` here always did."""
419
422
  try:
420
- name = input('Username (Enter to register): ').strip()
423
+ name = input('Account (username@lab, Enter to register): ').strip()
421
424
  except (EOFError, KeyboardInterrupt):
422
425
  print()
423
426
  return 1
424
427
  if not name:
425
428
  return _register()
426
- try:
427
- with _working("Looking up your account…"):
428
- deployment = homes.lookup_username(_global_origin(), name)
429
- except (RuntimeError, ValueError) as exc:
430
- printf(f'Could not reach RemoteRF Global: {_reason(exc)}', Sty.WARNING)
431
- return 2
432
- if deployment is None:
433
- printf(f'No RemoteRF account named {name!r} was found.', Sty.WARNING)
434
- printf('Run: ', Sty.GRAY, 'remoterf --register', Sty.CYAN)
435
- return 2
436
- if deployment == homes.GLOBAL_HOME:
429
+ return _login_as(name)
430
+
431
+
432
+ def _login_as(account: str) -> int:
433
+ """Log in to `name@lab` at that lab; a bare name or an email is Global."""
434
+ from remoterf.deployment import homes
435
+
436
+ name, lab = homes.split_account(account)
437
+ if lab is None or lab == homes.GLOBAL_ACCOUNTS:
437
438
  return _use_global(register=False, username=name)
438
- host = f'{deployment}.{homes.GLOBAL_DOMAIN}'
439
+ if lab in homes.homes():
440
+ return _use_home(lab, register=False, username=name)
441
+ host = f'{lab}.{homes.GLOBAL_DOMAIN}'
439
442
  try:
440
443
  with _working(f"Looking up {host}…"):
441
444
  lab, home, route = homes.register_home(host)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -19,63 +19,88 @@ def test_login_on_a_fresh_install_registers_on_enter():
19
19
  with tmp, env, \
20
20
  mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
21
21
  mock.patch("builtins.input", return_value=""), \
22
- mock.patch("remoterf.deployment.homes.lookup_username") as lookup, \
23
22
  mock.patch.object(remoterf_cli, "_register", return_value=0) as register, \
24
23
  mock.patch.object(remoterf_cli, "_account_shell") as shell:
25
24
  assert remoterf_cli._login(None) == 0
26
25
  register.assert_called_once_with()
27
26
  shell.assert_not_called()
28
- lookup.assert_not_called()
29
27
 
30
28
 
31
- def test_login_on_a_fresh_install_finds_the_account_by_username():
29
+ def test_an_account_name_says_where_it_lives():
30
+ from remoterf.deployment.homes import split_account
31
+
32
+ assert split_account(" jdoe@ucla ") == ("jdoe", "ucla")
33
+ assert split_account("JDoe@UCLA") == ("JDoe", "ucla")
34
+ assert split_account("jdoe@global") == ("jdoe", "global")
35
+ assert split_account("jdoe") == ("jdoe", None)
36
+ assert split_account("jdoe@ucla.edu") == ("jdoe@ucla.edu", None) # an email
37
+ assert split_account("a@b@ucla") == ("a@b", "ucla")
38
+ assert split_account("@ucla") == ("@ucla", None)
39
+ assert split_account("jdoe@") == ("jdoe@", None)
40
+
41
+
42
+ def test_login_on_a_fresh_install_goes_to_the_lab_in_the_account_name():
32
43
  from remoterf import remoterf_cli
33
44
 
34
45
  tmp, env = _fresh_home()
35
46
  route = {"kind": "relay", "host": "ucla.global.remoterf.net", "port": 12321}
36
47
  with tmp, env, \
37
48
  mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
38
- mock.patch("builtins.input", return_value="ian"), \
39
- mock.patch("remoterf.deployment.homes.lookup_username", return_value="ucla") as lookup, \
49
+ mock.patch("builtins.input", return_value="jdoe@ucla"), \
40
50
  mock.patch("remoterf.deployment.homes.register_home",
41
51
  return_value=("ucla", {"display_name": "UCLA"}, route)) as register_home, \
42
52
  mock.patch.object(remoterf_cli, "_account_shell", return_value=0) as shell:
43
53
  assert remoterf_cli._login(None) == 0
44
- assert lookup.call_args.args[1] == "ian"
45
54
  register_home.assert_called_once_with("ucla.global.remoterf.net")
46
55
  assert shell.call_args.kwargs["register"] is False
47
56
  assert shell.call_args.kwargs["home"] == "ucla"
48
- assert shell.call_args.kwargs["username"] == "ian"
57
+ assert shell.call_args.kwargs["username"] == "jdoe"
49
58
 
50
59
 
51
- def test_login_on_a_fresh_install_finds_a_global_account():
60
+ def test_login_on_a_fresh_install_global_bare_names_and_emails_go_to_global():
52
61
  from remoterf import remoterf_cli
53
62
 
54
- tmp, env = _fresh_home()
55
- with tmp, env, \
56
- mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
57
- mock.patch("builtins.input", return_value="ian"), \
58
- mock.patch("remoterf.deployment.homes.lookup_username", return_value="home"), \
59
- mock.patch.object(remoterf_cli, "_use_global", return_value=0) as use_global:
60
- assert remoterf_cli._login(None) == 0
61
- use_global.assert_called_once_with(register=False, username="ian")
63
+ for typed, username in (("jdoe@global", "jdoe"), ("jdoe", "jdoe"), ("jdoe@ucla.edu", "jdoe@ucla.edu")):
64
+ tmp, env = _fresh_home()
65
+ with tmp, env, \
66
+ mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
67
+ mock.patch("builtins.input", return_value=typed), \
68
+ mock.patch("remoterf.deployment.homes.register_home") as register_home, \
69
+ mock.patch.object(remoterf_cli, "_use_global", return_value=0) as use_global:
70
+ assert remoterf_cli._login(None) == 0
71
+ use_global.assert_called_once_with(register=False, username=username)
72
+ register_home.assert_not_called()
62
73
 
63
74
 
64
- def test_login_on_a_fresh_install_with_an_unknown_username():
75
+ def test_login_to_a_lab_that_does_not_answer():
65
76
  from remoterf import remoterf_cli
66
77
 
67
78
  tmp, env = _fresh_home()
68
79
  with tmp, env, \
69
80
  mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
70
- mock.patch("builtins.input", return_value="nobody"), \
71
- mock.patch("remoterf.deployment.homes.lookup_username", return_value=None), \
72
- mock.patch.object(remoterf_cli, "_register") as register, \
81
+ mock.patch("builtins.input", return_value="jdoe@nolab"), \
82
+ mock.patch("remoterf.deployment.homes.register_home",
83
+ side_effect=RuntimeError("no such deployment")) as register_home, \
73
84
  mock.patch.object(remoterf_cli, "_account_shell") as shell:
74
85
  assert remoterf_cli._login(None) == 2
75
- register.assert_not_called()
86
+ register_home.assert_called_once_with("nolab.global.remoterf.net")
76
87
  shell.assert_not_called()
77
88
 
78
89
 
90
+ def test_login_with_an_account_uses_the_saved_home():
91
+ from remoterf import remoterf_cli
92
+
93
+ tmp, env = _fresh_home()
94
+ with tmp, env, \
95
+ mock.patch("remoterf.config.config.require_tos_for_login", return_value=True), \
96
+ mock.patch("remoterf.deployment.homes.homes", return_value={"ucla": {}}), \
97
+ mock.patch("remoterf.deployment.homes.register_home") as register_home, \
98
+ mock.patch.object(remoterf_cli, "_use_home", return_value=0) as use_home:
99
+ assert remoterf_cli._login("jdoe@ucla") == 0
100
+ use_home.assert_called_once_with("ucla", register=False, username="jdoe")
101
+ register_home.assert_not_called()
102
+
103
+
79
104
  def test_login_global_on_a_fresh_install_logs_in_to_global():
80
105
  from remoterf import remoterf_cli
81
106
 
@@ -144,19 +169,35 @@ def test_register_global_asks_for_the_tos_like_register(monkeypatch):
144
169
  use_global.assert_not_called()
145
170
 
146
171
 
147
- def test_a_found_username_is_not_asked_again():
172
+ def _login_at(home, typed, *, https=False, pending=None):
148
173
  from remoterf.core import app
149
174
 
150
- account = mock.Mock(is_https_home=False, backend=None)
175
+ account = mock.Mock(is_https_home=https, backend=None)
151
176
  account.login_user.return_value = True
152
177
  with mock.patch.object(app, "account", account), \
153
- mock.patch.object(app, "pending_username", "ian"), \
178
+ mock.patch.object(app, "home_name", home), \
179
+ mock.patch.object(app, "pending_username", pending), \
154
180
  mock.patch.object(app, "local_capabilities",
155
181
  return_value={"registration_policy": {}}), \
156
- mock.patch.object(app, "_home_name", return_value="ucla"), \
182
+ mock.patch.object(app, "_home_name", return_value=home), \
157
183
  mock.patch.object(app, "_remember_login"), \
158
- mock.patch.object(app, "_ask", return_value="secret") as ask:
184
+ mock.patch.object(app, "_ask", side_effect=[typed, "secret"] if typed else ["secret"]) as ask:
185
+ if https:
186
+ account.backend = mock.Mock(capabilities={"registration_policy": {}})
159
187
  assert app._account_command("login") is True
160
188
  assert app.pending_username is None
161
- assert account.username == "ian"
189
+ return account.username, ask
190
+
191
+
192
+ def test_a_found_username_is_not_asked_again():
193
+ username, ask = _login_at("ucla", None, pending="jdoe")
194
+ assert username == "jdoe"
162
195
  ask.assert_called_once_with("Password (Hidden)", hidden=True)
196
+
197
+
198
+ def test_the_full_account_name_works_at_its_own_home_only():
199
+ assert _login_at("ucla", "jdoe@ucla")[0] == "jdoe"
200
+ assert _login_at("ucla", "jdoe")[0] == "jdoe"
201
+ assert _login_at("otheruni", "jdoe@ucla")[0] == "jdoe@ucla" # not this lab's: no such name here
202
+ assert _login_at(None, "jdoe@global", https=True)[0] == "jdoe"
203
+ assert _login_at(None, "jdoe@ucla.edu", https=True)[0] == "jdoe@ucla.edu" # an email
@@ -444,7 +444,7 @@ def test_every_command_after_login_goes_native_to_the_home(reachable, monkeypatc
444
444
  return response
445
445
 
446
446
  monkeypatch.setattr(grpc_acc, "rpc_client", rpc_client)
447
- account = grpc_acc.RemoteRFAccount("ethan", "secret")
447
+ account = grpc_acc.RemoteRFAccount("jdoe", "secret")
448
448
  # A deployment-native account is not an HTTPS home: no Global in the path.
449
449
  assert not account.is_https_home
450
450
 
@@ -466,7 +466,7 @@ def test_every_command_after_login_goes_native_to_the_home(reachable, monkeypatc
466
466
  assert token == "reservation-token"
467
467
  # Credentials go to the HOME itself, never through any broker.
468
468
  for _, args in sent:
469
- assert args["un"].string_value == "ethan"
469
+ assert args["un"].string_value == "jdoe"
470
470
 
471
471
  # And the transport those calls ride is this HOME's own direct channel.
472
472
  monkeypatch.setattr("grpc.secure_channel", lambda *a, **k: mock.Mock())
@@ -626,7 +626,7 @@ def test_reserve_shows_the_token_even_when_the_driver_fetch_fails(monkeypatch, c
626
626
 
627
627
  monkeypatch.setattr(grpc_acc, "rpc_client", rpc_client)
628
628
  monkeypatch.setattr("remoterf.drivers.dynamic_device.install_driver", install_driver)
629
- account = grpc_acc.RemoteRFAccount("ethan", "secret")
629
+ account = grpc_acc.RemoteRFAccount("jdoe", "secret")
630
630
  now = datetime.datetime.now()
631
631
 
632
632
  assert account.reserve_device(0, now, now + datetime.timedelta(hours=1)) == "reservation-token"
File without changes
File without changes
File without changes
File without changes
File without changes