Skip to content

Commit ed42279

Browse files
committed
Fix ssh and gdrive tests.
1 parent 525a9b5 commit ed42279

File tree

14 files changed

+255
-175
lines changed

14 files changed

+255
-175
lines changed

.github/workflows/code_test_and_deploy.yml

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -59,38 +59,8 @@ jobs:
5959
python -m pip install --upgrade pip
6060
pip install .[dev]
6161
62-
# run SSH tests only on Linux because Windows and macOS
63-
# are already run within a virtual container and so cannot
64-
# run Linux containers because nested containerisation is disabled.
65-
# - name: Test SSH (Linux only)
66-
# if: runner.os == 'Linux'
67-
# run: |
68-
# sudo service mysql stop # free up port 3306 for ssh tests
69-
# pytest tests/tests_transfers/ssh
70-
#
71-
# - name: Test Google Drive
72-
# env:
73-
# GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
74-
# GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
75-
# GDRIVE_ROOT_FOLDER_ID: ${{ secrets.GDRIVE_ROOT_FOLDER_ID }}
76-
# GDRIVE_CONFIG_TOKEN: ${{ secrets.GDRIVE_CONFIG_TOKEN }}
77-
# run: |
78-
# pytest tests/tests_transfers/gdrive
79-
80-
# - name: Test AWS
81-
# env:
82-
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
83-
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
84-
# AWS_REGION: ${{ secrets.AWS_REGION }}
85-
# AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
86-
# run: |
87-
# pytest tests/tests_transfers/aws
88-
89-
# - name: All Other Tests
90-
# run: |
91-
# pytest --ignore=tests/tests_transfers/ssh --ignore=tests/tests_transfers/gdrive --ignore=tests/tests_transfers/aws
92-
9362
- name: Install pass on Linux
63+
# this is required for Rclone config encryption
9464
if: runner.os == 'Linux'
9565
run: |
9666
set -euo pipefail
@@ -109,16 +79,42 @@ jobs:
10979
FPR="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')"
11080
pass init "$FPR"
11181
112-
# (Optional) smoke test: ensure pass can encrypt
113-
printf '%s\n' "$(openssl rand -base64 16)" | pass insert -m -f ci/smoke-test
82+
83+
# run SSH tests only on Linux because Windows and macOS
84+
# are already run within a virtual container and so cannot
85+
# run Linux containers because nested containerisation is disabled.
86+
- name: Test SSH (Linux only)
87+
if: runner.os == 'Linux'
88+
run: |
89+
sudo service mysql stop # free up port 3306 for ssh tests
90+
pytest tests/tests_transfers/ssh
91+
92+
- name: Test Google Drive
93+
env:
94+
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
95+
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
96+
GDRIVE_ROOT_FOLDER_ID: ${{ secrets.GDRIVE_ROOT_FOLDER_ID }}
97+
GDRIVE_CONFIG_TOKEN: ${{ secrets.GDRIVE_CONFIG_TOKEN }}
98+
run: |
99+
pytest tests/tests_transfers/gdrive
100+
101+
# - name: Test AWS
102+
# env:
103+
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
104+
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
105+
# AWS_REGION: ${{ secrets.AWS_REGION }}
106+
# AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
107+
# run: |
108+
# pytest tests/tests_transfers/aws
109+
110+
# - name: All Other Tests
111+
# run: |
112+
# pytest --ignore=tests/tests_transfers/ssh --ignore=tests/tests_transfers/gdrive --ignore=tests/tests_transfers/aws
114113

115114
- name: RClone Encryption
116115
run: |
117-
set -euo pipefail
118-
# GNUPGHOME is available here because we wrote it to $GITHUB_ENV
119116
pytest -k test_rclone_encryption
120117
121-
122118
build_sdist_wheels:
123119
name: Build source distribution
124120
needs: [test]

datashuttle/tui/interface.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from datashuttle import DataShuttle
1515
from datashuttle.configs import load_configs
16-
from datashuttle.utils import aws, gdrive, rclone, ssh, utils
16+
from datashuttle.utils import aws, rclone, ssh, utils
1717

1818

1919
class Interface:
@@ -552,11 +552,13 @@ def get_rclone_message_for_gdrive_without_browser(
552552
) -> InterfaceOutput:
553553
"""Get the rclone message for Google Drive setup without a browser."""
554554
try:
555-
output = gdrive.preliminary_for_setup_without_browser(
556-
self.project.cfg,
557-
gdrive_client_secret,
558-
self.project.cfg.rclone.get_rclone_config_name("gdrive"),
559-
log=False,
555+
output = (
556+
rclone.preliminary_setup_gdrive_config_for_without_browser(
557+
self.project.cfg,
558+
gdrive_client_secret,
559+
self.project.cfg.rclone.get_rclone_config_name("gdrive"),
560+
log=False,
561+
)
560562
)
561563
return True, output
562564
except BaseException as e:

datashuttle/tui/screens/setup_gdrive.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
Static,
2222
)
2323

24+
from datashuttle.utils import rclone_encryption
25+
2426

2527
class SetupGdriveScreen(ModalScreen):
2628
"""Dialog window that sets up a Google Drive connection.
@@ -293,6 +295,7 @@ async def setup_gdrive_connection_and_update_ui(
293295
# This function is called from different screens that
294296
# contain different widgets. Therefore remove all possible
295297
# widgets that may / may not be present on the previous screen.
298+
self.show_encryption_screen()
296299
for id in [
297300
"#setup_gdrive_cancel_button",
298301
"#setup_gdrive_generic_input_box",
@@ -335,6 +338,18 @@ def setup_gdrive_connection(
335338
# Set encryption on RClone config
336339
# ----------------------------------------------------------------------------------
337340

341+
def show_encryption_screen(self):
342+
"""Show the screen asking the user whether to encrypt the Rclone password."""
343+
message = f"{rclone_encryption.get_explanation_message(self.interface.project.cfg)}"
344+
self.update_message_box_message(message)
345+
346+
yes_button = Button("Yes", id="setup_gdrive_set_encryption_yes_button")
347+
no_button = Button("No", id="setup_gdrive_set_encryption_no_button")
348+
349+
self.query_one("#setup_gdrive_buttons_horizontal").mount(
350+
yes_button, no_button
351+
)
352+
338353
def set_rclone_encryption(self):
339354
"""Try and encrypt the Rclone config file and inform the user of success / failure."""
340355
success, output = self.interface.try_setup_rclone_encryption()

datashuttle/utils/gdrive.py

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,12 @@
11
from __future__ import annotations
22

3-
import json
43
from typing import TYPE_CHECKING
54

65
if TYPE_CHECKING:
76
from datashuttle.configs.config_class import Configs
87

98
from datashuttle.utils import rclone, utils
109

11-
# -----------------------------------------------------------------------------
12-
# Helper Functions
13-
# -----------------------------------------------------------------------------
14-
15-
# These functions are used by both API and TUI for setting up connections to google drive.
16-
17-
18-
def preliminary_for_setup_without_browser(
19-
cfg: Configs,
20-
gdrive_client_secret: str | None,
21-
rclone_config_name: str,
22-
log: bool = True,
23-
) -> str:
24-
"""Prepare rclone configuration for Google Drive without using a browser.
25-
26-
This function prepares the rclone configuration for Google Drive without using a browser.
27-
28-
The `config_is_local=false` flag tells rclone that the configuration process is being run
29-
on a headless machine which does not have access to a browser.
30-
31-
The `--non-interactive` flag is used to control rclone's behaviour while running it through
32-
external applications. An `rclone config create` command would assume default values for config
33-
variables in an interactive mode. If the `--non-interactive` flag is provided and rclone needs
34-
the user to input some detail, a JSON blob will be returned with the question in it. For this
35-
particular setup, rclone outputs a command for user to run on a machine with a browser.
36-
37-
This function runs `rclone config create` with the user credentials and returns the rclone's output info.
38-
This output info is presented to the user while asking for a `config_token`.
39-
40-
Next, the user will run rclone's given command, authenticate with google drive and input the
41-
config token given by rclone for datashuttle to proceed with the setup.
42-
"""
43-
client_id_key_value = (
44-
f"client_id {cfg['gdrive_client_id']} "
45-
if cfg["gdrive_client_id"]
46-
else " "
47-
)
48-
client_secret_key_value = (
49-
f"client_secret {gdrive_client_secret} "
50-
if gdrive_client_secret
51-
else ""
52-
)
53-
output = rclone.call_rclone(
54-
f"config create "
55-
f"{rclone_config_name} "
56-
f"drive "
57-
f"{client_id_key_value}"
58-
f"{client_secret_key_value}"
59-
f"scope drive "
60-
f"root_folder_id {cfg['gdrive_root_folder_id']} "
61-
f"config_is_local=false "
62-
f"--non-interactive",
63-
pipe_std=True,
64-
)
65-
66-
# Extracting rclone's message from the json
67-
output_json = json.loads(output.stdout)
68-
message = output_json["Option"]["Help"]
69-
70-
if log:
71-
utils.log(message)
72-
73-
return message
74-
75-
7610
# -----------------------------------------------------------------------------
7711
# Python API
7812
# -----------------------------------------------------------------------------
@@ -108,7 +42,7 @@ def prompt_and_get_config_token(
10842
with google drive and input the `config_token` generated by rclone. The `config_token` is
10943
then used to complete rclone's config setup for google drive.
11044
"""
111-
message = preliminary_for_setup_without_browser(
45+
message = rclone.preliminary_setup_gdrive_config_for_without_browser(
11246
cfg, gdrive_client_secret, rclone_config_name, log=log
11347
)
11448
input_ = utils.get_user_input(

datashuttle/utils/rclone.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from datashuttle.configs.config_class import Configs
1010
from datashuttle.utils.custom_types import TopLevelFolder
1111

12+
import json
1213
import os
1314
import platform
1415
import shlex
@@ -158,7 +159,7 @@ def await_call_rclone_with_popen_for_central_connection_raise_on_fail(
158159
lambda_func = lambda: process.communicate()
159160

160161
stdout, stderr = run_function_that_requires_encrypted_rclone_config_access(
161-
cfg, lambda_func
162+
cfg, lambda_func, check_config_exists=False
162163
)
163164

164165
if process.returncode != 0:
@@ -171,7 +172,7 @@ def await_call_rclone_with_popen_for_central_connection_raise_on_fail(
171172

172173

173174
def run_function_that_requires_encrypted_rclone_config_access(
174-
cfg, lambda_func
175+
cfg, lambda_func, check_config_exists: bool = True
175176
) -> Any:
176177
"""Run command that requires possibly encrypted Rclone config file.
177178
@@ -185,7 +186,7 @@ def run_function_that_requires_encrypted_rclone_config_access(
185186
cfg.rclone.get_rclone_central_connection_config_filepath()
186187
)
187188

188-
if not rclone_config_filepath.is_file():
189+
if check_config_exists and not rclone_config_filepath.is_file():
189190
if version.parse(get_datashuttle_version()) <= version.parse("0.7.1"):
190191
raise RuntimeError(
191192
f"The way RClone configs are managed has changed since version v0.7.1\n"
@@ -371,6 +372,71 @@ def setup_rclone_config_for_gdrive(
371372
return process
372373

373374

375+
def preliminary_setup_gdrive_config_for_without_browser(
376+
cfg: Configs,
377+
gdrive_client_secret: str | None,
378+
rclone_config_name: str,
379+
log: bool = True,
380+
) -> str:
381+
"""Prepare rclone configuration for Google Drive without using a browser.
382+
383+
This function prepares the rclone configuration for Google Drive without using a browser.
384+
385+
The `config_is_local=false` flag tells rclone that the configuration process is being run
386+
on a headless machine which does not have access to a browser.
387+
388+
The `--non-interactive` flag is used to control rclone's behaviour while running it through
389+
external applications. An `rclone config create` command would assume default values for config
390+
variables in an interactive mode. If the `--non-interactive` flag is provided and rclone needs
391+
the user to input some detail, a JSON blob will be returned with the question in it. For this
392+
particular setup, rclone outputs a command for user to run on a machine with a browser.
393+
394+
This function runs `rclone config create` with the user credentials and returns the rclone's output info.
395+
This output info is presented to the user while asking for a `config_token`.
396+
397+
Next, the user will run rclone's given command, authenticate with google drive and input the
398+
config token given by rclone for datashuttle to proceed with the setup.
399+
"""
400+
client_id_key_value = (
401+
f"client_id {cfg['gdrive_client_id']} "
402+
if cfg["gdrive_client_id"]
403+
else " "
404+
)
405+
client_secret_key_value = (
406+
f"client_secret {gdrive_client_secret} "
407+
if gdrive_client_secret
408+
else ""
409+
)
410+
411+
cfg.rclone.delete_existing_rclone_config_file()
412+
413+
output = call_rclone(
414+
f"config create "
415+
f"{get_config_arg(cfg)} "
416+
f"{rclone_config_name} "
417+
f"drive "
418+
f"{client_id_key_value}"
419+
f"{client_secret_key_value}"
420+
f"scope drive "
421+
f"root_folder_id {cfg['gdrive_root_folder_id']} "
422+
f"config_is_local=false "
423+
f"--non-interactive",
424+
pipe_std=True,
425+
)
426+
427+
try:
428+
# Extracting rclone's message from the json
429+
output_json = json.loads(output.stdout)
430+
message = output_json["Option"]["Help"]
431+
except:
432+
assert False, f"{output.stderr}"
433+
434+
if log:
435+
utils.log(message)
436+
437+
return message
438+
439+
374440
def setup_rclone_config_for_aws(
375441
cfg: Configs,
376442
rclone_config_name: str,

datashuttle/utils/rclone_encryption.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def run_rclone_config_encrypt(cfg: Configs) -> None:
242242

243243

244244
def remove_rclone_encryption(cfg: Configs) -> None:
245-
"""Remove encryption from an Rclone config file.
245+
"""Remove encryption from a Rclone config file.
246246
247247
Set the credentials one last time to remove encryption from
248248
the RClone config file. Once removed, clean up the password
@@ -322,7 +322,7 @@ def get_windows_password_filepath(
322322
def get_explanation_message(
323323
cfg: Configs,
324324
) -> str:
325-
"""Explaining rclone's default credential storage and OS-specific encryption options.
325+
"""Explaining Rclone's default credential storage and OS-specific encryption options.
326326
327327
Displayed in both the Python API and the TUI.
328328
"""

0 commit comments

Comments
 (0)