unit test_Downloader;

{$mode objfpc}{$H+}

/// <summary>
/// test_Downloader — FPCUnit tests for uHCLDownloader.
///
/// Exercises the SHA-256 hash computation (<see cref="HCLComputeSHA256"/>)
/// using NIST FIPS 180-4 known-answer test vectors (byte-exact expected
/// digests), plus edge-case coverage (empty file, non-existent path,
/// multi-block input).  Also covers the pure download-naming functions
/// (<see cref="HCLSafeFileName"/>, <see cref="HCLContentDispositionFileName"/>,
/// <see cref="HCLDownloadStem"/>, <see cref="HCLResolveDownloadName"/>).
///
/// The download flow itself (HTTP, IPS-CSRF, resume) is not exercised here
/// because it requires a live server; integration tests for that belong in a
/// separate network-enabled suite.
/// </summary>

interface

uses
  Classes, SysUtils,
  fpcunit, testregistry,
  uHCLDownloader;

type
  /// <summary>
  /// FPCUnit test suite for <see cref="HCLComputeSHA256"/>.
  ///
  /// Each test creates a temporary file, writes known content, computes the
  /// SHA-256 via <c>HCLComputeSHA256</c>, compares against a pre-computed
  /// reference digest, and cleans up the file in <c>TearDown</c>.
  /// </summary>
  TTestDownloader = class(TTestCase)
  private
    FTempFile: String;

    /// <summary>
    /// Writes <paramref name="AContent"/> (raw bytes) to a fresh temp file
    /// and stores the path in <see cref="FTempFile"/>.
    /// </summary>
    procedure WriteTemp(const AContent: RawByteString);

  protected
    procedure SetUp; override;
    procedure TearDown; override;

  published
    // ------------------------------------------------------------------
    // NIST FIPS 180-4 short-message test vectors
    // ------------------------------------------------------------------

    /// <summary>
    /// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    /// (NIST FIPS 180-4, example A.1 — zero-length message)
    /// </summary>
    procedure TestEmptyString;

    /// <summary>
    /// SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
    /// (verified with Python 3.12 hashlib.sha256)
    /// </summary>
    procedure TestAbc;

    /// <summary>
    /// SHA-256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
    ///   = 248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1
    /// (NIST FIPS 180-4 two-block test vector)
    /// </summary>
    procedure TestTwoBlockVector;

    /// <summary>
    /// SHA-256 of a 55-byte message (fits in a single padding block with the
    /// length field — edge case where message + 0x80 + length = exactly 64 bytes).
    /// SHA-256("The quick brown fox jumps over the lazy dog")
    ///   = d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592
    /// </summary>
    procedure TestQuickBrownFox;

    /// <summary>
    /// SHA-256 of a 44-byte message.
    /// SHA-256("The quick brown fox jumps over the lazy dog!")
    ///   = 66163cdaaaad4fa4f7f5e385778d100965f3c2c8653974033badac8caf1958d5
    /// (verified with Python 3.12 hashlib.sha256)
    /// </summary>
    procedure TestPaddingBoundary56Bytes;

    // ------------------------------------------------------------------
    // Edge cases
    // ------------------------------------------------------------------

    /// <summary>
    /// <c>HCLComputeSHA256</c> returns empty string for a non-existent path.
    /// </summary>
    procedure TestNonExistentFile;

    /// <summary>
    /// <c>HCLComputeSHA256</c> of a file whose content is all zero bytes
    /// (64 zero bytes, a single full block before padding).
    /// SHA-256(64 × 0x00) = f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b
    /// </summary>
    procedure TestAllZeroBlock;

    // ------------------------------------------------------------------
    // HCLSafeFileName
    // ------------------------------------------------------------------

    /// <summary>A plain, already-safe name is returned unchanged.</summary>
    procedure TestSafeNamePlainUnchanged;

    /// <summary>
    /// Relative traversal and absolute paths (either separator) are reduced to
    /// the bare last component.
    /// </summary>
    procedure TestSafeNameStripsTraversal;

    /// <summary>A drive-relative <c>C:evil.dll</c> (no separator) loses its colon.</summary>
    procedure TestSafeNameDriveRelative;

    /// <summary>Characters Windows forbids, and control characters, become <c>_</c>.</summary>
    procedure TestSafeNameForbiddenChars;

    /// <summary>Trailing dots and spaces are removed.</summary>
    procedure TestSafeNameTrailingDotsSpaces;

    /// <summary>Inputs with no usable name component reduce to an empty string.</summary>
    procedure TestSafeNameEmptyResults;

    /// <summary>
    /// Reserved device names get a leading <c>_</c> regardless of extension or
    /// case; longer names that merely start with one are left alone.
    /// </summary>
    procedure TestSafeNameReservedDevices;

    /// <summary>Over-long names are capped at 200 characters, keeping a short extension.</summary>
    procedure TestSafeNameLengthCap;

    // ------------------------------------------------------------------
    // HCLContentDispositionFileName
    // ------------------------------------------------------------------

    /// <summary>Quoted and unquoted <c>filename=</c> values are extracted.</summary>
    procedure TestCDPlainFileName;

    /// <summary>The parameter name is matched case-insensitively.</summary>
    procedure TestCDCaseInsensitive;

    /// <summary>RFC 5987 <c>filename*=</c> is percent-decoded, with or without a language tag.</summary>
    procedure TestCDRfc5987;

    /// <summary>When both forms are present, <c>filename*=</c> wins in either order.</summary>
    procedure TestCDRfc5987Preferred;

    /// <summary>Traversal in either header form is neutralised to the bare name.</summary>
    procedure TestCDTraversalNeutralised;

    /// <summary>A header with no filename parameter yields an empty string.</summary>
    procedure TestCDNoFileName;

    /// <summary>A parameter whose name merely ends in <c>filename</c> does not match.</summary>
    procedure TestCDParamBoundary;

    /// <summary>
    /// Malformed percent escapes are kept literally rather than raising
    /// (observed: <c>a%zz.zip</c>).
    /// </summary>
    procedure TestCDMalformedPercent;

    // ------------------------------------------------------------------
    // HCLDownloadStem
    // ------------------------------------------------------------------

    /// <summary>An IPS file page URL yields its slug, with or without a trailing slash.</summary>
    procedure TestStemPageUrl;

    /// <summary>Query string and fragment are ignored.</summary>
    procedure TestStemIgnoresQueryFragment;

    /// <summary>A direct file URL yields the file name, percent-decoded.</summary>
    procedure TestStemDirectFileUrl;

    /// <summary>URLs with no usable path segment fall back to <c>download</c>.</summary>
    procedure TestStemFallback;

    /// <summary>Distinct page URLs give distinct stems (the resume-collision guard).</summary>
    procedure TestStemDistinctPerUrl;

    /// <summary>The stem is always a safe file name (reserved device names are prefixed).</summary>
    procedure TestStemIsSafe;

    // ------------------------------------------------------------------
    // HCLResolveDownloadName
    // ------------------------------------------------------------------

    /// <summary>Precedence is requested name, then server name, then stem.</summary>
    procedure TestResolvePrecedence;

    /// <summary>Unsafe candidates are sanitised, or skipped when nothing survives.</summary>
    procedure TestResolveSanitises;

    /// <summary>When every candidate is empty or unusable the result is <c>download</c>.</summary>
    procedure TestResolveAllEmpty;

    /// <summary>No catalog downloadUrl: exactly the URL the launcher always used.</summary>
    procedure TestIpsUrlDefault;

    /// <summary>A same-site downloadUrl naming a record gets the csrfKey appended.</summary>
    procedure TestIpsUrlSpecificRecord;

    /// <summary>
    /// A downloadUrl on any other origin - another host, a look-alike host, a
    /// userinfo trick, another scheme or port - falls back to the page URL, so
    /// the session cookie and csrfKey never leave the site.
    /// </summary>
    procedure TestIpsUrlRefusesOtherSites;
  end;

implementation

// ---------------------------------------------------------------------------
// TTestDownloader — helpers
// ---------------------------------------------------------------------------

procedure TTestDownloader.WriteTemp(const AContent: RawByteString);
var
  FS: TFileStream;
begin
  FTempFile := GetTempFileName('', 'hcl_sha256_test_');
  FS := TFileStream.Create(FTempFile, fmCreate);
  try
    if Length(AContent) > 0 then
      FS.WriteBuffer(AContent[1], Length(AContent));
  finally
    FS.Free;
  end;
end;

procedure TTestDownloader.SetUp;
begin
  FTempFile := '';
end;

procedure TTestDownloader.TearDown;
begin
  if (FTempFile <> '') and FileExists(FTempFile) then
    DeleteFile(FTempFile);
  FTempFile := '';
end;

// ---------------------------------------------------------------------------
// TTestDownloader — test methods
// ---------------------------------------------------------------------------

procedure TTestDownloader.TestEmptyString;
begin
  WriteTemp('');
  AssertEquals(
    'SHA-256 of empty string',
    'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
    HCLComputeSHA256(FTempFile)
  );
end;

procedure TTestDownloader.TestAbc;
begin
  WriteTemp('abc');
  AssertEquals(
    'SHA-256 of "abc"',
    'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
    HCLComputeSHA256(FTempFile)
  );
end;

procedure TTestDownloader.TestTwoBlockVector;
begin
  WriteTemp('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq');
  AssertEquals(
    'SHA-256 two-block NIST vector',
    '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1',
    HCLComputeSHA256(FTempFile)
  );
end;

procedure TTestDownloader.TestQuickBrownFox;
begin
  WriteTemp('The quick brown fox jumps over the lazy dog');
  AssertEquals(
    'SHA-256 of "The quick brown fox jumps over the lazy dog"',
    'd7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592',
    HCLComputeSHA256(FTempFile)
  );
end;

procedure TTestDownloader.TestPaddingBoundary56Bytes;
begin
  // "The quick brown fox jumps over the lazy dog!" = 44 bytes.
  // This exercises a single-block padding path (44 + 1 + 11 + 8 = 64).
  WriteTemp('The quick brown fox jumps over the lazy dog!');
  AssertEquals(
    'SHA-256 of 44-byte message (padding boundary check)',
    '66163cdaaaad4fa4f7f5e385778d100965f3c2c8653974033badac8caf1958d5',
    HCLComputeSHA256(FTempFile)
  );
end;

procedure TTestDownloader.TestNonExistentFile;
begin
  // FTempFile is '' — no file written, no file exists.
  AssertEquals(
    'SHA-256 of non-existent path must return empty string',
    '',
    HCLComputeSHA256('C:\this_file_does_not_exist_hcl_test_9z8y7x.tmp')
  );
end;

procedure TTestDownloader.TestAllZeroBlock;
var
  ZeroBuf: RawByteString;
begin
  SetLength(ZeroBuf, 64);
  FillChar(ZeroBuf[1], 64, 0);
  WriteTemp(ZeroBuf);
  AssertEquals(
    'SHA-256 of 64 zero bytes',
    'f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b',
    HCLComputeSHA256(FTempFile)
  );
end;

// ---------------------------------------------------------------------------
// HCLSafeFileName
// ---------------------------------------------------------------------------

procedure TTestDownloader.TestSafeNamePlainUnchanged;
begin
  AssertEquals('plain name', 'lazHCEffects.zip', HCLSafeFileName('lazHCEffects.zip'));
end;

procedure TTestDownloader.TestSafeNameStripsTraversal;
begin
  AssertEquals('backslash traversal', 'evil.dll', HCLSafeFileName('..\..\evil.dll'));
  AssertEquals('slash traversal', 'evil.dll', HCLSafeFileName('../../evil.dll'));
  AssertEquals('absolute Windows path', 'evil.dll', HCLSafeFileName('C:\Windows\evil.dll'));
  AssertEquals('absolute POSIX path', 'passwd', HCLSafeFileName('/etc/passwd'));
end;

procedure TTestDownloader.TestSafeNameDriveRelative;
var
  R: String;
begin
  R := HCLSafeFileName('C:evil.dll');
  AssertTrue('no colon remains in "' + R + '"', Pos(':', R) = 0);
  AssertTrue('non-empty', R <> '');
end;

procedure TTestDownloader.TestSafeNameForbiddenChars;
begin
  AssertEquals('forbidden chars', 'a_b_c_d_e_f_g_h.zip',
    HCLSafeFileName('a<b>c:d"e|f?g*h.zip'));
  AssertEquals('tab', 'a_b.zip', HCLSafeFileName('a'#9'b.zip'));
  AssertEquals('SOH', 'a_b.zip', HCLSafeFileName('a'#1'b.zip'));
end;

procedure TTestDownloader.TestSafeNameTrailingDotsSpaces;
begin
  AssertEquals('trailing dots/spaces', 'name.zip', HCLSafeFileName('name.zip. .'));
end;

procedure TTestDownloader.TestSafeNameEmptyResults;
begin
  AssertEquals('empty', '', HCLSafeFileName(''));
  AssertEquals('whitespace', '', HCLSafeFileName('   '));
  AssertEquals('dot', '', HCLSafeFileName('.'));
  AssertEquals('dot dot', '', HCLSafeFileName('..'));
  AssertEquals('three dots', '', HCLSafeFileName('...'));
  AssertEquals('slash', '', HCLSafeFileName('/'));
  AssertEquals('backslash', '', HCLSafeFileName('\'));
  AssertEquals('trailing separator', '', HCLSafeFileName('a/'));
end;

procedure TTestDownloader.TestSafeNameReservedDevices;
begin
  AssertEquals('CON', '_CON', HCLSafeFileName('CON'));
  AssertEquals('con.zip', '_con.zip', HCLSafeFileName('con.zip'));
  AssertEquals('LPT1.txt', '_LPT1.txt', HCLSafeFileName('LPT1.txt'));
  AssertEquals('COM9.dll', '_COM9.dll', HCLSafeFileName('COM9.dll'));
  AssertEquals('CONSOLE.zip not reserved', 'CONSOLE.zip', HCLSafeFileName('CONSOLE.zip'));
  AssertEquals('lpt10.txt not reserved', 'lpt10.txt', HCLSafeFileName('lpt10.txt'));
end;

procedure TTestDownloader.TestSafeNameLengthCap;
var
  R: String;
begin
  R := HCLSafeFileName(StringOfChar('a', 296) + '.zip');
  AssertEquals('capped length with ext', 200, Length(R));
  AssertEquals('extension kept', '.zip', Copy(R, Length(R) - 3, 4));

  R := HCLSafeFileName(StringOfChar('b', 300));
  AssertEquals('capped length no ext', 200, Length(R));
end;

// ---------------------------------------------------------------------------
// HCLContentDispositionFileName
// ---------------------------------------------------------------------------

procedure TTestDownloader.TestCDPlainFileName;
begin
  AssertEquals('quoted', 'lazHCEffects.zip',
    HCLContentDispositionFileName('attachment; filename="lazHCEffects.zip"'));
  AssertEquals('unquoted', 'lazHCEffects.zip',
    HCLContentDispositionFileName('attachment; filename=lazHCEffects.zip'));
  AssertEquals('unquoted then param', 'a.zip',
    HCLContentDispositionFileName('attachment; filename=a.zip; size=10'));
end;

procedure TTestDownloader.TestCDCaseInsensitive;
begin
  AssertEquals('upper-case', 'a.zip',
    HCLContentDispositionFileName('ATTACHMENT; FILENAME="a.zip"'));
end;

procedure TTestDownloader.TestCDRfc5987;
begin
  AssertEquals('RFC 5987 only', 'my file.zip',
    HCLContentDispositionFileName('attachment; filename*=UTF-8''''my%20file.zip'));
  AssertEquals('RFC 5987 with language', 'x.zip',
    HCLContentDispositionFileName('attachment; filename*=UTF-8''en''x.zip'));
end;

procedure TTestDownloader.TestCDRfc5987Preferred;
begin
  AssertEquals('filename* after filename', 'real.zip',
    HCLContentDispositionFileName(
      'attachment; filename="fallback.zip"; filename*=UTF-8''''real.zip'));
  AssertEquals('filename* before filename', 'real.zip',
    HCLContentDispositionFileName(
      'attachment; filename*=UTF-8''''real.zip; filename="fallback.zip"'));
end;

procedure TTestDownloader.TestCDTraversalNeutralised;
begin
  AssertEquals('quoted traversal', 'evil.dll',
    HCLContentDispositionFileName('attachment; filename="..\..\evil.dll"'));
  AssertEquals('encoded traversal', 'evil.dll',
    HCLContentDispositionFileName('attachment; filename*=UTF-8''''..%2F..%2Fevil.dll'));
end;

procedure TTestDownloader.TestCDNoFileName;
begin
  AssertEquals('attachment only', '', HCLContentDispositionFileName('attachment'));
  AssertEquals('empty header', '', HCLContentDispositionFileName(''));
  AssertEquals('inline', '', HCLContentDispositionFileName('inline'));
end;

procedure TTestDownloader.TestCDParamBoundary;
begin
  AssertEquals('xfilename alone', '',
    HCLContentDispositionFileName('attachment; xfilename="bad.zip"'));
  AssertEquals('xfilename then filename', 'good.zip',
    HCLContentDispositionFileName('attachment; xfilename="bad.zip"; filename="good.zip"'));
end;

procedure TTestDownloader.TestCDMalformedPercent;
var
  R: String;
begin
  R := HCLContentDispositionFileName('attachment; filename*=UTF-8''''a%zz.zip');
  AssertTrue('non-empty', R <> '');
  AssertTrue('contains a: "' + R + '"', Pos('a', R) > 0);
  AssertEquals('ends .zip', '.zip', Copy(R, Length(R) - 3, 4));
  AssertEquals('malformed escape kept verbatim', 'a%zz.zip', R);
end;

// ---------------------------------------------------------------------------
// HCLDownloadStem
// ---------------------------------------------------------------------------

procedure TTestDownloader.TestStemPageUrl;
begin
  AssertEquals('trailing slash', '835-exportdll-lazhceffects',
    HCLDownloadStem('https://harpgamer.com/harpforum/files/file/835-exportdll-lazhceffects/'));
  AssertEquals('no trailing slash', '835-exportdll-lazhceffects',
    HCLDownloadStem('https://harpgamer.com/harpforum/files/file/835-exportdll-lazhceffects'));
end;

procedure TTestDownloader.TestStemIgnoresQueryFragment;
begin
  AssertEquals('query + fragment', '274-caribean',
    HCLDownloadStem('https://host/files/file/274-caribean/?do=download&csrfKey=abc#x'));
end;

procedure TTestDownloader.TestStemDirectFileUrl;
begin
  AssertEquals('direct file', 'pack.zip', HCLDownloadStem('https://host/dl/pack.zip'));
  AssertEquals('percent-decoded', 'my pack.zip', HCLDownloadStem('https://host/dl/my%20pack.zip'));
end;

procedure TTestDownloader.TestStemFallback;
begin
  AssertEquals('host with slash', 'download', HCLDownloadStem('https://host/'));
  AssertEquals('host only', 'download', HCLDownloadStem('https://host'));
  AssertEquals('empty', 'download', HCLDownloadStem(''));
  AssertEquals('query only', 'download', HCLDownloadStem('https://host/?q=1'));
end;

procedure TTestDownloader.TestStemDistinctPerUrl;
begin
  AssertTrue('different pages give different stems',
    HCLDownloadStem('https://harpgamer.com/harpforum/files/file/835-exportdll-lazhceffects/') <>
    HCLDownloadStem('https://harpgamer.com/harpforum/files/file/274-caribean/'));
end;

procedure TTestDownloader.TestStemIsSafe;
begin
  AssertEquals('reserved segment', '_CON', HCLDownloadStem('https://host/files/CON'));
end;

// ---------------------------------------------------------------------------
// HCLResolveDownloadName
// ---------------------------------------------------------------------------

procedure TTestDownloader.TestResolvePrecedence;
begin
  AssertEquals('requested wins', 'cat.zip',
    HCLResolveDownloadName('cat.zip', 'server.zip', 'stem'));
  AssertEquals('server when no requested', 'server.zip',
    HCLResolveDownloadName('', 'server.zip', 'stem'));
  AssertEquals('stem when both empty', '835-x',
    HCLResolveDownloadName('', '', '835-x'));
end;

procedure TTestDownloader.TestResolveSanitises;
begin
  AssertEquals('unusable requested falls through', 'server.zip',
    HCLResolveDownloadName('..\..\', 'server.zip', 'stem'));
  AssertEquals('traversal requested sanitised', 'evil.dll',
    HCLResolveDownloadName('..\evil.dll', 'server.zip', 'stem'));
end;

procedure TTestDownloader.TestResolveAllEmpty;
begin
  AssertEquals('all empty', 'download', HCLResolveDownloadName('', '', ''));
  AssertEquals('all unusable', 'download', HCLResolveDownloadName('..', '/', ' . '));
end;

const
  PAGE = 'https://harpgamer.com/harpforum/files/file/289-caribbeanbasin/';
  KEY  = '0123456789abcdef0123456789abcdef';

procedure TTestDownloader.TestIpsUrlDefault;
begin
  AssertEquals('empty downloadUrl: unchanged legacy URL',
    PAGE + '?do=download&csrfKey=' + KEY, HCLBuildIpsDownloadUrl(PAGE, '', KEY));
end;

procedure TTestDownloader.TestIpsUrlSpecificRecord;
begin
  AssertEquals('record link gets the key appended',
    PAGE + '?do=download&r=1234&csrfKey=' + KEY,
    HCLBuildIpsDownloadUrl(PAGE, PAGE + '?do=download&r=1234', KEY));
  AssertEquals('host compared case-insensitively',
    'https://HarpGamer.com/harpforum/files/file/289-x/?do=download&r=1&csrfKey=' + KEY,
    HCLBuildIpsDownloadUrl(PAGE, 'https://HarpGamer.com/harpforum/files/file/289-x/?do=download&r=1', KEY));
  AssertEquals('a same-site URL without a query gets ?csrfKey=',
    'https://harpgamer.com/dl/x.zip?csrfKey=' + KEY,
    HCLBuildIpsDownloadUrl(PAGE, 'https://harpgamer.com/dl/x.zip', KEY));
end;

procedure TTestDownloader.TestIpsUrlRefusesOtherSites;
const
  FALLBACK = PAGE + '?do=download&csrfKey=' + KEY;
begin
  AssertEquals('another host', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'https://evil.example/files/?do=download&r=1', KEY));
  AssertEquals('look-alike host with the real one as a prefix', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'https://harpgamer.com.evil.example/?do=download&r=1', KEY));
  AssertEquals('userinfo trick', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'https://harpgamer.com@evil.example/?do=download&r=1', KEY));
  AssertEquals('scheme downgrade', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'http://harpgamer.com/harpforum/files/file/289-x/?do=download&r=1', KEY));
  AssertEquals('different port', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'https://harpgamer.com:8443/?do=download&r=1', KEY));
  AssertEquals('not http(s)', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'file:///C:/Windows/win.ini', KEY));
  AssertEquals('relative path', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, '/harpforum/files/file/289-x/?do=download&r=1', KEY));
  AssertEquals('backslash host trick', FALLBACK,
    HCLBuildIpsDownloadUrl(PAGE, 'https://evil.example\@harpgamer.com/', KEY));
end;

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

initialization
  RegisterTest(TTestDownloader);

end.
