Welcome to PyTest local FTP Server’s documentation!¶
Contents:
PyTest FTP Server¶
A PyTest plugin which provides an FTP fixture for your tests
- Free software: MIT license
- Documentation: https://pytest-localftpserver.readthedocs.io/en/latest/index.html
Attention!¶
As of version 1.0.0
the support for python 2.7 and 3.4 was dropped.
If you need to support those versions you should pin the version to 0.6.0
,
i.e. add the following lines to your “requirements_dev.txt”:
# pytest_localftpserver==0.6.0
https://github.com/oz123/pytest-localftpserver/archive/v0.6.0.zip
Usage Quickstart:¶
This Plugin provides the fixtures ftpserver
and ftpserver_TLS
,
which are threaded instances of a FTP server, with which you can upload files and test FTP
functionality. It can be configured using the following environment variables:
Environment variable | Usage |
---|---|
FTP_USER |
Username of the registered user. |
FTP_PASS |
Password of the registered user. |
FTP_PORT |
Port for the normal ftp server to run on. |
FTP_HOME |
Home folder (host system) of the registered user. |
FTP_FIXTURE_SCOPE |
Scope/lifetime of the fixture. |
FTP_PORT_TLS |
Port for the TLS ftp server to run on. |
FTP_HOME_TLS |
Home folder (host system) of the registered user, used by the TLS ftp server. |
FTP_CERTFILE |
Certificate (host system) to be used by the TLS ftp server. |
See the tests directory or the documentation for examples.
You can either set environment variables on a system level or use tools such as pytest-env or tox, to change the default settings of this plugin. Sample config for pytest-cov:
$ cat pytest.ini
[pytest]
env =
FTP_USER=benz
FTP_PASS=erni1
FTP_HOME = /home/ftp_test
FTP_PORT=31175
FTP_FIXTURE_SCOPE=function
# only affects ftpserver_TLS
FTP_PORT_TLS = 31176
FTP_HOME_TLS = /home/ftp_test_TLS
FTP_CERTFILE = ./tests/test_keycert.pem
Sample config for Tox:
$ cat tox.ini
[tox]
envlist = py{36,37,38,39,310}
[testenv]
setenv =
FTP_USER=benz
FTP_PASS=erni1
FTP_HOME = {envtmpdir}
FTP_PORT=31175
FTP_FIXTURE_SCOPE=function
# only affects ftpserver_TLS
FTP_PORT_TLS = 31176
FTP_HOME_TLS = /home/ftp_test_TLS
FTP_CERTFILE = {toxinidir}/tests/test_keycert.pem
commands =
pytest tests
Credits¶
This package was inspired by, https://pypi.org/project/pytest-localserver/ made by Sebastian Rahlf, which lacks an FTP server.
This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.
Installation¶
Stable release¶
To install PyTest FTP Server, run this command in your terminal:
$ pip install pytest-localftpserver
If you don’t have pip installed, this Python installation guide can guide you through the process.
Or if you prefer to use conda:
$ conda install -c conda-forge pytest-localftpserver
This are the preferred methods to install PyTest FTP Server, as it will always install the most recent stable release.
From sources¶
The sources for PyTest FTP Server can be downloaded from the Github repo.
You can either clone the public repository:
$ git clone git://github.com/oz123/pytest-localftpserver
Or download the tarball:
$ curl -OL https://github.com/oz123/pytest-localftpserver/tarball/master
Once you have a copy of the source, you can install it with:
$ python setup.py install
Usage¶
After installing pytest_localftpserver the fixture ftpserver
is available for
your pytest test functions. Note that you can’t use fixtures outside of functions and
need to pass them as arguments.
Basic usage¶
A basic example of using pytest_localftpserver would be, if you wanted to test code, which uploads a file to a FTP-server.
import os
def test_your_code_to_upload_files(ftpserver):
your_code_to_upload_files(host="localhost",
port=ftpserver.server_port,
username=ftpserver.username,
password=ftpserver.password,
files=["testfile.txt"])
uploaded_file_path = os.path.join(ftpserver.server_home, "testfile.txt")
with open("testfile.txt") as original, open(uploaded_file_path) as uploaded:
assert original.read() == uploaded.read()
Note
Like most public FTP-servers pytest_localftpserver doesn’t allow the anonymous user to upload files. The anonymous user is only allowed to browse the folder structure and download files. If you want to upload files you need to use the registered user, with its password.
An other common use case would be retrieving a file from a FTP-server.
import os
from shutil import copyfile
def test_your_code_retrieving_files(ftpserver):
dest_path = os.path.join(ftpserver.anon_root, "testfile.txt")
copyfile("testfile.txt", dest_path)
your_code_retrieving_files(host="localhost",
port=ftpserver.server_port
file_paths=[{"remote": "testfile.txt",
"local": "testfile_downloaded.txt"
}])
with open("testfile.txt") as original, open("testfile_downloaded.txt") as downloaded:
assert original.read() == downloaded.read()
Login with the TLS server¶
This example utilizes methods of the the high-level interface, which are explained in Getting login credentials and Gaining information about the content of files on the server.
The below example test logs into the TLS ftpserver, creates the file testfile.txt
, with content ‘test text’ and
checks if it was written properly.
from ftplib import FTP_TLS
from ssl import SSLContext
try:
from ssl import PROTOCOL_TLS
except Exception:
from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS
def test_TLS_login(ftpserver_TLS):
if PYTHON3:
ssl_context = SSLContext(PROTOCOL_TLS)
ssl_context.load_cert_chain(certfile=DEFAULT_CERTFILE)
ftp = FTP_TLS(context=ssl_context)
else:
ftp = FTP_TLS(certfile=DEFAULT_CERTFILE)
login_dict = ftpserver_TLS.get_login_data()
ftp.connect(login_dict["host"], login_dict["port"])
ftp.login(login_dict["user"], login_dict["passwd"])
ftp.prot_p()
ftp.cwd("/")
filename = "testfile.txt"
file_path_local = tmpdir.join(filename)
file_path_local.write("test text")
with open(str(file_path_local), "rb") as f:
ftp.storbinary("STOR "+filename, f)
ftp.quit()
file_list = list(ftpserver_TLS.get_file_contents()
assert file_list == [{"path": "testfile.txt", "content": "test text"}]
High-Level Interface¶
To allow you a faster and more comfortable handling of common ftp tasks a high-level
interface was implemented. Most of the following methods have the keyword anon
, which
allows to switch between the registered (anon=False) and the anonymous (anon=True) user.
For more information on how those methods work, take a look at the API Documentation .
Note
The following examples aren’t working code, since the aren’t called from
within a function, which means that the ftpserver
fixture isn’t available.
They are thought to be a quick overview of the available functionality and
its output.
Getting login credentials¶
To quickly get all needed login data you can use get_login_data
, which will either return
a dict or an url to log into the ftp:
>>> ftpserver.get_login_data()
{"host": "localhost", "port": 8888, "user": "fakeusername", "passwd": "qweqwe"}
>>> ftpserver.get_login_data(style="url", anon=False)
ftp://fakeusername:qweqwe@localhost:8888
>>> ftpserver.get_login_data(style="url", anon=True)
ftp://localhost:8888
Populating the FTP server with files and folders¶
To test ftp download capabilities of your code, you might want to populate the files on the server.
To “upload” files to the server you can use the method put_files
:
>>> ftpserver.put_files("test_folder/test_file", style="rel_path", anon=False)
["test_file"]
>>> ftpserver.put_files("test_folder/test_file", style="url", anon=False)
["ftp://fakeusername:qweqwe@localhost:8888/test_file"]
>>> ftpserver.put_files("test_folder/test_file", style="url", anon=True)
["ftp://localhost:8888/test_file"]
>>> ftpserver.put_files({"src": "test_folder/test_file",
... "dest": "remote_folder/uploaded_file"},
... style="url", anon=True)
["ftp://localhost:8888/remote_folder/uploaded_file"]
>>> ftpserver.put_files("test_folder/test_file", return_content=True)
[{"path": "test_file", "content": "some text in test_file"}]
>>> ftpserver.put_files("test_file.zip", return_content=True, read_mode="rb")
[{"path": "test_file.zip", "content": b'PK\\x03\\x04\\x14\\x00\\x00...'}]
>>> ftpserver.put_files("test_file", return_paths="new")
UserWarning: test_file does already exist and won't be overwritten.
Set `overwrite` to True to overwrite it anyway.
[]
>>> ftpserver.put_files("test_file", return_paths="new", overwrite=True)
["test_file"]
>>> ftpserver.put_files("test_file3", return_paths="all")
["test_file", "remote_folder/uploaded_file", "test_file.zip"]
Resetting files on the server¶
Since ftpserver
is a module scope fixture, you might want to make sure that uploaded files
get deleted after/before a test. This can be done by using the method reset_tmp_dirs
.
filesystem before:
+---server_home
| +---test_file1
| +---test_folder
| +---test_file2
|
+---anon_root
+---test_file3
+---test_folder
+---test_file4
>>> ftpserver.reset_tmp_dirs()
filesystem after:
+---server_home
|
+---anon_root
Gaining information on which files are on the server¶
If you want to know which files are on the server, i.e. if you want to know if your
file upload functionality is working, you can use the get_file_paths
method, which will
yield the paths to all files on the server.
filesystem
+---server_home
| +---test_file1
| +---test_folder
| +---test_file2
|
+---anon_root
+---test_file3
+---test_folder
+---test_file4
>>> list(ftpserver.get_file_paths(style="rel_path", anon=False))
["test_file1", "test_folder/test_file2"]
>>> list(ftpserver.get_file_paths(style="rel_path", anon=True))
["test_file3", "test_folder/test_file4"]
Gaining information about the content of files on the server¶
If you are interested in the content of a specific file, multiple files or all files,
i.e. to verify that your file upload functionality did work properly, you can use the
get_file_contents
method.
filesystem
+---server_home
+---test_file1.txt
+---test_folder
+---test_file2.zip
>>> list(ftpserver.get_file_contents())
[{"path": "test_file1.txt", "content": "test text"},
{"path": "test_folder/test_file2.txt", "content": "test text2"}]
>>> list(ftpserver.get_file_contents("test_file1.txt"))
[{"path": "test_file1.txt", "content": "test text"}]
>>> list(ftpserver.get_file_contents("test_file1.txt", style="url"))
[{"path": "ftp://fakeusername:qweqwe@localhost:8888/test_file1.txt",
"content": "test text"}]
>>> list(ftpserver.get_file_contents(["test_file1.txt", "test_folder/test_file2.zip"],
... read_mode="rb"))
[{"path": "test_file1.txt", "content": b"test text"},
{"path": "test_folder/test_file2.zip", "content": b'PK\\x03\\x04\\x14\\x00\\x00...'}]
Configuration¶
To configure custom values for for the username, the users password, the ftp port and/or
the location of the users home folder on the local storage, you need to set the environment
variables FTP_USER
, FTP_PASS
, FTP_PORT
, FTP_HOME
, FTP_FIXTURE_SCOPE
,
FTP_PORT_TLS
, FTP_HOME_TLS
and FTP_CERTFILE
.
Environment variable | Usage |
---|---|
FTP_USER |
Username of the registered user. |
FTP_PASS |
Password of the registered user. |
FTP_PORT |
Port for the normal ftp server to run on. |
FTP_HOME |
Home folder (host system) of the registered user. |
FTP_FIXTURE_SCOPE |
Scope/lifetime of the fixture. |
FTP_PORT_TLS |
Port for the TLS ftp server to run on. |
FTP_HOME_TLS |
Home folder (host system) of the registered user, used by the TLS ftp server. |
FTP_CERTFILE |
Certificate (host system) to be used by the TLS ftp server. |
You can either set environment variables on a system level or use tools such as pytest-env or tox, which would be the recommended way.
Note
You might run into OSError: [Errno 48] Address already in use
when setting a fixed port
(FTP_PORT
/ FTP_PORT_TLS
).
This is due to the server still listening on that port, which prevents it from adding another listener
on that port. When using pythons buildin ftplib
, you should use the
quit method
to terminate the connection, since it’s the ‘the “polite” way to close a connection’ and lets the
server know that the client isn’t just experiencing connection problems, but won’t come back.
Configuration with pytest-env¶
The configuration of pytest-env is done in the pytest.ini
file.
The following example configuration will use the username benz
, the password erni1
,
the ftp port 31175
and the home folder /home/ftp_test
.
For the encrypted version of the fixture it uses port 31176
, the home folder /home/ftp_test
and
the certificate ./tests/test_keycert.pem
:
$ cat pytest.ini
[pytest]
env =
FTP_USER=benz
FTP_PASS=erni1
FTP_HOME = /home/ftp_test
FTP_PORT=31175
FTP_FIXTURE_SCOPE=function
# only affects ftpserver_TLS
FTP_PORT_TLS = 31176
FTP_HOME_TLS = /home/ftp_test_TLS
FTP_CERTFILE = ./tests/test_keycert.pem
Configuration with Tox¶
The configuration of tox is done in the tox.ini
file.
The following example configuration will run the tests in the folder tests
on
python 3.6+ and use the username benz
, the password erni1
,
the tempfolder of each virtual environment the tests are run in ({envtmpdir}
) and
the ftp port 31175
.
For the encrypted version of the fixture it uses port 31176
and the certificate
{toxinidir}/tests/test_keycert.pem
:
$ cat tox.ini
[tox]
envlist = py{36,37,38,39,310}
[testenv]
setenv =
FTP_USER=benz
FTP_PASS=erni1
FTP_HOME = {envtmpdir}
FTP_PORT=31175
FTP_FIXTURE_SCOPE=function
# only affects ftpserver_TLS
FTP_PORT_TLS = 31176
FTP_HOME_TLS = /home/ftp_test_TLS
FTP_CERTFILE = {toxinidir}/tests/test_keycert.pem
commands =
pytest tests
API Documentation¶
This is the detailed documentation of FunctionalityWrapper
, which
holds all the functionality you gain by PyTest local FTP Server
.
FunctionalityWrapper |
Baseclass which holds the functionality of ftpserver. |
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
Report bugs at https://github.com/oz123/pytest-localftpserver/issues.
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.
Implement Features¶
Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.
Write Documentation¶
PyTest FTP Server could always use more documentation, whether as part of the official PyTest FTP Server docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
The best way to send feedback is to file an issue at https://github.com/oz123/pytest-localftpserver/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!¶
Ready to contribute? Here’s how to set up pytest_localftpserver for local development.
Fork the pytest-localftpserver repo on GitHub.
Clone your fork locally:
$ git clone git@github.com:your_name_here/pytest-localftpserver.git
Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:
$ mkvirtualenv pytest_localftpserver $ cd pytest-localftpserver/ $ pip install -r requirements_dev.txt $ python setup.py develop
Create a branch for local development:
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:
$ tox
To get flake8 and tox, just pip install them into your virtualenv.
Commit your changes and push your branch to GitHub:
$ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
- The pull request should include tests.
- If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
- The pull request should work for Python 3.6+. Check https://github.com/oz123/pytest-localftpserver/actions?query=workflow%3ATests and make sure that the tests pass for all supported Python versions.
Deploying¶
A reminder for the maintainers on how to deploy. Make sure all your changes are committed (including an entry in HISTORY.rst). Then run:
$ bump2version patch # possible: major / minor / patch
$ git push --follow-tags
Credits¶
Development Lead¶
- Oz Tiram <oz.tiram@gmail.com>
Contributors¶
- Sebastian Weigand <s.weigand.phy@gmail.com>
History¶
1.0.1 (2019-12-10)¶
- Include the certificate in the source package
- Use a bigger certificate
1.0.0 (2019-09-05)¶
- Dropped support for Python 2.7 and 3.4
0.6.0 - released as tag only¶
- Added fixture scope configuration.
- Added
ftpserver_TLS
as TLS version of the fixture.
0.5.0 (2018-12-04)¶
- Added support for Windows.
- Added hightlevel interface.
0.1.0 (2016-12-09)¶
- First release on PyPI.