Repository: barseghyanartur/faker-file (language: python)

Issue:
Feature/sftp storage



Discussion / hints:

Fix patch:
diff --git a/.secrets.baseline b/.secrets.baseline
index ce2c60b4..f9114ac6 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -148,6 +148,15 @@
         "line_number": 34
       }
     ],
+    "src/faker_file/tests/test_sftp_server.py": [
+      {
+        "type": "Secret Keyword",
+        "filename": "src/faker_file/tests/test_sftp_server.py",
+        "hashed_secret": "e8662cfb96bd9c7fe84c31d76819ec3a92c80e63",
+        "is_verified": true,
+        "line_number": 84
+      }
+    ],
     "src/faker_file/tests/test_storages.py": [
       {
         "type": "Secret Keyword",
@@ -158,5 +167,5 @@
       }
     ]
   },
-  "generated_at": "2023-06-13T23:24:27Z"
+  "generated_at": "2023-06-15T21:40:37Z"
 }
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ba4055b2..0a630738 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,6 +15,14 @@ are used for versioning (schema follows below):
   0.3.4 to 0.4).
 - All backwards incompatible changes are mentioned in this document.
 
+0.15.4
+------
+2023-06-15
+
+- Improved ``SFTPStorage`` tests.
+- Stop testing against Python 3.7.
+- Stop testing against Django 4.0.
+
 0.15.3
 ------
 2023-06-14
diff --git a/examples/requirements/common.in b/examples/requirements/common.in
index 8903369b..aaa2e3ca 100644
--- a/examples/requirements/common.in
+++ b/examples/requirements/common.in
@@ -10,3 +10,4 @@ python-pptx
 reportlab
 tablib
 xml2epub
+paramiko
diff --git a/examples/requirements/common.txt b/examples/requirements/common.txt
index f2044145..08f3fb20 100644
--- a/examples/requirements/common.txt
+++ b/examples/requirements/common.txt
@@ -20,6 +20,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -35,7 +37,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -49,7 +53,9 @@ colorama==0.4.6
 coverage[toml]==6.5.0
     # via pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 edge-tts==6.0.8
@@ -122,6 +128,8 @@ openpyxl==3.0.10
     # via -r examples/requirements/common.in
 packaging==22.0
     # via pytest
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -146,6 +154,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pytest==7.2.0
     # via
     #   pathy
diff --git a/examples/requirements/dev.txt b/examples/requirements/dev.txt
index 68329593..43771dc9 100644
--- a/examples/requirements/dev.txt
+++ b/examples/requirements/dev.txt
@@ -12,6 +12,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -23,6 +25,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 bleach==5.0.1
@@ -40,7 +44,9 @@ certifi==2022.9.24
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -59,7 +65,9 @@ coverage[toml]==6.5.0
     #   pytest-cov
 cryptography==38.0.4
     # via
+    #   asyncssh
     #   azure-storage-blob
+    #   paramiko
     #   secretstorage
 defusedxml==0.7.1
     # via odfpy
@@ -194,6 +202,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -232,6 +242,8 @@ pygments==2.13.0
     # via
     #   readme-renderer
     #   rich
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -346,7 +358,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_2_2.txt b/examples/requirements/django_2_2.txt
index ac0346c4..243433d9 100644
--- a/examples/requirements/django_2_2.txt
+++ b/examples/requirements/django_2_2.txt
@@ -10,6 +10,8 @@ aiosignal==1.3.1
     # via aiohttp
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -21,6 +23,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -36,7 +40,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -52,7 +58,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -167,6 +176,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -199,6 +210,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -298,7 +311,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_2_2_and_flask.txt b/examples/requirements/django_2_2_and_flask.txt
index fa981d87..6f6405a0 100644
--- a/examples/requirements/django_2_2_and_flask.txt
+++ b/examples/requirements/django_2_2_and_flask.txt
@@ -12,6 +12,8 @@ alembic==1.9.0
     # via -r examples/requirements/flask.in
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -25,6 +27,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.36
@@ -42,7 +46,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -61,7 +67,10 @@ coverage[toml]==7.0.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -207,6 +216,8 @@ packaging==22.0
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -239,6 +250,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.2.1
     # via tox
 pyrsistent==0.19.3
@@ -346,7 +359,9 @@ typer==0.7.0
 typer-cli==0.0.1
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_3_2.txt b/examples/requirements/django_3_2.txt
index 27690b7b..6e36a2d6 100644
--- a/examples/requirements/django_3_2.txt
+++ b/examples/requirements/django_3_2.txt
@@ -12,6 +12,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -23,6 +25,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -38,7 +42,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -54,7 +60,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -172,6 +181,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -204,6 +215,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -303,7 +316,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_3_2_and_flask.txt b/examples/requirements/django_3_2_and_flask.txt
index 66b4654b..24ca8411 100644
--- a/examples/requirements/django_3_2_and_flask.txt
+++ b/examples/requirements/django_3_2_and_flask.txt
@@ -14,6 +14,8 @@ asgiref==3.6.0
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -27,6 +29,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.36
@@ -44,7 +48,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -63,7 +69,10 @@ coverage[toml]==7.0.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -212,6 +221,8 @@ packaging==22.0
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -244,6 +255,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.2.1
     # via tox
 pyrsistent==0.19.3
@@ -351,7 +364,9 @@ typer==0.7.0
 typer-cli==0.0.1
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_4_0.txt b/examples/requirements/django_4_0.txt
index e7efad89..70a21010 100644
--- a/examples/requirements/django_4_0.txt
+++ b/examples/requirements/django_4_0.txt
@@ -12,6 +12,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -23,6 +25,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -38,7 +42,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -54,7 +60,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -172,6 +181,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -204,6 +215,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -303,7 +316,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_4_0_and_flask.txt b/examples/requirements/django_4_0_and_flask.txt
index e57d50de..3603d7ff 100644
--- a/examples/requirements/django_4_0_and_flask.txt
+++ b/examples/requirements/django_4_0_and_flask.txt
@@ -14,6 +14,8 @@ asgiref==3.6.0
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -27,6 +29,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.36
@@ -44,7 +48,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -63,7 +69,10 @@ coverage[toml]==7.0.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -212,6 +221,8 @@ packaging==22.0
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -244,6 +255,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.2.1
     # via tox
 pyrsistent==0.19.3
@@ -351,7 +364,9 @@ typer==0.7.0
 typer-cli==0.0.1
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_4_1.txt b/examples/requirements/django_4_1.txt
index 9f3b877a..3bc7c884 100644
--- a/examples/requirements/django_4_1.txt
+++ b/examples/requirements/django_4_1.txt
@@ -12,6 +12,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -23,6 +25,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -38,7 +42,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -54,7 +60,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -172,6 +181,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -204,6 +215,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -303,7 +316,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_4_1_and_flask.txt b/examples/requirements/django_4_1_and_flask.txt
index 5b6e1230..23701bb6 100644
--- a/examples/requirements/django_4_1_and_flask.txt
+++ b/examples/requirements/django_4_1_and_flask.txt
@@ -14,6 +14,8 @@ asgiref==3.6.0
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -27,6 +29,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.36
@@ -44,7 +48,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -63,7 +69,10 @@ coverage[toml]==7.0.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -212,6 +221,8 @@ packaging==22.0
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -244,6 +255,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.2.1
     # via tox
 pyrsistent==0.19.3
@@ -351,7 +364,9 @@ typer==0.7.0
 typer-cli==0.0.1
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/django_4_2.txt b/examples/requirements/django_4_2.txt
index 6229d6e6..ddff8931 100644
--- a/examples/requirements/django_4_2.txt
+++ b/examples/requirements/django_4_2.txt
@@ -12,6 +12,8 @@ asgiref==3.7.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==23.1.0
     # via
     #   aiohttp
@@ -20,6 +22,8 @@ azure-core==1.26.4
     # via azure-storage-blob
 azure-storage-blob==12.16.0
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.12.2
     # via xml2epub
 boto3==1.26.142
@@ -35,7 +39,9 @@ cachetools==5.3.1
 certifi==2023.5.7
     # via requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==3.1.0
@@ -55,7 +61,10 @@ coverage[toml]==7.2.6
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==40.0.2
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -170,6 +179,8 @@ packaging==23.1
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -202,6 +213,8 @@ pyasn1-modules==0.3.0
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.5.1
     # via tox
 pyrsistent==0.19.3
@@ -297,6 +310,7 @@ typer-cli==0.0.13
 typing-extensions==4.6.2
     # via
     #   asgiref
+    #   asyncssh
     #   azure-core
     #   azure-storage-blob
 uritemplate==4.1.1
diff --git a/examples/requirements/django_4_2_and_flask.txt b/examples/requirements/django_4_2_and_flask.txt
index e13c7a08..28322352 100644
--- a/examples/requirements/django_4_2_and_flask.txt
+++ b/examples/requirements/django_4_2_and_flask.txt
@@ -14,6 +14,8 @@ asgiref==3.7.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==23.1.0
     # via
     #   aiohttp
@@ -24,6 +26,8 @@ azure-storage-blob==12.16.0
     # via pathy
 babel==2.12.1
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.12.2
     # via xml2epub
 blinker==1.6.2
@@ -41,7 +45,9 @@ cachetools==5.3.1
 certifi==2023.5.7
     # via requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==3.1.0
@@ -62,7 +68,10 @@ coverage[toml]==7.2.6
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==40.0.2
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -205,6 +214,8 @@ packaging==23.1
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -237,6 +248,8 @@ pyasn1-modules==0.3.0
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.5.1
     # via tox
 pyrsistent==0.19.3
@@ -343,6 +356,7 @@ typing-extensions==4.6.2
     # via
     #   alembic
     #   asgiref
+    #   asyncssh
     #   azure-core
     #   azure-storage-blob
     #   sqlalchemy
diff --git a/examples/requirements/docs.txt b/examples/requirements/docs.txt
index 89063823..e68bbd70 100644
--- a/examples/requirements/docs.txt
+++ b/examples/requirements/docs.txt
@@ -14,6 +14,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -27,6 +29,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via sphinx
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -42,7 +46,9 @@ certifi==2022.9.24
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -58,7 +64,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -189,6 +198,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -225,6 +236,8 @@ pygments==2.13.0
     # via
     #   rst2pdf
     #   sphinx
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -352,7 +365,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/examples/requirements/flask.txt b/examples/requirements/flask.txt
index 016570d6..432d2648 100644
--- a/examples/requirements/flask.txt
+++ b/examples/requirements/flask.txt
@@ -12,6 +12,8 @@ alembic==1.9.0
     # via -r examples/requirements/flask.in
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -24,6 +26,8 @@ azure-storage-blob==12.14.1
     # via pathy
 babel==2.11.0
     # via flask-babelex
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.36
@@ -41,7 +45,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -60,7 +66,10 @@ coverage[toml]==7.0.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -185,6 +194,8 @@ packaging==22.0
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -217,6 +228,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.2.1
     # via tox
 pytest==7.2.1
@@ -314,7 +327,9 @@ typer==0.7.0
 typer-cli==0.0.1
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 urllib3==1.26.13
     # via
     #   botocore
diff --git a/examples/requirements/ml.txt b/examples/requirements/ml.txt
index 208497f6..b5c9565f 100644
--- a/examples/requirements/ml.txt
+++ b/examples/requirements/ml.txt
@@ -10,6 +10,8 @@ aiosignal==1.3.1
     # via aiohttp
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.2.0
     # via
     #   aiohttp
@@ -20,6 +22,8 @@ azure-core==1.26.2
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via
     #   gdown
@@ -39,7 +43,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 chardet==5.1.0
     # via tox
 charset-normalizer==2.1.1
@@ -59,7 +65,10 @@ coverage[toml]==7.0.5
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==39.0.0
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -182,6 +191,8 @@ pandas==1.5.2
     # via nlpaug
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -214,6 +225,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyproject-api==1.4.0
     # via tox
 pysocks==1.7.1
@@ -334,6 +347,7 @@ typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
     # via
+    #   asyncssh
     #   azure-core
     #   huggingface-hub
     #   torch
diff --git a/setup.py b/setup.py
index d725aad6..58d636b1 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 from setuptools import find_packages, setup
 
-version = "0.15.3"
+version = "0.15.4"
 
 try:
     readme = open(os.path.join(os.path.dirname(__file__), "README.rst")).read()
diff --git a/src/faker_file/__init__.py b/src/faker_file/__init__.py
index 56276fde..af4dff45 100644
--- a/src/faker_file/__init__.py
+++ b/src/faker_file/__init__.py
@@ -1,5 +1,5 @@
 __title__ = "faker_file"
-__version__ = "0.15.3"
+__version__ = "0.15.4"
 __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
 __copyright__ = "2022-2023 Artur Barseghyan"
 __license__ = "MIT"
diff --git a/tox.ini b/tox.ini
index 57881a91..38349b7e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,9 @@
 [tox]
 envlist =
-;    py{37,38,39,310}-django{22}  # no longer support: 30,31
+;    py{37,38,39,310}-django{22,30,31}  # no longer supported
+;    py{38,39,310,311}-django{40}  # no longer supported
     py{37,38,39,310,311}-django{32}
-    py{38,39,310,311}-django{40,41,42}
+    py{38,39,310,311}-django{41,42}
 
 [testenv]
 envlogdir={work_dir}/{env_name}/tmp
@@ -13,14 +14,14 @@ deps =
 ;    django30: -r{toxinidir}/examples/requirements/django_3_0.txt
 ;    django31: -r{toxinidir}/examples/requirements/django_3_1.txt
     django32: -r{toxinidir}/examples/requirements/django_3_2.txt
-    django40: -r{toxinidir}/examples/requirements/django_4_0.txt
+;    django40: -r{toxinidir}/examples/requirements/django_4_0.txt
     django41: -r{toxinidir}/examples/requirements/django_4_1.txt
     django42: -r{toxinidir}/examples/requirements/django_4_2.txt
 commands =
     mkdir -p examples/django_example/project/media/
     mkdir -p examples/django_example/project/static/
     pip install -e .[all]
-    {envpython} -m pytest -vrx -m "not optional" --ignore src/faker_file/tests/test_sqlalchemy_integration.py --ignore src/faker_file/tests/test_augment_file_from_dir_provider.py --ignore src/faker_file/tests/test_sftp_storage.py
+    {envpython} -m pytest -vrx -m "not optional" --ignore src/faker_file/tests/test_sqlalchemy_integration.py --ignore src/faker_file/tests/test_augment_file_from_dir_provider.py
 
 [gh-actions]
 python =


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7ce244f2..6cf9e426 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,7 +33,7 @@ jobs:
 #          cache: 'pip'
 #          cache-dependency-path: '**/setup.py'
       - name: Install detect-secrets
-        run: pip install detect-secrets doc8 isort==5.11.5
+        run: pip install --no-cache-dir detect-secrets doc8 isort==5.11.5
       - name: Run pre-commit
         uses: pre-commit/action@v3.0.0
 
@@ -73,11 +73,11 @@ jobs:
           python -m pip install --upgrade pip
       - name: Install tox
         run: |
-          python -m pip install tox-gh-actions pip-tools Django djangorestframework django-extensions drf_spectacular
+          python -m pip install --no-cache-dir tox-gh-actions pip-tools Django djangorestframework django-extensions drf_spectacular
       - name: Compile requirements
         run: pip-compile examples/requirements/flask.in
       - name: Install requirements
-        run: pip install -r examples/requirements/flask.txt
+        run: pip install --no-cache-dir -r examples/requirements/flask.txt
       - name: Run test suite
 #        continue-on-error: true
         run: pytest -vrx src/faker_file/tests/test_sqlalchemy_integration.py #--workers 2
@@ -117,11 +117,11 @@ jobs:
           python -m pip install --upgrade pip
       - name: Install tox
         run: |
-          python -m pip install tox-gh-actions pip-tools Django djangorestframework django-extensions drf_spectacular
+          python -m pip install --no-cache-dir tox-gh-actions pip-tools Django djangorestframework django-extensions drf_spectacular
       - name: Compile requirements
         run: pip-compile examples/requirements/ml.in
       - name: Install requirements
-        run: pip install -r examples/requirements/ml.txt
+        run: pip install --no-cache-dir -r examples/requirements/ml.txt
       - name: Run test suite
 #        continue-on-error: true
         run: pytest -vrx src/faker_file/tests/test_augment_file_from_dir_provider.py #--workers 2
@@ -155,7 +155,7 @@ jobs:
           - "3.10"
           - "3.9"
           - "3.8"
-          - "3.7"
+#          - "3.7"
     steps:
     - name: Install wkhtmltopdf
       run: sudo apt-get update && sudo apt-get install -y wkhtmltopdf
@@ -170,7 +170,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
     - name: Install tox
-      run: python -m pip install tox-gh-actions
+      run: python -m pip install --no-cache-dir tox-gh-actions
     - name: Run test suite
       run: tox -r --parallel auto
       env:
diff --git a/examples/requirements/test.in b/examples/requirements/test.in
index 700df1bc..7b22d06e 100644
--- a/examples/requirements/test.in
+++ b/examples/requirements/test.in
@@ -11,3 +11,4 @@ pytest-parallel
 pytest-rerunfailures
 pytest>=7.0.1
 tox
+asyncssh
diff --git a/examples/requirements/test.txt b/examples/requirements/test.txt
index 8a71f99a..f7252088 100644
--- a/examples/requirements/test.txt
+++ b/examples/requirements/test.txt
@@ -4,12 +4,18 @@
 #
 #    pip-compile examples/requirements/test.in
 #
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via pytest
+cffi==1.15.1
+    # via cryptography
 coverage[toml]==6.5.0
     # via
     #   -r examples/requirements/test.in
     #   pytest-cov
+cryptography==41.0.1
+    # via asyncssh
 distlib==0.3.6
     # via virtualenv
 exceptiongroup==1.1.0
@@ -47,6 +53,8 @@ py==1.11.0
     # via
     #   -r examples/requirements/test.in
     #   tox
+pycparser==2.21
+    # via cffi
 pyparsing==3.0.9
     # via packaging
 pytest==7.2.1
@@ -86,5 +94,7 @@ tomli==2.0.1
     #   tox
 tox==3.27.1
     # via -r examples/requirements/test.in
+typing-extensions==4.6.3
+    # via asyncssh
 virtualenv==20.17.0
     # via tox
diff --git a/examples/requirements/testing.txt b/examples/requirements/testing.txt
index 1c244442..18d39062 100644
--- a/examples/requirements/testing.txt
+++ b/examples/requirements/testing.txt
@@ -12,6 +12,8 @@ asgiref==3.5.2
     # via django
 async-timeout==4.0.2
     # via aiohttp
+asyncssh==2.13.1
+    # via -r examples/requirements/test.in
 attrs==22.1.0
     # via
     #   aiohttp
@@ -23,6 +25,8 @@ azure-core==1.26.1
     #   msrest
 azure-storage-blob==12.14.1
     # via pathy
+bcrypt==4.0.1
+    # via paramiko
 beautifulsoup4==4.11.1
     # via xml2epub
 boto3==1.26.29
@@ -38,7 +42,9 @@ certifi==2022.12.7
     #   msrest
     #   requests
 cffi==1.15.1
-    # via cryptography
+    # via
+    #   cryptography
+    #   pynacl
 charset-normalizer==2.1.1
     # via
     #   aiohttp
@@ -54,7 +60,10 @@ coverage[toml]==6.5.0
     #   -r examples/requirements/test.in
     #   pytest-cov
 cryptography==38.0.4
-    # via azure-storage-blob
+    # via
+    #   asyncssh
+    #   azure-storage-blob
+    #   paramiko
 defusedxml==0.7.1
     # via odfpy
 distlib==0.3.6
@@ -172,6 +181,8 @@ packaging==21.3
     #   tox
 parametrize==0.1.1
     # via -r examples/requirements/test.in
+paramiko==3.2.0
+    # via -r examples/requirements/common.in
 pathy[all]==0.10.1
     # via -r examples/requirements/common.in
 pdfkit==1.0.0
@@ -204,6 +215,8 @@ pyasn1-modules==0.2.8
     # via google-auth
 pycparser==2.21
     # via cffi
+pynacl==1.5.0
+    # via paramiko
 pyparsing==3.0.9
     # via packaging
 pyrsistent==0.19.3
@@ -303,7 +316,9 @@ typer==0.3.2
 typer-cli==0.0.12
     # via pathy
 typing-extensions==4.4.0
-    # via azure-core
+    # via
+    #   asyncssh
+    #   azure-core
 uritemplate==4.1.1
     # via drf-spectacular
 urllib3==1.26.13
diff --git a/src/faker_file/tests/sftp_server.py b/src/faker_file/tests/sftp_server.py
new file mode 100644
index 00000000..fe96dbfe
--- /dev/null
+++ b/src/faker_file/tests/sftp_server.py
@@ -0,0 +1,151 @@
+import asyncio
+import logging
+import os
+import tempfile
+import threading
+from asyncio import Semaphore
+from typing import Type
+
+import asyncssh
+
+__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
+__copyright__ = "2022-2023 Artur Barseghyan"
+__license__ = "MIT"
+__all__ = (
+    "SFTPServer",
+    "SFTPServerManager",
+    "SSHServer",
+    "start_server",
+    "start_server_async",
+)
+
+DIR_PATH = os.environ.get("DIR_PATH", tempfile.gettempdir())
+SFTP_USER = os.environ.get("SFTP_USER", "foo")
+SFTP_PASS = os.environ.get("SFTP_PASS", "pass")
+SFTP_HOST = os.environ.get("SFTP_HOST", "0.0.0.0")
+SFTP_PORT = int(os.environ.get("SFTP_PORT", 2222))
+NUM_CONCURRENT_CONNECTIONS = int(
+    os.environ.get("NUM_CONCURRENT_CONNECTIONS", 50)
+)
+LOGGER = logging.getLogger(__name__)
+
+
+class SFTPServer(asyncssh.SFTPServer):
+    def __init__(self: "SFTPServer", conn: asyncssh.SSHServerChannel) -> None:
+        root = DIR_PATH
+        super().__init__(conn, chroot=root)
+
+
+class SSHServer(asyncssh.SSHServer):
+    def __init__(self: "SSHServer", connection_semaphore: Semaphore) -> None:
+        self._connection_semaphore = connection_semaphore
+
+    def password_auth_supported(self: "SSHServer") -> bool:
+        return True
+
+    def validate_password(
+        self: "SSHServer", username: str, password: str
+    ) -> bool:
+        user_passwords = {SFTP_USER: SFTP_PASS}
+        return user_passwords.get(username) == password
+
+    def session_requested(self: "SSHServer") -> bool:
+        return True
+
+    def sftp_requested(self: "SSHServer") -> Type[SFTPServer]:
+        return SFTPServer
+
+    async def begin_auth(self: "SSHServer", username: str) -> bool:
+        await self._connection_semaphore.acquire()
+        return True
+
+    def auth_completed(self: "SSHServer") -> None:
+        self._connection_semaphore.release()
+
+
+async def start_server_async(
+    host: str = SFTP_HOST, port: int = SFTP_PORT
+) -> None:
+    # Generate an SSH keypair or use an existing one
+    server_key = asyncssh.generate_private_key("ssh-rsa")
+
+    # Create a connection semaphore with the desired maximum number of
+    # connections.
+    connection_semaphore = Semaphore(50)
+
+    LOGGER.info(f"Starting SFTP server at {host}:{port}")
+    print(f"start_server_async: Starting SFTP server at {host}:{port}")
+
+    server = await asyncssh.listen(
+        host,
+        port,
+        server_host_keys=[server_key],
+        server_factory=lambda: SSHServer(connection_semaphore),
+        sftp_factory=SFTPServer,
+    )
+
+    async with server:
+        try:
+            await server.wait_closed()
+        except asyncio.CancelledError:
+            pass
+
+
+def start_server(host: str = SFTP_HOST, port: int = SFTP_PORT) -> None:
+    print(f"start_server: Starting SFTP server at {host}:{port}")
+
+    # This function will be run in a new thread
+    def run_loop_in_thread(_loop):
+        asyncio.set_event_loop(_loop)
+        _loop.run_forever()
+
+    # Get the current event loop, create if it doesn't exist
+    loop = asyncio.new_event_loop()
+
+    # Schedule the coroutine to be executed
+    loop.create_task(start_server_async(host=host, port=port))
+
+    # Start a new thread running the loop
+    server_thread = threading.Thread(target=run_loop_in_thread, args=(loop,))
+    server_thread.daemon = True
+    server_thread.start()
+
+
+class SFTPServerManager:
+    def __init__(self, host: str = SFTP_HOST, port: int = SFTP_PORT) -> None:
+        self.loop = asyncio.get_event_loop()
+        self.stop_event = asyncio.Event()
+        self.host = host
+        self.port = port
+
+    async def start_server(self) -> None:
+        # Generate an SSH keypair or use an existing one
+        server_key = asyncssh.generate_private_key("ssh-rsa")
+
+        # Create a connection semaphore with the desired maximum number of
+        # connections.
+        connection_semaphore = Semaphore(50)
+
+        server = await asyncssh.listen(
+            self.host,
+            self.port,
+            server_host_keys=[server_key],
+            server_factory=lambda: SSHServer(connection_semaphore),
+            sftp_factory=SFTPServer,
+        )
+        # Just replace stop_event with self.stop_event
+
+        async with server:
+            try:
+                await self.stop_event.wait()
+            except asyncio.CancelledError:
+                pass
+            finally:
+                server.close()
+                await server.wait_closed()
+
+    def start(self) -> None:
+        self.loop.run_until_complete(self.start_server())
+
+    def stop(self) -> None:
+        self.loop.call_soon_threadsafe(self.stop_event.set)
diff --git a/src/faker_file/tests/test_sftp_server.py b/src/faker_file/tests/test_sftp_server.py
new file mode 100644
index 00000000..ed179ed8
--- /dev/null
+++ b/src/faker_file/tests/test_sftp_server.py
@@ -0,0 +1,255 @@
+import asyncio
+import logging
+import os
+import socket
+import threading
+import time
+from typing import Callable
+from unittest import IsolatedAsyncioTestCase
+
+import asyncssh
+from faker import Faker
+
+from faker_file.providers.txt_file import TxtFileProvider
+
+from .sftp_server import SFTPServerManager, start_server, start_server_async
+from .utils import AutoFreePortInt
+
+__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
+__copyright__ = "2022-2023 Artur Barseghyan"
+__license__ = "MIT"
+__all__ = (
+    "TestSFTPServerWithManager",
+    "TestSFTPServerWithStartServer",
+    "TestSFTPServerWithStartServerAsync",
+)
+
+SFTP_USER = os.environ.get("SFTP_USER", "foo")
+SFTP_PASS = os.environ.get("SFTP_PASS", "pass")
+SFTP_HOST = os.environ.get("SFTP_HOST", "127.0.0.1")
+SFTP_PORT = int(os.environ.get("SFTP_PORT", AutoFreePortInt(host=SFTP_HOST)))
+SFTP_ROOT_PATH = os.environ.get("SFTP_ROOT_PATH", "/upload")
+
+LOGGER = logging.getLogger(__name__)
+
+FAKER = Faker()
+FAKER.add_provider(TxtFileProvider)
+
+
+class __TestSFTPServerMixin:
+    """Test SFTP server mix-in."""
+
+    assertIsInstance: Callable
+    assertRaises: Callable
+    assertEqual: Callable
+    assertTrue: Callable
+    assertFalse: Callable
+    sftp_host: str
+    sftp_port: int
+    sftp_user: str
+    sftp_pass: str
+
+    @staticmethod
+    def is_port_in_use(host: str, port: int) -> bool:
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            return s.connect_ex((host, port)) == 0
+
+    @classmethod
+    def free_port(cls: "__TestSFTPServerMixin") -> None:
+        # Check if the port is in use and wait until it is free
+        while cls.is_port_in_use(cls.sftp_host, cls.sftp_port):
+            LOGGER.info(
+                f"Port {cls.sftp_port} in use on host {cls.sftp_host}, "
+                f"waiting..."
+            )
+            time.sleep(1)
+
+    async def test_successful_connection(self: "__TestSFTPServerMixin") -> None:
+        async with asyncssh.connect(
+            self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            known_hosts=None,
+        ) as conn:
+            async with conn.start_sftp_client() as sftp:
+                self.assertIsInstance(sftp, asyncssh.SFTPClient)
+
+    async def test_failed_connection(self: "__TestSFTPServerMixin") -> None:
+        with self.assertRaises(asyncssh.PermissionDenied):
+            async with asyncssh.connect(
+                self.sftp_host,
+                port=self.sftp_port,
+                username=self.sftp_user,
+                password="wrong_password",
+                known_hosts=None,
+            ):
+                pass
+
+    async def test_file_upload(self: "__TestSFTPServerMixin") -> None:
+        async with asyncssh.connect(
+            self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            known_hosts=None,
+        ) as conn:
+            async with conn.start_sftp_client() as sftp:
+                test_file = FAKER.txt_file()
+                await sftp.put(
+                    test_file.data["filename"], "/testfile_upload.txt"
+                )
+
+                # Read back the file and check its contents
+                async with sftp.open(
+                    "/testfile_upload.txt", "r"
+                ) as uploaded_file:
+                    uploaded_contents = await uploaded_file.read()
+
+                self.assertEqual(test_file.data["content"], uploaded_contents)
+
+    async def test_file_delete(self: "__TestSFTPServerMixin") -> None:
+        async with asyncssh.connect(
+            self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            known_hosts=None,
+        ) as conn:
+            async with conn.start_sftp_client() as sftp:
+                test_file = FAKER.txt_file()
+                await sftp.put(
+                    test_file.data["filename"], "/testfile_delete.txt"
+                )
+
+                # Ensure the file exists
+                self.assertTrue(await sftp.exists("/testfile_delete.txt"))
+
+                # Delete the file and ensure it's gone
+                await sftp.remove("/testfile_delete.txt")
+                self.assertFalse(await sftp.exists("/testfile_delete.txt"))
+
+
+class TestSFTPServerWithStartServerAsync(
+    IsolatedAsyncioTestCase,
+    __TestSFTPServerMixin,
+):
+    sftp_host: str = SFTP_HOST
+    sftp_port: int = int(AutoFreePortInt(host=SFTP_HOST))
+    sftp_user: str = SFTP_USER
+    sftp_pass: str = SFTP_PASS
+
+    @classmethod
+    def setUpClass(cls):
+        # Free port
+        cls.free_port()
+
+        # This function will be run in a new thread
+        def run_loop_in_thread(_loop):
+            asyncio.set_event_loop(_loop)
+            _loop.run_forever()
+
+        # Get the current event loop, create if it doesn't exist
+        loop = asyncio.new_event_loop()
+
+        # Schedule the coroutine to be executed
+        loop.create_task(
+            start_server_async(host=cls.sftp_host, port=cls.sftp_port)
+        )
+
+        # Start a new thread running the loop
+        server_thread = threading.Thread(
+            target=run_loop_in_thread, args=(loop,)
+        )
+        server_thread.daemon = True
+        server_thread.start()
+
+        # Allow some time for the server to start
+        time.sleep(2)
+
+    @classmethod
+    def tearDownClass(cls):
+        # Since the server thread is a daemon, it will be stopped when the
+        # main thread exits.
+        pass
+
+
+class TestSFTPServerWithStartServer(
+    IsolatedAsyncioTestCase,
+    __TestSFTPServerMixin,
+):
+    sftp_host = SFTP_HOST
+    sftp_port = int(AutoFreePortInt(host=SFTP_HOST))
+    sftp_user = SFTP_USER
+    sftp_pass = SFTP_PASS
+
+    @classmethod
+    def setUpClass(cls):
+        # Free port
+        cls.free_port()
+
+        # Note: start_server is not async, and it creates its own thread
+        start_server(host=cls.sftp_host, port=cls.sftp_port)
+
+        # Give server some time to start
+        loop = asyncio.new_event_loop()
+        loop.run_until_complete(asyncio.sleep(2))
+
+    @classmethod
+    def tearDownClass(cls):
+        # Since the server is running in a daemonized thread,
+        # it will be terminated when the main process finishes.
+        # No explicit tear down is required for the server in this test case.
+        pass
+
+
+class TestSFTPServerWithManager(
+    IsolatedAsyncioTestCase,
+    __TestSFTPServerMixin,
+):
+    manager: SFTPServerManager
+    manager_thread: threading.Thread
+    sftp_host = SFTP_HOST
+    sftp_port = int(AutoFreePortInt(host=SFTP_HOST))
+    sftp_user = SFTP_USER
+    sftp_pass = SFTP_PASS
+
+    @classmethod
+    def setUpClass(cls):
+        # Create and set an event loop
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+
+        # Free port
+        cls.free_port()
+
+        cls.manager = SFTPServerManager(host=cls.sftp_host, port=cls.sftp_port)
+        # Starting the manager in a separate thread since it uses
+        # `run_until_complete`.
+        cls.manager_thread = threading.Thread(target=cls.manager.start)
+        cls.manager_thread.daemon = True
+        cls.manager_thread.start()
+
+        # Allow some time for the server to start and check if it's ready
+        max_retries = 100
+        retries = 0
+        while retries < max_retries:
+            try:
+                # Try to establish a connection to the server
+                with socket.create_connection(
+                    (cls.sftp_host, cls.sftp_port), timeout=5
+                ):
+                    LOGGER.info(f"Server started on port {cls.sftp_port}")
+                    break
+            except (ConnectionRefusedError, socket.timeout):
+                LOGGER.info("Waiting for server to start...")
+                retries += 1
+                time.sleep(1)
+        else:
+            raise RuntimeError("Server did not start")
+
+    @classmethod
+    def tearDownClass(cls):
+        # Stop the server
+        cls.manager.stop()
+        cls.manager_thread.join()
diff --git a/src/faker_file/tests/test_sftp_storage.py b/src/faker_file/tests/test_sftp_storage.py
index f78a5a2c..849577b6 100644
--- a/src/faker_file/tests/test_sftp_storage.py
+++ b/src/faker_file/tests/test_sftp_storage.py
@@ -1,5 +1,11 @@
+import logging
 import os
+import socket
+import tempfile
+import threading
+import time
 import unittest
+from functools import partial
 from typing import Any, Dict, Type, Union
 
 from faker import Faker
@@ -7,30 +13,93 @@
 
 from ..providers.txt_file import TxtFileProvider
 from ..storages.sftp_storage import SFTPStorage
+from .sftp_server import SFTPServerManager, start_server
+from .utils import AutoFreePortInt
 
 __author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
 __copyright__ = "2022-2023 Artur Barseghyan"
 __license__ = "MIT"
 __all__ = ("TestSFTPStorageTestCase",)
 
-FAKER = Faker()
-FAKER.add_provider(TxtFileProvider)
 SFTP_USER = os.environ.get("SFTP_USER", "foo")
 SFTP_PASS = os.environ.get("SFTP_PASS", "pass")
-SFTP_HOST = "0.0.0.0"
-SFTP_PORT = 2222
-SFTP_ROOT_PATH = "/upload"
+SFTP_HOST = os.environ.get("SFTP_HOST", "127.0.0.1")
+SFTP_PORT = int(os.environ.get("SFTP_PORT", AutoFreePortInt(host=SFTP_HOST)))
+SFTP_ROOT_PATH = os.environ.get("SFTP_ROOT_PATH", "/upload")
+
+LOGGER = logging.getLogger(__name__)
+
+FAKER = Faker()
+FAKER.add_provider(TxtFileProvider)
 
 
 class TestSFTPStorageTestCase(unittest.TestCase):
-    """Test SFTP storage.
+    """Test SFTP storage."""
+
+    server_manager: SFTPServerManager
+    server_thread: threading.Thread
+    sftp_host: str = SFTP_HOST
+    sftp_port: int = int(AutoFreePortInt(host=SFTP_HOST))
+    sftp_user: str = SFTP_USER
+    sftp_pass: str = SFTP_PASS
+    sftp_root_path: str = SFTP_ROOT_PATH
+
+    # @classmethod
+    # def setUpClass(cls):
+    #     os.makedirs(
+    #         os.path.join(tempfile.gettempdir(), "upload", "sub"),
+    #         exist_ok=True,
+    #     )
+    #     # Start the server in a separate thread
+    #     cls.server_manager = SFTPServerManager()
+    #     cls.server_thread = threading.Thread(target=cls.server_manager.start)
+    #     # Daemonize the thread, so it exits when the main thread exits
+    #     cls.server_thread.daemon = True
+    #     cls.server_thread.start()
+    #     time.sleep(2)
+    #
+    # @classmethod
+    # def tearDownClass(cls):
+    #     # Stop the server when tests are done
+    #     cls.server_manager.stop()
+    #     cls.server_thread.join()  # Wait for the server thread to finish
+
+    @staticmethod
+    def is_port_in_use(host: str, port: int) -> bool:
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            return s.connect_ex((host, port)) == 0
+
+    @classmethod
+    def free_port(cls: "TestSFTPStorageTestCase") -> None:
+        # Check if the port is in use and wait until it is free
+        while cls.is_port_in_use(cls.sftp_host, cls.sftp_port):
+            LOGGER.info(
+                f"Port {cls.sftp_port} in use on host {cls.sftp_host}, "
+                f"waiting..."
+            )
+            time.sleep(1)
 
-    It's assumed that the server is running on port 2222.
+    @classmethod
+    def setUpClass(cls):
+        # Free port
+        cls.free_port()
 
-    Example:
+        os.makedirs(
+            os.path.join(tempfile.gettempdir(), "upload", "sub"),
+            exist_ok=True,
+        )
+
+        # Create a partial function with custom arguments
+        start_server_partial = partial(
+            start_server, host=cls.sftp_host, port=cls.sftp_port
+        )
 
-        docker run -p 2222:22 -d atmoz/sftp foo:pass:::upload
-    """
+        # Start the server in a separate thread
+        cls.server_thread = threading.Thread(target=start_server_partial)
+        # Daemonize the thread, so it exits when the main thread exits
+        cls.server_thread.daemon = True
+        cls.server_thread.start()
+        time.sleep(2)
 
     @parametrize(
         "storage_cls, kwargs, prefix, basename, extension",
@@ -39,11 +108,11 @@ class TestSFTPStorageTestCase(unittest.TestCase):
             (
                 SFTPStorage,
                 {
-                    "host": SFTP_HOST,
-                    "port": SFTP_PORT,
-                    "username": SFTP_USER,
-                    "password": SFTP_PASS,
-                    "root_path": SFTP_ROOT_PATH,
+                    "host": sftp_host,
+                    "port": sftp_port,
+                    "username": sftp_user,
+                    "password": sftp_pass,
+                    "root_path": sftp_root_path,
                 },
                 "zzz",
                 None,
@@ -52,11 +121,11 @@ class TestSFTPStorageTestCase(unittest.TestCase):
             (
                 SFTPStorage,
                 {
-                    "host": SFTP_HOST,
-                    "port": SFTP_PORT,
-                    "username": SFTP_USER,
-                    "password": SFTP_PASS,
-                    "root_path": SFTP_ROOT_PATH,
+                    "host": sftp_host,
+                    "port": sftp_port,
+                    "username": sftp_user,
+                    "password": sftp_pass,
+                    "root_path": sftp_root_path,
                 },
                 None,
                 "my_zzz_filename",
@@ -103,11 +172,11 @@ def test_storage(
             (
                 SFTPStorage,
                 {
-                    "host": SFTP_HOST,
-                    "port": SFTP_PORT,
-                    "username": SFTP_USER,
-                    "password": SFTP_PASS,
-                    "root_path": SFTP_ROOT_PATH,
+                    "host": sftp_host,
+                    "port": sftp_port,
+                    "username": sftp_user,
+                    "password": sftp_pass,
+                    "root_path": sftp_root_path,
                 },
                 "zzz",
                 "",
@@ -138,42 +207,46 @@ def test_file_system_storage_abspath(
         """Test `FileSystemStorage` `abspath`."""
 
         storage = SFTPStorage(
-            host=SFTP_HOST,
-            port=SFTP_PORT,
-            username=SFTP_USER,
-            password=SFTP_PASS,
-            root_path=SFTP_ROOT_PATH,
+            host=self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            root_path=self.sftp_root_path,
         )
         filename = storage.generate_filename(prefix="", extension="tmp")
-        self.assertTrue(storage.abspath(filename).startswith(SFTP_ROOT_PATH))
+        self.assertTrue(
+            storage.abspath(filename).startswith(self.sftp_root_path)
+        )
 
     def test_integration(self: "TestSFTPStorageTestCase") -> None:
         storage = SFTPStorage(
-            host=SFTP_HOST,
-            port=SFTP_PORT,
-            username=SFTP_USER,
-            password=SFTP_PASS,
-            root_path=SFTP_ROOT_PATH,
+            host=self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            root_path=self.sftp_root_path,
         )
         file = FAKER.txt_file(storage=storage)
         self.assertTrue(storage.exists(file.data["filename"]))
         self.assertTrue(
-            storage.abspath(file.data["filename"]).startswith(SFTP_ROOT_PATH)
+            storage.abspath(file.data["filename"]).startswith(
+                self.sftp_root_path
+            )
         )
 
     def test_integration_sub_dir(self: "TestSFTPStorageTestCase") -> None:
         storage = SFTPStorage(
-            host=SFTP_HOST,
-            port=SFTP_PORT,
-            username=SFTP_USER,
-            password=SFTP_PASS,
-            root_path=SFTP_ROOT_PATH,
+            host=self.sftp_host,
+            port=self.sftp_port,
+            username=self.sftp_user,
+            password=self.sftp_pass,
+            root_path=self.sftp_root_path,
             rel_path="sub",
         )
         file = FAKER.txt_file(storage=storage)
         self.assertTrue(storage.exists(file.data["filename"]))
         self.assertTrue(
             storage.abspath(file.data["filename"]).startswith(
-                os.path.join(SFTP_ROOT_PATH, "sub")
+                os.path.join(self.sftp_root_path, "sub")
             )
         )
diff --git a/src/faker_file/tests/utils.py b/src/faker_file/tests/utils.py
new file mode 100644
index 00000000..0d4308f2
--- /dev/null
+++ b/src/faker_file/tests/utils.py
@@ -0,0 +1,99 @@
+import random
+import socket
+
+__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
+__copyright__ = "2022-2023 Artur Barseghyan"
+__license__ = "MIT"
+__all__ = (
+    "AutoIncPortInt",
+    "AutoFreePortInt",
+)
+
+
+class AutoIncPortInt(int):
+    """Automatically incremented integer value.
+
+    Contains state of issued values.
+    Starts from 2223. Every time initialized, value increases.
+    For instance:
+
+        int1 = AutoInt()  # 2223
+        int2 = AutoInt()  # 2224
+        int3 = AutoInt()  # 2225
+
+    For the rest, it behaves like a normal integer.
+    """
+
+    # This is a class attribute that keeps the current state
+    _next_value = 2223
+
+    def __new__(cls, *args, **kwargs):
+        """Create a new instance of AutoInt with the next value."""
+        # Use __new__ because int is immutable, so we need to set the value
+        # before creating the instance.
+        instance = super().__new__(cls, cls._next_value)
+        # Increment the class attribute for the next instance
+        cls._next_value += 1
+        # Return the new instance
+        return instance
+
+
+class AutoFreePortInt(int):
+    """Automatically and randomly picks a free port within a specified range.
+
+    Contains state of issued ports.
+    For instance:
+
+        # Random free port between default range 2223 and 5000
+        port1 = AutoInt()
+
+        # Random free port between 3000 and 4000
+        port2 = AutoInt(min_port=3000, max_port=4000)
+
+    For the rest, it behaves like a normal integer.
+    """
+
+    # Default port range
+    DEFAULT_MIN_PORT: int = 2223
+    DEFAULT_MAX_PORT: int = 5000
+
+    @staticmethod
+    def _is_port_free(host: str, port: int) -> bool:
+        """Check if the given port is free."""
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            try:
+                # Try to bind the socket to the port
+                s.bind((host, port))
+                return True
+            except OSError:
+                return False
+
+    def __new__(
+        cls,
+        min_port: int = DEFAULT_MIN_PORT,
+        max_port: int = DEFAULT_MAX_PORT,
+        host: str = "localhost",
+        *args,
+        **kwargs,
+    ):
+        """Create a new instance of AutoFreePortInt.
+
+        With a free random port within the specified range.
+        """
+        # Validate the range
+        if not (
+            isinstance(min_port, int)
+            and isinstance(max_port, int)
+            and min_port <= max_port
+        ):
+            raise ValueError("Invalid port range specified")
+
+        # Pick a random port within the specified range and check if it's free
+        while True:
+            port = random.randint(min_port, max_port)
+            if cls._is_port_free(host, port):
+                break
+
+        # Use __new__ to create the instance with the free port as the value
+        instance = super().__new__(cls, port)
+        return instance