Test: Basic freshclam CDIFF tests

Adds 3 tests to validate that:

1. a CDIFF update works

2. a CDIFF partial update (with 1 missing CDIFF) works
   and that a subsequent update is ok with being 1 behind

3. a CDIFF partial update (with 2 missing CDIFFs) works
   and that a subsequent update will try to get the WHOLE CVD -
   because being 2+ CDIFFs behind without any update isn't good enough.

Also fixed a minor bug so that the database name is properly displayed
when a partial update occurs instead of displaying "(null)".

Also changed the freshclam test port to 8001 to deconflict with
CVD-Update, in case that's running in the background.

TODO: Make the tests smarter so they find an open port instead of
hoping that 8001 is available.
pull/176/head
Micah Snyder 4 years ago
parent b5e4d95373
commit cbe60b30b0
  1. 1
      libfreshclam/libfreshclam_internal.c
  2. 276
      unit_tests/freshclam_test.py
  3. BIN
      unit_tests/test_db/test-1.cvd
  4. BIN
      unit_tests/test_db/test-2.cdiff
  5. BIN
      unit_tests/test_db/test-2.cvd
  6. BIN
      unit_tests/test_db/test-3.cdiff
  7. BIN
      unit_tests/test_db/test-3.cvd
  8. BIN
      unit_tests/test_db/test-4.cdiff
  9. BIN
      unit_tests/test_db/test-4.cvd
  10. BIN
      unit_tests/test_db/test-5.cdiff
  11. BIN
      unit_tests/test_db/test-5.cvd
  12. BIN
      unit_tests/test_db/test-6.cdiff
  13. BIN
      unit_tests/test_db/test-6.cvd

@ -2345,6 +2345,7 @@ fc_error_t updatedb(
newLocalFilename = cli_strdup(remoteFilename);
} else if (0 == numPatchesReceived) {
logg("The database server doesn't have the latest patch for the %s database (version %u). The server will likely have updated if you check again in a few hours.\n", database, remoteVersion);
*dbFilename = cli_strdup(localFilename);
goto up_to_date;
} else {
/*

@ -14,6 +14,7 @@ import subprocess
import sys
import time
import unittest
from functools import partial
from http.server import HTTPServer, BaseHTTPRequestHandler
import cgi
@ -32,16 +33,13 @@ class TC(testcase.TestCase):
# Prepare a directory to host our test databases
TC.path_www = Path(TC.path_tmp, 'www')
TC.path_www.mkdir()
shutil.copy(
str(TC.path_build / 'unit_tests' / 'clamav.hdb'),
str(TC.path_www),
)
TC.path_db = Path(TC.path_tmp, 'database')
TC.freshclam_pid = Path(TC.path_tmp, 'freshclam-test.pid')
TC.freshclam_config = Path(TC.path_tmp, 'freshclam-test.conf')
TC.mock_mirror_port = 8000
TC.mock_mirror_port = 8001 # Chosen instead of 8000 because CVD-Update tool serves on 8000 by default.
# TODO: Ideally we'd find an open port to use for these tests instead of crossing our fingers.
TC.mock_mirror = None
@classmethod
@ -56,8 +54,19 @@ class TC(testcase.TestCase):
TC.mock_mirror.terminate()
TC.mock_mirror = None
if (TC.path_db / 'freshclam.dat').exists():
os.remove(str(TC.path_db / 'freshclam.dat'))
# Clear the database directory
try:
shutil.rmtree(self.path_db)
except Exception:
pass
self.path_db.mkdir()
# Clear the www directory
try:
shutil.rmtree(self.path_www)
except Exception:
pass
self.path_www.mkdir()
super(TC, self).tearDown()
self.verify_valgrind_log()
@ -94,6 +103,12 @@ class TC(testcase.TestCase):
if TC.freshclam_config.exists():
os.remove(str(TC.freshclam_config))
# Select database files for test
shutil.copy(
str(TC.path_build / 'unit_tests' / 'clamav.hdb'),
str(TC.path_www),
)
TC.freshclam_config.write_text('''
DatabaseMirror http://localhost:{port}
PidFile {freshclam_pid}
@ -242,7 +257,205 @@ class TC(testcase.TestCase):
]
self.verify_output(output.out, expected=expected_results)
def mock_database_mirror(handler, port=8000):
def test_freshclam_05_cdiff_update(self):
self.step_name('Verify that freshclam can update from an older CVD to a newer with CDIFF patches')
# start with this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-1.cvd', TC.path_db / 'test.cvd')
# update to this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cvd', TC.path_www / 'test.cvd')
# using these CDIFFs
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-2.cdiff', TC.path_www)
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-3.cdiff', TC.path_www)
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-4.cdiff', TC.path_www)
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-5.cdiff', TC.path_www)
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cdiff', TC.path_www)
handler = partial(WebServerHandler_WWW, TC.path_www)
TC.mock_mirror = Process(target=mock_database_mirror, args=(handler, TC.mock_mirror_port))
TC.mock_mirror.start()
if TC.freshclam_config.exists():
os.remove(str(TC.freshclam_config))
TC.freshclam_config.write_text('''
DatabaseMirror http://localhost:{port}
DNSDatabaseInfo no
PidFile {freshclam_pid}
LogVerbose yes
LogFileMaxSize 0
LogTime yes
DatabaseDirectory {path_db}
DatabaseOwner {user}
'''.format(
freshclam_pid=TC.freshclam_pid,
path_db=TC.path_db,
port=TC.mock_mirror_port,
user=getpass.getuser(),
))
command = '{valgrind} {valgrind_args} {freshclam} --config-file={freshclam_config} --update-db=test'.format(
valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, freshclam=TC.freshclam, freshclam_config=TC.freshclam_config
)
output = self.execute_command(command)
assert output.ec == 0 # success
expected_results = [
'test.cld updated',
]
unexpected_results = [
'already up-to-date'
]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
def test_freshclam_06_cdiff_partial_minus_1(self):
self.step_name('Verify that freshclam can update from an older CVD to a newer with CDIFF patches')
# start with this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-3.cvd', TC.path_db / 'test.cvd')
# update to this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cvd', TC.path_www / 'test.cvd')
# using these CDIFFs
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-4.cdiff', TC.path_www)
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-5.cdiff', TC.path_www)
#shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cdiff', TC.path_www) # <-- don't give them the last CDIFF
handler = partial(WebServerHandler_WWW, TC.path_www)
TC.mock_mirror = Process(target=mock_database_mirror, args=(handler, TC.mock_mirror_port))
TC.mock_mirror.start()
if TC.freshclam_config.exists():
os.remove(str(TC.freshclam_config))
TC.freshclam_config.write_text('''
DatabaseMirror http://localhost:{port}
DNSDatabaseInfo no
PidFile {freshclam_pid}
LogVerbose yes
LogFileMaxSize 0
LogTime yes
DatabaseDirectory {path_db}
DatabaseOwner {user}
'''.format(
freshclam_pid=TC.freshclam_pid,
path_db=TC.path_db,
port=TC.mock_mirror_port,
user=getpass.getuser(),
))
command = '{valgrind} {valgrind_args} {freshclam} --config-file={freshclam_config} --update-db=test'.format(
valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, freshclam=TC.freshclam, freshclam_config=TC.freshclam_config
)
output = self.execute_command(command)
assert output.ec == 0 # success
expected_results = [
'Downloaded 2 patches for test, which is fewer than the 3 expected patches',
'We\'ll settle for this partial-update, at least for now',
'test.cld updated',
]
unexpected_results = [
'already up-to-date'
]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
#
# Try again, we should be 1 behind which is tolerable and should not trigger a full CVD download
#
command = '{valgrind} {valgrind_args} {freshclam} --config-file={freshclam_config} --update-db=test'.format(
valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, freshclam=TC.freshclam, freshclam_config=TC.freshclam_config
)
output = self.execute_command(command)
assert output.ec == 0 # success
expected_results = [
'The database server doesn\'t have the latest patch',
'The server will likely have updated if you check again in a few hours',
]
unexpected_results = [
'test.cld updated',
'test.cvd updated',
]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
def test_freshclam_07_cdiff_partial_minus_2(self):
self.step_name('Verify that freshclam can update from an older CVD to a newer with CDIFF patches')
# start with this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-3.cvd', TC.path_db / 'test.cvd')
# update to this CVD
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cvd', TC.path_www / 'test.cvd')
# using these CDIFFs
shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-4.cdiff', TC.path_www)
# shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-5.cdiff', TC.path_www) <--- dont' give them the second to last, either!
# shutil.copy(TC.path_source / 'unit_tests' / 'test_db' / 'test-6.cdiff', TC.path_www) <--- don't give them the last CDIFF
handler = partial(WebServerHandler_WWW, TC.path_www)
TC.mock_mirror = Process(target=mock_database_mirror, args=(handler, TC.mock_mirror_port))
TC.mock_mirror.start()
if TC.freshclam_config.exists():
os.remove(str(TC.freshclam_config))
TC.freshclam_config.write_text('''
DatabaseMirror http://localhost:{port}
DNSDatabaseInfo no
PidFile {freshclam_pid}
LogVerbose yes
LogFileMaxSize 0
LogTime yes
DatabaseDirectory {path_db}
DatabaseOwner {user}
'''.format(
freshclam_pid=TC.freshclam_pid,
path_db=TC.path_db,
port=TC.mock_mirror_port,
user=getpass.getuser(),
))
command = '{valgrind} {valgrind_args} {freshclam} --config-file={freshclam_config} --update-db=test'.format(
valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, freshclam=TC.freshclam, freshclam_config=TC.freshclam_config
)
output = self.execute_command(command)
assert output.ec == 0 # success
expected_results = [
'Downloaded 1 patches for test, which is fewer than the 3 expected patches',
'We\'ll settle for this partial-update, at least for now',
'test.cld updated',
]
unexpected_results = [
'already up-to-date'
]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
#
# Try again, we should be 2 behind which is NOT tolerable and SHOULD trigger a full CVD download
#
command = '{valgrind} {valgrind_args} {freshclam} --config-file={freshclam_config} --update-db=test'.format(
valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, freshclam=TC.freshclam, freshclam_config=TC.freshclam_config
)
output = self.execute_command(command)
assert output.ec == 0 # success
expected_results = [
'Incremental update failed, trying to download test.cvd',
'test.cvd updated',
]
unexpected_results = [
'test.cld updated',
]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
def mock_database_mirror(handler, port=8001):
'''
Process entry point for our HTTP Server to mock a database mirror.
'''
@ -309,3 +522,50 @@ class WebServerHandler_04(BaseHTTPRequestHandler):
Retry later please!
</body></html>'''
self.wfile.write(page)
class WebServerHandler_WWW(BaseHTTPRequestHandler):
'''
Make an HTTP server handler that has a configurable directory for hosting files.
Server handler to send a CVD header indicating an update is available,
and then to serve up CDIFFs that should allow the test to do an incremental update.
'''
def __init__(self, path_www, *args, **kwargs):
self.path_www = path_www
super().__init__(*args, **kwargs)
def do_GET(self):
requested_file = self.path_www / self.path.lstrip('/')
print("Mock Server: Test requested: {}".format(requested_file))
if not requested_file.exists():
self.send_error(404, "{} Not Found".format(self.path.lstrip('/')))
elif 'Range' in self.headers:
# This will send a CVD header so FreshClam thinks there is an update.
(range_start, range_end) = self.headers['Range'].split('=')[-1].split('-')
print("Mock Server: But they only want bytes {} through {} ...".format(range_start, range_end))
with requested_file.open('rb') as the_file:
self.send_response(206) # Partial file
self.send_header('Content-type', 'application/octet-stream')
self.end_headers()
the_file.seek(int(range_start))
page = the_file.read(int(range_end) - int(range_start) + 1)
bytes_written = self.wfile.write(page)
print("Mock Server: Sending {} bytes back to client.".format(bytes_written))
else:
# Send back some whole files
with requested_file.open('rb') as the_file:
self.send_response(200) # Partial file
self.send_header('Content-type', 'application/octet-stream')
self.end_headers()
page = the_file.read()
bytes_written = self.wfile.write(page)
print("Mock Server: Sending {} bytes back to client.".format(bytes_written))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save