Skip to content

Add tests for FtpLibrary - 10 tests, 100% coverage of ftp.py#1487

Open
Raavi29 wants to merge 5 commits intoOWASP:masterfrom
Raavi29:add-ftp-tests
Open

Add tests for FtpLibrary - 10 tests, 100% coverage of ftp.py#1487
Raavi29 wants to merge 5 commits intoOWASP:masterfrom
Raavi29:add-ftp-tests

Conversation

@Raavi29
Copy link
Copy Markdown

@Raavi29 Raavi29 commented Apr 1, 2026

Proposed change

Summary

Adds unit tests for nettacker/core/lib/ftp.py, bringing coverage from 0% to 100% (12 statements, 0 missed).

Tests Added

  • TestFtpLibraryBruteForce - 8 tests covering:
    1. Successful login returns correct dict with host/port/username/password
    2. connect(), login(), close() each called with correct arguments
    3. ftplib.error_perm raised on wrong credentials
    4. TimeoutError raised on connection timeout
    5. TImeout value correctly passed to FTP constructor
  • TestFtpEngine - 2 tests for engine instantiation and library assignment

Approach

FtpLibrary uses self.client (assigned to ftplib.FTP) rather than calling ftplib.FTP directly. All tests use patch.object(FtpLibrary, 'client') to intercept network calls - no real FTP connections made.

Tests run

python -m pytest tests/core/lib/test_ftp.py -v — 10 passed

Type of change

  • New core framework functionality
  • Bugfix (non-breaking change which fixes an issue)
  • Code refactoring without any functionality changes
  • New or existing module/payload change
  • Documentation/localization improvement
  • Test coverage improvement
  • Dependency upgrade
  • Other improvement (best practice, cleanup, optimization, etc)

Checklist

  • I've followed the contributing guidelines
  • I have digitally signed all my commits in this PR
  • I've run make pre-commit and confirm it didn't generate any warnings/changes
  • I've run make test, I confirm all tests passed locally
  • I've added/updated any relevant documentation in the docs/ folder
  • I've linked this PR with an open issue
  • I've tested and verified that my code works as intended and resolves the issue as described
  • I have attached screenshots demonstrating my code works as intended
  • I've checked all other open PRs to avoid submitting duplicate work
  • I confirm that the code and comments in this PR are not direct unreviewed outputs of AI
  • I confirm that I am the Sole Responsible Author for every line of code, comment, and design decision

Raavi29 added 5 commits March 25, 2026 08:21
- Tests weak algorithms: sha1, md5, md2, md4
- Tests case insensitivity (uppercase input)
- Tests safe algorithms: sha256, sha512, sha384
- Tests edge cases: empty string, random string
- All 11 tests passing

Part of improving test coverage for GSoC 2026
- TestIsSingleIPv4: 12 tests for IPv4 address validation
- TestIsSingleIPv6: 10 tests including None bug documentation
- TestIsIPv4Range: 8 tests (documents naming swap with is_ipv4_cidr)
- TestIsIPv4CIDR: 7 tests (documents naming swap with is_ipv4_range)
- TestIsIPv6Range: 6 tests for IPv6 dash-range detection
- TestIsIPv6CIDR: 8 tests for IPv6 CIDR detection
- TestGenerateIPRange: 7 tests covering both code branches
- TestGetIPRange: 4 tests using unittest.mock for HTTP isolation

Coverage: nettacker/core/ip.py 0% -> 83%
Note: is_ipv4_range/is_ipv4_cidr and is_ipv6_range/is_ipv6_cidr
appear to have swapped names - documented in test docstrings
- TestFtpLibraryBruteForce: 8 tests covering successful login,
  method call verification, error_perm on wrong password,
  timeout handling, and return value structure
- TestFtpEngine: 2 tests for engine instantiation and library assignment
- All network calls mocked via patch.object(FtpLibrary, 'client')

Coverage: nettacker/core/lib/ftp.py 0% -> 100%
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Summary by CodeRabbit

  • Tests

    • Added comprehensive test coverage for FTP connectivity and authentication features with mock-based verification
    • Added new test suite for IP address parsing and range generation utilities with edge case handling
    • Updated hash algorithm validation tests
  • Chores

    • Updated security analysis workflow action versions

Walkthrough

This PR pins the CodeQL GitHub Action to a new version and adds comprehensive test coverage for FTP library functionality, IP utility parsing, and weak hash algorithm detection, while significantly refactoring existing SSL test coverage.

Changes

Cohort / File(s) Summary
CI/CD Workflow
.github/workflows/ci_cd.yml
Updated CodeQL action version pins for "Initialize CodeQL" and "Perform CodeQL Analysis" steps.
FTP Library Tests
tests/core/lib/test_ftp.py
Added new test module with two test classes: TestFtpLibraryBruteForce covering brute-force logic, connection flow, error propagation, and timeout argument forwarding via mocked FTP client; TestFtpEngine validating engine instantiation and library assignment.
SSL Test Refactoring
tests/core/lib/test_ssl.py
Removed comprehensive test coverage for SslEngine, SslLibrary, socket/cipher scanning, certificate scanning, and response conditions (555 lines). Replaced with focused tests for is_weak_hash_algo function covering weak algorithm detection, case-insensitivity, and edge cases (39 lines).
IP Utility Tests
tests/test_ip.py
Added new test suite for IPv4/IPv6 parsing, CIDR/range generation, and IP range utilities, including mocked network request handling for fallback scenarios and comprehensive coverage of valid/invalid input formats.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1353: Pins CodeQL GitHub Action to specific version in CI/CD workflow alongside other action version updates.

Suggested reviewers

  • securestep9
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding 10 tests for FtpLibrary with 100% code coverage of ftp.py, which aligns with the primary objective of the PR.
Description check ✅ Passed The description is detailed and directly related to the changeset, providing context about the tests added, testing approach, and verification that tests pass, all aligned with the FTP test coverage improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/core/lib/test_ftp.py (1)

65-75: Minor redundancy with test_successful_login_returns_dict.

This test checks that keys exist in the result, but test_successful_login_returns_dict already implicitly verifies this by asserting specific values for the same keys. Consider consolidating or removing this test to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/lib/test_ftp.py` around lines 65 - 75, The
test_result_contains_all_keys test is redundant because
test_successful_login_returns_dict already asserts the same keys/values; remove
test_result_contains_all_keys (or merge its intent into
test_successful_login_returns_dict) to avoid duplicate assertions—locate the
test function named test_result_contains_all_keys in tests/core/lib/test_ftp.py
that patches FtpLibrary.client and either delete that test or replace it with a
focused assertion (e.g., only validate structure differences not covered by
test_successful_login_returns_dict) so the suite remains concise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/lib/test_ssl.py`:
- Around line 19-38: Add a test that verifies non-prefix matches for the current
"unsafe_algo in algo" behavior of is_weak_hash_algo by asserting that an
algorithm string with the weak token not at the start (e.g.,
"rsaWithSHA1Encryption") returns True; locate the test cases in
tests/core/lib/test_ssl.py alongside the other weak-hash tests and add a new
test method (mirroring naming style like test_sha1_uppercase_is_weak) that calls
is_weak_hash_algo("rsaWithSHA1Encryption") and asserts True so the suite will
detect changes from containment to startswith logic.

In `@tests/test_ip.py`:
- Around line 261-296: Add a success-path unit test (e.g.,
test_success_returns_generated_range) that mocks nettacker.core.ip.requests.get
to return a valid API payload (set return_value.content to JSON bytes matching
the structure get_ip_range expects), call get_ip_range with a sample IP, and
assert the result is the non-fallback range by comparing it to the output of
generate_ip_range for that sample IP (i.e., result ==
generate_ip_range(sample_ip) and result != [sample_ip]); use the same patch
style as the existing tests.
- Line 1: Move the test file so it mirrors the package layout: create
tests/core/ and move tests/test_ip.py to tests/core/test_ip.py; then update the
test's import(s) to import the production module using its package path (e.g.,
from nettacker.core.ip import <function_or_class_under_test> or import
nettacker.core.ip as ip) so pytest discovers it under the mirrored layout and
the tests reference the correct module symbols.
- Around line 93-97: The test currently catches any Exception using
pytest.raises((TypeError, Exception)); narrow this to the specific expected
exception by replacing the tuple with TypeError only so test_none_raises asserts
that is_single_ipv6(None) raises a TypeError (the error coming from
netaddr.valid_ipv6(None)), updating the pytest.raises usage in the
test_none_raises function accordingly.

---

Nitpick comments:
In `@tests/core/lib/test_ftp.py`:
- Around line 65-75: The test_result_contains_all_keys test is redundant because
test_successful_login_returns_dict already asserts the same keys/values; remove
test_result_contains_all_keys (or merge its intent into
test_successful_login_returns_dict) to avoid duplicate assertions—locate the
test function named test_result_contains_all_keys in tests/core/lib/test_ftp.py
that patches FtpLibrary.client and either delete that test or replace it with a
focused assertion (e.g., only validate structure differences not covered by
test_successful_login_returns_dict) so the suite remains concise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 008303d2-a33b-4a14-a10f-73a78e9185a6

📥 Commits

Reviewing files that changed from the base of the PR and between f4c1bbf and b92e5c6.

📒 Files selected for processing (4)
  • .github/workflows/ci_cd.yml
  • tests/core/lib/test_ftp.py
  • tests/core/lib/test_ssl.py
  • tests/test_ip.py

Comment on lines +19 to +38
def test_sha1_is_weak(self):
assert is_weak_hash_algo("sha1WithRSAEncryption") is True

def version(self):
return self.Version
def test_md5_is_weak(self):
assert is_weak_hash_algo("md5WithRSAEncryption") is True

def test_md2_is_weak(self):
assert is_weak_hash_algo("md2WithRSAEncryption") is True

class SubjectObject:
def __init__(self, subject="subject"):
self.subject = subject
def test_md4_is_weak(self):
assert is_weak_hash_algo("md4WithRSAEncryption") is True

def get_components(self):
return [
(b"component", str.encode(self.subject)),
]
# --- Case insensitivity — function lowercases input, so these must also work ---

def test_sha1_uppercase_is_weak(self):
# The function does algo.lower() so uppercase should still be caught
assert is_weak_hash_algo("SHA1WithRSAEncryption") is True

class IssuerObject:
def __init__(self, issuer="issuer"):
self.issuer = issuer
def test_md5_uppercase_is_weak(self):
assert is_weak_hash_algo("MD5WithRSAEncryption") is True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add one non-prefix weak-hash case.

nettacker/core/lib/ssl.py:13-18 uses unsafe_algo in algo, not prefix matching. Since every positive input here starts with the weak token, a future change to startswith() would still pass this suite while changing runtime behavior. A case like rsaWithSHA1Encryption would pin the current semantics.

💡 Suggested addition
     def test_md5_uppercase_is_weak(self):
         assert is_weak_hash_algo("MD5WithRSAEncryption") is True
+
+    def test_sha1_in_middle_is_weak(self):
+        assert is_weak_hash_algo("rsaWithSHA1Encryption") is True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_sha1_is_weak(self):
assert is_weak_hash_algo("sha1WithRSAEncryption") is True
def version(self):
return self.Version
def test_md5_is_weak(self):
assert is_weak_hash_algo("md5WithRSAEncryption") is True
def test_md2_is_weak(self):
assert is_weak_hash_algo("md2WithRSAEncryption") is True
class SubjectObject:
def __init__(self, subject="subject"):
self.subject = subject
def test_md4_is_weak(self):
assert is_weak_hash_algo("md4WithRSAEncryption") is True
def get_components(self):
return [
(b"component", str.encode(self.subject)),
]
# --- Case insensitivity — function lowercases input, so these must also work ---
def test_sha1_uppercase_is_weak(self):
# The function does algo.lower() so uppercase should still be caught
assert is_weak_hash_algo("SHA1WithRSAEncryption") is True
class IssuerObject:
def __init__(self, issuer="issuer"):
self.issuer = issuer
def test_md5_uppercase_is_weak(self):
assert is_weak_hash_algo("MD5WithRSAEncryption") is True
def test_sha1_is_weak(self):
assert is_weak_hash_algo("sha1WithRSAEncryption") is True
def test_md5_is_weak(self):
assert is_weak_hash_algo("md5WithRSAEncryption") is True
def test_md2_is_weak(self):
assert is_weak_hash_algo("md2WithRSAEncryption") is True
def test_md4_is_weak(self):
assert is_weak_hash_algo("md4WithRSAEncryption") is True
# --- Case insensitivity — function lowercases input, so these must also work ---
def test_sha1_uppercase_is_weak(self):
# The function does algo.lower() so uppercase should still be caught
assert is_weak_hash_algo("SHA1WithRSAEncryption") is True
def test_md5_uppercase_is_weak(self):
assert is_weak_hash_algo("MD5WithRSAEncryption") is True
def test_sha1_in_middle_is_weak(self):
assert is_weak_hash_algo("rsaWithSHA1Encryption") is True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/lib/test_ssl.py` around lines 19 - 38, Add a test that verifies
non-prefix matches for the current "unsafe_algo in algo" behavior of
is_weak_hash_algo by asserting that an algorithm string with the weak token not
at the start (e.g., "rsaWithSHA1Encryption") returns True; locate the test cases
in tests/core/lib/test_ssl.py alongside the other weak-hash tests and add a new
test method (mirroring naming style like test_sha1_uppercase_is_weak) that calls
is_weak_hash_algo("rsaWithSHA1Encryption") and asserts True so the suite will
detect changes from containment to startswith logic.

@@ -0,0 +1,296 @@
import pytest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move this test file to mirror the package layout.

nettacker/core/ip.py should be tested from a mirrored path (for example tests/core/test_ip.py), not tests/test_ip.py.

As per coding guidelines, tests/**/*.py: Tests must be located in tests/ directory and mirror the package layout with pattern tests/core/, tests/lib/, etc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_ip.py` at line 1, Move the test file so it mirrors the package
layout: create tests/core/ and move tests/test_ip.py to tests/core/test_ip.py;
then update the test's import(s) to import the production module using its
package path (e.g., from nettacker.core.ip import <function_or_class_under_test>
or import nettacker.core.ip as ip) so pytest discovers it under the mirrored
layout and the tests reference the correct module symbols.

Comment on lines +93 to +97
def test_none_raises(self):
# No str() wrapper in source — None causes TypeError
# We document this bug with pytest.raises
with pytest.raises((TypeError, Exception)):
is_single_ipv6(None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where broad raises are used and inspect target implementation.
rg -n --type=py -C2 'pytest\.raises\(\(TypeError,\s*Exception\)\)'
fd 'ip.py$' | xargs -I{} rg -n -C4 'def is_single_ipv6|valid_ipv6' {}

Repository: OWASP/Nettacker

Length of output: 1119


Narrow pytest.raises to a specific exception type.

pytest.raises((TypeError, Exception)) is ineffective—it's equivalent to catching Exception, which can mask unrelated failures. The test documentation explicitly notes that only TypeError is expected from netaddr.valid_ipv6(None).

Proposed fix
-        with pytest.raises((TypeError, Exception)):
+        with pytest.raises(TypeError):
             is_single_ipv6(None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_none_raises(self):
# No str() wrapper in source — None causes TypeError
# We document this bug with pytest.raises
with pytest.raises((TypeError, Exception)):
is_single_ipv6(None)
def test_none_raises(self):
# No str() wrapper in source — None causes TypeError
# We document this bug with pytest.raises
with pytest.raises(TypeError):
is_single_ipv6(None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_ip.py` around lines 93 - 97, The test currently catches any
Exception using pytest.raises((TypeError, Exception)); narrow this to the
specific expected exception by replacing the tuple with TypeError only so
test_none_raises asserts that is_single_ipv6(None) raises a TypeError (the error
coming from netaddr.valid_ipv6(None)), updating the pytest.raises usage in the
test_none_raises function accordingly.

Comment on lines +261 to +296
"""
get_ip_range() makes a live HTTP request to RIPE's API.
We mock requests.get to avoid network calls in tests.
Two behaviours to test:
1. Successful API response — returns generate_ip_range() result
2. Any failure (network down, bad JSON, missing key) — returns [ip]
"""

def test_exception_returns_ip_as_list(self):
# When anything goes wrong, function returns [ip] as fallback
# We simulate failure by making requests.get raise an exception
with patch("nettacker.core.ip.requests.get") as mock_get:
mock_get.side_effect = Exception("network error")
result = get_ip_range("8.8.8.8")
assert result == ["8.8.8.8"]

def test_invalid_json_returns_ip_as_list(self):
# Bad JSON response — json.loads will fail, fallback kicks in
with patch("nettacker.core.ip.requests.get") as mock_get:
mock_get.return_value.content = b"not valid json"
result = get_ip_range("1.1.1.1")
assert result == ["1.1.1.1"]

def test_missing_key_returns_ip_as_list(self):
# Valid JSON but wrong structure — KeyError triggers fallback
with patch("nettacker.core.ip.requests.get") as mock_get:
mock_get.return_value.content = b'{"unexpected": "structure"}'
result = get_ip_range("9.9.9.9")
assert result == ["9.9.9.9"]

def test_returns_list_type(self):
# Whatever happens, result must always be a list
with patch("nettacker.core.ip.requests.get") as mock_get:
mock_get.side_effect = Exception("timeout")
result = get_ip_range("8.8.8.8")
assert isinstance(result, list) No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a success-path test for get_ip_range.

This block validates only fallback/error behavior. Please add one test that mocks a valid API payload and asserts the non-fallback returned range, so the primary behavior is guarded too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_ip.py` around lines 261 - 296, Add a success-path unit test (e.g.,
test_success_returns_generated_range) that mocks nettacker.core.ip.requests.get
to return a valid API payload (set return_value.content to JSON bytes matching
the structure get_ip_range expects), call get_ip_range with a sample IP, and
assert the result is the non-fallback range by comparing it to the output of
generate_ip_range for that sample IP (i.e., result ==
generate_ip_range(sample_ip) and result != [sample_ip]); use the same patch
style as the existing tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant