Welcome to PyTest local FTP Server’s documentation!

Contents:

PyTest FTP Server

https://img.shields.io/pypi/v/pytest_localftpserver.svg https://camo.githubusercontent.com/89b9f56d30241e30f546daf9f43653f08e920f16/68747470733a2f2f696d672e736869656c64732e696f2f636f6e64612f766e2f636f6e64612d666f7267652f7079746573742d6c6f63616c6674707365727665722e737667 https://img.shields.io/pypi/pyversions/pytest_localftpserver.svg https://github.com/oz123/pytest-localftpserver/workflows/Tests/badge.svg Documentation Status Coverage

A PyTest plugin which provides an FTP fixture for your tests

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.

  1. Fork the pytest-localftpserver repo on GitHub.

  2. Clone your fork locally:

    $ git clone git@github.com:your_name_here/pytest-localftpserver.git
    
  3. 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
    
  4. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  5. 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.

  6. 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
    
  7. Submit a pull request through the GitHub website.

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.
  2. 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.
  3. 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.

Tips

To run a subset of tests:

$ pytest tests/test_pytest_localftpserver.py::<test_name>

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

Contributors

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.

Indices and tables