Skip to content

Commit 6d1dcbd

Browse files
committed
Fix ssh and gdrive tests.
1 parent c1d1c65 commit 6d1dcbd

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
@@ -66,38 +66,8 @@ jobs:
6666
python -m pip install --upgrade pip
6767
pip install .[dev]
6868
69-
# run SSH tests only on Linux because Windows and macOS
70-
# are already run within a virtual container and so cannot
71-
# run Linux containers because nested containerisation is disabled.
72-
# - name: Test SSH (Linux only)
73-
# if: runner.os == 'Linux'
74-
# run: |
75-
# sudo service mysql stop # free up port 3306 for ssh tests
76-
# pytest tests/tests_transfers/ssh
77-
#
78-
# - name: Test Google Drive
79-
# env:
80-
# GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
81-
# GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
82-
# GDRIVE_ROOT_FOLDER_ID: ${{ secrets.GDRIVE_ROOT_FOLDER_ID }}
83-
# GDRIVE_CONFIG_TOKEN: ${{ secrets.GDRIVE_CONFIG_TOKEN }}
84-
# run: |
85-
# pytest tests/tests_transfers/gdrive
86-
87-
# - name: Test AWS
88-
# env:
89-
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
90-
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
91-
# AWS_REGION: ${{ secrets.AWS_REGION }}
92-
# AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
93-
# run: |
94-
# pytest tests/tests_transfers/aws
95-
96-
# - name: All Other Tests
97-
# run: |
98-
# pytest --ignore=tests/tests_transfers/ssh --ignore=tests/tests_transfers/gdrive --ignore=tests/tests_transfers/aws
99-
10069
- name: Install pass on Linux
70+
# this is required for Rclone config encryption
10171
if: runner.os == 'Linux'
10272
run: |
10373
set -euo pipefail
@@ -116,16 +86,42 @@ jobs:
11686
FPR="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')"
11787
pass init "$FPR"
11888
119-
# (Optional) smoke test: ensure pass can encrypt
120-
printf '%s\n' "$(openssl rand -base64 16)" | pass insert -m -f ci/smoke-test
89+
90+
# run SSH tests only on Linux because Windows and macOS
91+
# are already run within a virtual container and so cannot
92+
# run Linux containers because nested containerisation is disabled.
93+
- name: Test SSH (Linux only)
94+
if: runner.os == 'Linux'
95+
run: |
96+
sudo service mysql stop # free up port 3306 for ssh tests
97+
pytest tests/tests_transfers/ssh
98+
99+
- name: Test Google Drive
100+
env:
101+
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
102+
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
103+
GDRIVE_ROOT_FOLDER_ID: ${{ secrets.GDRIVE_ROOT_FOLDER_ID }}
104+
GDRIVE_CONFIG_TOKEN: ${{ secrets.GDRIVE_CONFIG_TOKEN }}
105+
run: |
106+
pytest tests/tests_transfers/gdrive
107+
108+
# - name: Test AWS
109+
# env:
110+
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
111+
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
112+
# AWS_REGION: ${{ secrets.AWS_REGION }}
113+
# AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
114+
# run: |
115+
# pytest tests/tests_transfers/aws
116+
117+
# - name: All Other Tests
118+
# run: |
119+
# pytest --ignore=tests/tests_transfers/ssh --ignore=tests/tests_transfers/gdrive --ignore=tests/tests_transfers/aws
121120

122121
- name: RClone Encryption
123122
run: |
124-
set -euo pipefail
125-
# GNUPGHOME is available here because we wrote it to $GITHUB_ENV
126123
pytest -k test_rclone_encryption
127124
128-
129125
build_sdist_wheels:
130126
name: Build source distribution
131127
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:
@@ -568,11 +568,13 @@ def get_rclone_message_for_gdrive_without_browser(
568568
) -> InterfaceOutput:
569569
"""Get the rclone message for Google Drive setup without a browser."""
570570
try:
571-
output = gdrive.preliminary_for_setup_without_browser(
572-
self.project.cfg,
573-
gdrive_client_secret,
574-
self.project.cfg.rclone.get_rclone_config_name("gdrive"),
575-
log=False,
571+
output = (
572+
rclone.preliminary_setup_gdrive_config_for_without_browser(
573+
self.project.cfg,
574+
gdrive_client_secret,
575+
self.project.cfg.rclone.get_rclone_config_name("gdrive"),
576+
log=False,
577+
)
576578
)
577579
return True, output
578580
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.
@@ -294,6 +296,7 @@ async def setup_gdrive_connection_and_update_ui(
294296
# This function is called from different screens that
295297
# contain different widgets. Therefore, remove all possible
296298
# widgets that may / may not be present on the previous screen.
299+
self.show_encryption_screen()
297300
for id in [
298301
"#setup_gdrive_cancel_button",
299302
"#setup_gdrive_generic_input_box",
@@ -336,6 +339,18 @@ def setup_gdrive_connection(
336339
# Set encryption on RClone config
337340
# ----------------------------------------------------------------------------------
338341

342+
def show_encryption_screen(self):
343+
"""Show the screen asking the user whether to encrypt the Rclone password."""
344+
message = f"{rclone_encryption.get_explanation_message(self.interface.project.cfg)}"
345+
self.update_message_box_message(message)
346+
347+
yes_button = Button("Yes", id="setup_gdrive_set_encryption_yes_button")
348+
no_button = Button("No", id="setup_gdrive_set_encryption_no_button")
349+
350+
self.query_one("#setup_gdrive_buttons_horizontal").mount(
351+
yes_button, no_button
352+
)
353+
339354
def set_rclone_encryption(self):
340355
"""Try and encrypt the Rclone config file and inform the user of success / failure."""
341356
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"
@@ -370,6 +371,71 @@ def setup_rclone_config_for_gdrive(
370371
return process
371372

372373

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