"]
10 | license = "GNU"
11 |
12 | [tool.poetry.scripts]
13 | lobbyboy-server = 'lobbyboy.main:main'
14 | lobbyboy-config-example = 'lobbyboy.scripts:print_example_config'
15 |
16 | [tool.poetry.dependencies]
17 | python = "^3.7"
18 | paramiko = {extras = ["gssapi"], version = "^2.8.0"}
19 | python-digitalocean = "^1.17.0"
20 | toml = "^0.10.2"
21 | linode-api4 = "^5.2.1"
22 | pyvultr = "^0.1.5"
23 | pre-commit = "^2.16.0"
24 |
25 | [tool.poetry.dev-dependencies]
26 | black = "^21.9b0"
27 | bumpversion = "^0.6.0"
28 | pytest = "^6.2.5"
29 | flake8 = "^4.0.1"
30 | freezegun = "^1.1.0"
31 | pytest-cov = "^3.0.0"
32 |
33 | [tool.black]
34 | target-version = ["py37"]
35 | line-length = 120
36 |
37 | [tool.coverage.run]
38 | source = ["lobbyboy"]
39 |
40 | [tool.isort]
41 | profile = "black"
42 | atomic = true
43 |
44 | [tool.bandit]
45 | recursive = true
46 | # B108: Probable insecure usage of temp file/directory.
47 | # B602: Subprocess call with shell=True identified, security issue.
48 | # B404: Consider possible security implications associated with the subprocess module.
49 | # B603: Subprocess call - check for execution of untrusted input.
50 | # B607: Starting a process with a partial executable path.
51 | skips = ["B108", "B602", "B404", "B603", "B607"]
52 | assert_used.skips = ['tests/test_*.py']
53 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Lobbyboy
2 |
3 | > What is a lobby boy? A lobby boy is completely invisible, yet always in sight.
4 | > A lobby boy remembers what people hate. A lobby boy anticipates the client's
5 | > needs before the needs are needed. A lobby boy is, above all, discreet to a
6 | > fault.
7 | >
8 | > --The Grand Budapest Hotel
9 |
10 | [](https://results.pre-commit.ci/latest/github/lobbyboy-ssh/lobbyboy/main)
11 | [](https://github.com/lobbyboy-ssh/lobbyboy/actions/workflows/unittest.yaml)
12 |
13 |
14 |
15 | [](https://codecov.io/gh/lobbyboy-ssh/lobbyboy)
16 |
17 | **This project is still under testing, it worked but may have bugs.**
18 |
19 |
20 |
21 | * [What is lobbyboy?](#what-is-lobbyboy)
22 | * [Key Features](#key-features)
23 | * [Installation](#installation)
24 | * [Run server](#run-server)
25 | * [Generate a key pair for authentication](#generate-a-key-pair-for-authentication)
26 | * [Deployment](#deployment)
27 | * [Systemd Example](#systemd-example)
28 | * [Run in Docker](#run-in-docker)
29 | * [Providers](#providers)
30 | * [Builtin Providers](#builtin-providers)
31 | * [Vagrant Provider](#vagrant-provider)
32 | * [Footloose Provider](#footloose-provider)
33 | * [DigitalOcean Provider](#digitalocean-provider)
34 | * [Linode Provider](#linode-provider)
35 | * [Ignite(Firecracker) Provider](#ignitefirecracker-provider)
36 | * [Multipass Provider](#multipass-provider)
37 | * [Write Your Own Providers](#write-your-own-providers)
38 | * [Publish Your Own Providers](#publish-your-own-providers)
39 | * [FAQ](#faq)
40 | * [I Want to Know More!](#i-want-to-know-more)
41 |
42 |
43 |
44 | ## What is lobbyboy?
45 |
46 | Well, lobbyboy is a ssh server. Yes, like `sshd`. But instead of spawn a new
47 | shell on the server like sshd, when you ssh to lobbyboy, lobbyboy will create a
48 | new server(VPS) from available providers(meaning to say, DigitalOcean, AWS, GCP,
49 | Vultr, etc), then redirect you to the newly created servers. Of course, if
50 | lobbyboy finds any servers available already, he will just ask if you want to
51 | enter the existing server, or still want to create a new one.
52 |
53 | 
54 |
55 | ## Key Features
56 |
57 | - talks in SSH2 protocol, no need to install any software of configs for
58 | client-side, just ssh to lobbyboy!
59 | - extendable provider: just implement 3 methods, then lobbyboy can work with any
60 | provider!
61 | - destroy the server when you no longer needed.
62 | - manage ssh keys for you
63 |
64 | ## Installation
65 |
66 | Install libkrb5-dev first, this is a dependency for gssapi support.
67 |
68 | ```bash
69 | apt install libkrb5-dev
70 | ```
71 |
72 | Install via pip:
73 |
74 | ```bash
75 | pip install lobbyboy
76 | ```
77 |
78 | ## Run server
79 |
80 | First, generate a config file:
81 |
82 | ```bash
83 | lobbyboy-config-example > config.toml
84 | # Edit your config before running!
85 | ```
86 |
87 | Run the server with:
88 |
89 | ```bash
90 | lobbyboy-server -c config.toml
91 | ```
92 |
93 | You can ssh to Lobbyboy now, if you keep the default user `Gustave` in default
94 | config. You can ssh to Lobbyboy via:
95 |
96 | ```bash
97 | ssh Gustave@127.0.0.1 -p 12200
98 | # Enter the default password "Fiennes"(without quotes)
99 | Welcome to Lobbyboy 0.2.2!
100 | There are 1 available servers:
101 | 0 - Create a new server...
102 | 1 - Enter vagrant lobbyboy-41 127.0.0.1 (0 active sessions)
103 | Please input your choice (number):
104 | ```
105 |
106 | You may want to change the password in `config.toml` or use a public key for
107 | authentication. The latter is recommended in a production environment.
108 |
109 | ### Generate a key pair for authentication
110 |
111 | Generate a key pair:
112 |
113 | ```bash
114 | ssh-keygen -f lobbyboy_key
115 | ```
116 |
117 | Add the content of `lobbyboy_key.pub` to the end of `authorized_keys` under
118 | `[user.Gustave]` table. Now you can ssh to the lobbyboy server via:
119 |
120 | ```bash
121 | ssh Gustave@127.0.0.1 -i lobbyboy_key
122 | ```
123 |
124 | ## Deployment
125 |
126 | Lobbyboy is supposed to be a server daemon, so you can manage it by
127 | systemd/[supervisord](http://supervisord.org/) or put it into a docker.
128 |
129 | ### Systemd Example
130 |
131 | ```ini
132 | [Unit]
133 | Description=Lobbyboy Server
134 |
135 | [Service]
136 | User=me
137 | Group=me
138 | ExecStart=/path/to/lobbyboy-server -c /path/to/lobbyboy/config.toml
139 | Restart=on-failure
140 | WorkingDirectory=/path/to/lobbyboy/
141 |
142 | [Install]
143 | WantedBy=multi-user.target
144 | ```
145 |
146 | ### Run in Docker
147 |
148 | ```bash
149 | # Generate a config file
150 | docker run --rm ghcr.io/lobbyboy-ssh/lobbyboy lobbyboy-config-example > lobbyboy_config.toml
151 | # Run the docker container
152 | docker run -v `pwd`/lobbyboy_config.toml:/app/config.toml -p "12200:12200" -d ghcr.io/lobbyboy-ssh/lobbyboy
153 | ```
154 |
155 | The lobbyboy server should be active on 12200 port and you can connect to it
156 | with
157 |
158 | ```
159 | ssh Gustave@127.0.0.1 -p 12200
160 | ```
161 |
162 | The default password for user `Gustave` is `Fiennes`. **Please change it when
163 | you deployed it into production, and consider use ssh key to auth instead of
164 | password.**
165 |
166 | ## Providers
167 |
168 | // TBD
169 |
170 | ### Builtin Providers
171 |
172 | Lobbyboy current support multiple Providers:
173 |
174 | - Vagrant (Need vagrant and virtualbox to be installed)
175 | - Footlosse, in another words, containers (Need
176 | [footloose](https://github.com/weaveworks/footloose) and docker to be
177 | installed)
178 | - DigitalOcean
179 | - Linode
180 | - [Ignite](https://github.com/weaveworks/ignite) (Runs Firecracker VM)
181 | - [multipass](https://multipass.run)
182 |
183 | Different Providers support different configs, please see the
184 | [example config](https://github.com/laixintao/lobbyboy/blob/main/lobbyboy/conf/lobbyboy_config.toml)
185 | for more detail.
186 |
187 | #### Vagrant Provider
188 |
189 | Vagrant Provider won't cost you any money, [vagrant](https://www.vagrantup.com/)
190 | is a software runs on your computer along with virtual machine providers,
191 | vagrant can provision and control your VM.
192 |
193 | This provider can help you to create a new Vagrant instance when you login to
194 | Lobbyboy, and destroy the server when you no longer use it.
195 |
196 | Supported Features:
197 |
198 | - Create new Vagrant instances
199 | - You can configure your VM via `vagrantfile` config (see the config
200 | [example](./lobbyboy/conf/lobbyboy_config.toml)).
201 |
202 | #### Footloose Provider
203 |
204 | [footloose](https://github.com/weaveworks/footloose) can make your docker
205 | containers(or Firecracker with [ignite](https://github.com/weaveworks/ignite))
206 | act like virtual machine, so you can ssh to it.
207 |
208 | Supported feature:
209 |
210 | - Configurable base image
211 | - Create a docker container and redirect you in
212 |
213 | #### DigitalOcean Provider
214 |
215 | This Provider will create
216 | [Droplet](https://docs.digitalocean.com/products/droplets/) from DigitalOcean.
217 |
218 | Supported Features:
219 |
220 | - Create a new ssh key every time create a droplet.
221 | - Ask user to input region/droplet size/image when creating.
222 | - User can save favorite Droplet region/size/image in configs to quick create.
223 | - Destroy droplet when it is not in use.
224 |
225 | #### Linode Provider
226 |
227 | This Provider will create [Node](https://www.linode.com/docs/products/compute/)
228 | from Linode.
229 |
230 | Supported Features:
231 |
232 | - Create a new ssh key every time create a droplet.
233 | - Ask user to input region/node type/image when creating.
234 | - User can save favorite node region/type/image in configs to quick create.
235 | - Destroy node when it is not in use.
236 |
237 | Please see
238 | [configs](https://github.com/laixintao/lobbyboy/blob/main/lobbyboy/conf/lobbyboy_config.toml)
239 | to check available options.
240 |
241 | #### Ignite(Firecracker) Provider
242 |
243 | Supported Features:
244 |
245 | - Create a new Firecracker virtual machine
246 | - Destroy node when it is not in use.
247 |
248 | #### Multipass Provider
249 |
250 | Supported Features:
251 |
252 | - Create a new virtual machine
253 | - Destroy node when it is not in use.
254 |
255 | 
256 |
257 | ### Write Your Own Providers
258 |
259 | Providers are VPS vendors, by writing new providers, lobbyboy can work with any
260 | VPS vendors.
261 |
262 | To make a new Provider work, you need to extend base class
263 | `lobbyboy.provider.BaseProvider``, implement 2 methods:
264 |
265 | ```python
266 | def is_available(self) -> bool:
267 | """
268 | Override this method to check for requirements of this provider
269 |
270 | Returns:
271 | bool: True if this provider is available, False to disable it
272 | """
273 | return True
274 |
275 | def create_server(self, channel: Channel) -> LBServerMeta:
276 | """
277 | Args:
278 | channel: paramiko channel
279 |
280 | Returns:
281 | LBServerMeta: server meta info
282 | """
283 | ...
284 |
285 |
286 | def destroy_server(self, meta: LBServerMeta, channel: Channel = None) -> bool:
287 | """
288 | Args:
289 | meta: LBServerMeta, we use this to locate one server then destroy it.
290 | channel: Note that the channel can be None.
291 | If called from server_killer, channel will be None.
292 | if called when user logout from server, channel is active.
293 |
294 | Returns:
295 | bool: True if destroy successfully, False if not.
296 | """
297 | ...
298 | ```
299 |
300 | Then add your Provider to your config file.
301 |
302 | Those 3 configs are obligatory, as lobbyboy has to know when should he destroy
303 | your spare servers. You can add more configs, and read them from
304 | `self.provider_config` from code, just remember to add docs about it :)
305 |
306 | ```toml
307 | [provider.]
308 | load_module = "lobbyboy.contrib.provider.::"
309 | min_life_to_live = "1h"
310 | bill_time_unit = "1h"
311 | ```
312 |
313 | ### Publish Your Own Providers
314 |
315 | // TBD
316 |
317 | ## FAQ
318 |
319 | Q: Can I use lobbyboy as a proxy, like adding it to my `ProxyCommand` in ssh
320 | config?
321 |
322 | A: No. Lobbyboy works like a reverse proxy, meaning to say, for ssh client, it
323 | just like a ssh server(sshd maybe), ssh client get a shell from lobbyboy, and
324 | doesn't know if it is local shell or it is a nested shell which runs another
325 | ssh. (but you know it, right? :D )
326 |
327 | ## I Want to Know More!
328 |
329 | - [介绍 Lobbyboy 项目](https://www.kawabangga.com/posts/4576) (in Chinese)
330 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lobbyboy-ssh/lobbyboy/dafbb5905bd1c43facda7001a36e11b3a6ede837/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | test_pair = namedtuple("test_pair", "input, expected")
4 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from lobbyboy.config import LBConfig
4 |
5 | PARENT_DIR = Path(__file__).parent
6 | CONFIG_FILE = PARENT_DIR.parent / "lobbyboy" / "conf" / "lobbyboy_config.toml"
7 |
8 |
9 | def test_load_config():
10 | LBConfig.load(CONFIG_FILE)
11 |
12 |
13 | def test_load_providers_from_config():
14 | config = LBConfig.load(CONFIG_FILE)
15 | assert len(config.provider_cls) > 0
16 |
--------------------------------------------------------------------------------
/tests/test_providers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lobbyboy-ssh/lobbyboy/dafbb5905bd1c43facda7001a36e11b3a6ede837/tests/test_providers/__init__.py
--------------------------------------------------------------------------------
/tests/test_providers/conftest.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from lobbyboy.config import LBServerMeta
7 | from lobbyboy.contrib.provider.footloose import FootlooseConfig, FootlooseProvider
8 |
9 |
10 | @pytest.fixture
11 | def footloose_provider():
12 | workspace = Path("/tmp/footloose_test/")
13 | yield FootlooseProvider(name="footloose", config=FootlooseConfig(), workspace=workspace)
14 | shutil.rmtree(workspace, ignore_errors=True)
15 |
16 |
17 | @pytest.fixture
18 | def footloose_server_meta():
19 | workspace = Path("/tmp/footloose_test/")
20 | yield LBServerMeta(workspace=workspace, provider_name="footloose", server_name="2021-12-05-1405")
21 | shutil.rmtree(workspace, ignore_errors=True)
22 |
--------------------------------------------------------------------------------
/tests/test_providers/test_footloose.py:
--------------------------------------------------------------------------------
1 | import re
2 | from pathlib import Path
3 | from unittest import mock
4 | from unittest.mock import call
5 |
6 | from freezegun import freeze_time
7 |
8 | from lobbyboy.config import LBServerMeta
9 |
10 |
11 | @mock.patch("subprocess.run")
12 | def test_footloose_destroy(fake_subprocess_run, footloose_provider, footloose_server_meta):
13 | fake_complete_process = mock.MagicMock()
14 | fake_complete_process.return_value.returncode = 0
15 | fake_subprocess_run.side_effect = fake_complete_process
16 |
17 | destroy_command = footloose_provider.destroy_server(footloose_server_meta, None)
18 | fake_subprocess_run.assert_called_with(
19 | ["footloose", "delete", "-c", Path("/tmp/footloose_test/footloose.yaml")], capture_output=True
20 | )
21 | assert destroy_command is True
22 |
23 |
24 | def test_ssh_server_commands(footloose_provider, footloose_server_meta):
25 | command = footloose_provider.ssh_server_command(footloose_server_meta, None)
26 | assert command == ["cd /tmp/footloose_test && footloose ssh root@2021-12-05-14050"]
27 |
28 |
29 | @mock.patch("subprocess.Popen")
30 | @freeze_time("2012-01-14 12:00:01")
31 | def test_create_server(mock_popen, footloose_provider):
32 | mock_channel = mock.MagicMock()
33 | mock_process = mock.MagicMock()
34 | mock_process.poll.side_effect = [False, True]
35 | mock_process.returncode = 0
36 | mock_popen.return_value = mock_process
37 |
38 | server = footloose_provider.create_server(mock_channel)
39 |
40 | mock_popen.assert_called_with(["footloose", "create"], cwd="/tmp/footloose_test/2012-01-14-1200")
41 | assert mock_channel.sendall.mock_calls[:3] == [
42 | call(b"Generate server 2012-01-14-1200 workspace /tmp/footloose_test/2012-01-14-1200 done.\r\n"),
43 | call(b"Check footloose create done"),
44 | call(b"."),
45 | ]
46 | assert re.match(rb"OK\(\d.\ds\).\r\n", mock_channel.sendall.mock_calls[-1][1][0]) is not None
47 | assert server == LBServerMeta(
48 | provider_name="footloose",
49 | workspace=Path("/tmp/footloose_test/2012-01-14-1200"),
50 | server_name="2012-01-14-1200",
51 | server_host="127.0.0.1",
52 | server_user="root",
53 | server_port=22,
54 | created_timestamp=1326542401,
55 | ssh_extra_args=[],
56 | manage=True,
57 | )
58 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import os.path
3 | import sys
4 | from dataclasses import dataclass
5 | from datetime import date, datetime
6 | from decimal import Decimal
7 | from pathlib import Path
8 | from unittest import mock
9 |
10 | import pytest
11 |
12 | from lobbyboy.exceptions import CantEnsureBytesException, TimeStrParseTypeException
13 | from lobbyboy.utils import (
14 | confirm_dc_type,
15 | dict_factory,
16 | encoder_factory,
17 | ensure_bytes,
18 | humanize_seconds,
19 | import_class,
20 | port_is_open,
21 | send_to_channel,
22 | to_seconds,
23 | )
24 | from tests.conftest import test_pair
25 |
26 |
27 | @dataclass
28 | class FakeDataclass:
29 | A: int = 1
30 | B: str = "b"
31 |
32 |
33 | test_args_encoder_factory = [
34 | test_pair(input=date(2001, 1, 19), expected="2001-01-19"),
35 | test_pair(input=datetime(2000, 1, 10, 23, 58, 59), expected="2000-01-10 23:58:59"),
36 | test_pair(input=Decimal("19.3"), expected="19.3"),
37 | test_pair(input=Path("/go/to/test/path"), expected="/go/to/test/path"),
38 | test_pair(input=FakeDataclass(), expected={"A": 1, "B": "b"}),
39 | test_pair(input={"A": "A"}, expected={"A": "A"}),
40 | ]
41 |
42 |
43 | @pytest.mark.parametrize("test", test_args_encoder_factory)
44 | def test_encoder_factory(test: test_pair):
45 | encoder = encoder_factory(raise_error=False)
46 | assert encoder(test.input) == test.expected
47 |
48 |
49 | test_mutation_args_encoder_factory = [
50 | test_pair(input=date(2001, 1, 19), expected="2001/01/19"),
51 | test_pair(input=datetime(2000, 1, 10, 23, 58, 59), expected="20000110 235859"),
52 | test_pair(input=Decimal("19.3"), expected=19),
53 | test_pair(input=Path("/go/to/test/path"), expected="path"),
54 | test_pair(input={"B": "C"}, expected={"B": "C"}),
55 | ]
56 |
57 |
58 | @pytest.mark.parametrize("test", test_mutation_args_encoder_factory)
59 | def test_encoder_factory_mutation(test: test_pair):
60 | encoder = encoder_factory(
61 | date_fmt="%Y/%m/%d",
62 | dt_fmt="%Y%m%d %H%M%S",
63 | decimal_factory=int,
64 | path_factory=lambda x: os.path.basename(x),
65 | raise_error=False,
66 | )
67 | assert encoder(test.input) == test.expected
68 |
69 |
70 | def test_encoder_factory_exception():
71 | encoder = encoder_factory()
72 | with pytest.raises(TypeError):
73 | encoder(range(3))
74 |
75 |
76 | test_args_dict_factory = [
77 | test_pair(
78 | input=[
79 | {
80 | "A": "A",
81 | "B": "B",
82 | "ignore_field_1": "ignore_field_1",
83 | "ignore_field_2": "ignore_field_2",
84 | "_ignore_1": "_ignore_1",
85 | "_ignore_2": "_ignore_2",
86 | },
87 | ["ignore_field_1", "ignore_field_2"],
88 | lambda x: x.startswith("_"),
89 | ],
90 | expected={"A": "A", "B": "B"},
91 | ),
92 | test_pair(
93 | input=[
94 | {
95 | "C": Decimal("19.3"),
96 | "ignore_field_4": "ignore_field_14",
97 | "_ignore_2": "_ignore_2",
98 | },
99 | ["ignore_field_4", "ignore_field_5"],
100 | lambda x: False,
101 | encoder_factory(raise_error=False),
102 | ],
103 | expected={"C": "19.3", "_ignore_2": "_ignore_2"},
104 | ),
105 | ]
106 |
107 |
108 | @pytest.mark.parametrize("test", test_args_dict_factory)
109 | def test_dict_factory(test: test_pair):
110 | original = copy.deepcopy(test.input)
111 | assert dict_factory(*test.input) == test.expected
112 | assert original == test.input
113 |
114 |
115 | @mock.patch("socket.socket.connect_ex")
116 | def test_port_is_open(fake_socket):
117 | test_ip = "127.0.0.1"
118 |
119 | fake_socket.side_effect = mock.MagicMock(return_value=0)
120 | is_open = port_is_open(test_ip)
121 | assert is_open is True
122 |
123 | fake_socket.side_effect = mock.MagicMock(return_value=1)
124 | is_open = port_is_open(test_ip)
125 | assert is_open is False
126 |
127 |
128 | test_args_to_seconds = [
129 | test_pair(input="0", expected=0),
130 | test_pair(input="10s", expected=10),
131 | test_pair(input="1m", expected=60),
132 | test_pair(input="59m", expected=60 * 59),
133 | test_pair(input="1h", expected=60 * 60),
134 | test_pair(input="20h", expected=60 * 60 * 20),
135 | test_pair(input="1d", expected=60 * 60 * 24 * 1),
136 | test_pair(input="3d", expected=60 * 60 * 24 * 3),
137 | ]
138 |
139 |
140 | @pytest.mark.parametrize("test", test_args_to_seconds)
141 | def test_to_seconds(test: test_pair):
142 | assert to_seconds(test.input) == test.expected
143 |
144 |
145 | test_exception_args_to_seconds = [
146 | test_pair(input="-1", expected=TimeStrParseTypeException),
147 | test_pair(input="2", expected=TimeStrParseTypeException),
148 | test_pair(input="1w", expected=TimeStrParseTypeException),
149 | test_pair(input="1min", expected=TimeStrParseTypeException),
150 | ]
151 |
152 |
153 | @pytest.mark.parametrize("test", test_exception_args_to_seconds)
154 | def test_to_seconds_exception(test: test_pair):
155 | with pytest.raises(test.expected):
156 | to_seconds(test.input)
157 |
158 |
159 | test_args_humanize_seconds = [
160 | test_pair(input=0, expected="0:00:00"),
161 | test_pair(input=44, expected="0:00:44"),
162 | test_pair(input=364121, expected="4 days, 5:08:41"),
163 | ]
164 |
165 |
166 | @pytest.mark.parametrize("test", test_args_humanize_seconds)
167 | def test_humanize_seconds(test: test_pair):
168 | assert humanize_seconds(test.input) == test.expected
169 |
170 |
171 | test_args_ensure_bytes = [
172 | test_pair(input="test", expected=b"test"),
173 | test_pair(input=b"test", expected=b"test"),
174 | ]
175 |
176 |
177 | @pytest.mark.parametrize("test", test_args_ensure_bytes)
178 | def test_ensure_bytes(test: test_pair):
179 | assert ensure_bytes(test.input) == test.expected
180 |
181 |
182 | test_exception_args_ensure_bytes = [
183 | test_pair(input=None, expected=CantEnsureBytesException),
184 | test_pair(input=0, expected=CantEnsureBytesException),
185 | ]
186 |
187 |
188 | @pytest.mark.parametrize("test", test_exception_args_ensure_bytes)
189 | def test_ensure_bytes_exception(test: test_pair):
190 | with pytest.raises(test.expected):
191 | ensure_bytes(test.input)
192 |
193 |
194 | test_args_send_to_channel = [
195 | test_pair(input=["fake_msg1", "prefix1", "suffix1"], expected=None),
196 | test_pair(input=["fake_msg2", b"prefix2", "suffix2"], expected=None),
197 | test_pair(input=["fake_msg3", "prefix3", b"suffix3"], expected=None),
198 | ]
199 |
200 |
201 | @pytest.mark.parametrize("test", test_args_send_to_channel)
202 | def test_send_to_channel(test: test_pair):
203 | fake_channel = mock.MagicMock()
204 | assert send_to_channel(fake_channel, *test.input) == test.expected
205 |
206 |
207 | test_args_confirm_dc_type = [
208 | test_pair(input=[FakeDataclass(A=2, B="1"), FakeDataclass], expected=FakeDataclass(A=2, B="1")),
209 | test_pair(input=[dict(A=2, B="1"), FakeDataclass], expected=FakeDataclass(A=2, B="1")),
210 | test_pair(input=[None, FakeDataclass], expected=None),
211 | test_pair(input=[[1, 2, 3], FakeDataclass], expected=[1, 2, 3]),
212 | ]
213 |
214 |
215 | @pytest.mark.parametrize("test", test_args_confirm_dc_type)
216 | def test_confirm_dc_type(test: test_pair):
217 | assert test.expected == confirm_dc_type(*test.input)
218 |
219 |
220 | def test_choose_option(): ...
221 |
222 |
223 | def test_read_user_input_line(): ...
224 |
225 |
226 | def test_confirm_ssh_key_pair(): ...
227 |
228 |
229 | def test_generate_ssh_key_pair(): ...
230 |
231 |
232 | def test_write_key_to_file(): ...
233 |
234 |
235 | def test_try_load_key_from_file(): ...
236 |
237 |
238 | def test_import_class():
239 | assert import_class("") is None
240 | assert import_class("Test") is None
241 | assert import_class("Test::A::B") is None
242 |
243 | with mock.patch("importlib.import_module") as imp:
244 | imp.side_effect = mock.MagicMock(return_value=sys.modules[__name__])
245 | assert import_class(f"FAKE_MODULE::{FakeDataclass.__name__}") is FakeDataclass
246 |
--------------------------------------------------------------------------------