App-FuguVM

 view release on metacpan or  search on metacpan

t/fuguvm/mirror.t  view on Meta::CPAN

    close $fh;

    my $mirror = _mirror(tempdir(CLEANUP => 1), keys_dir => $keys_dir);
    is($mirror->key_path, "$keys_dir/openbsd-78-base.pub",
	'key_path maps 7.8 to openbsd-78-base.pub in keys_dir');

    # The share tree of this checkout also holds the 7.8 key, so the
    # answer above proves that keys_dir wins over it.
    ok(-f "$RealBin/../../share/fuguvm/signify/openbsd-78-base.pub",
	'the share tree holds the shipped key');
}

# The shipped key of the default release resolves from the share tree
{
    my $mirror = _mirror(tempdir(CLEANUP => 1));
    like($mirror->key_path, qr{share/fuguvm/signify/openbsd-78-base\.pub$},
	'key_path falls back to the share tree');
}

# An absent key is a diagnosed miss that names the file and each
# directory. Version 9.9 has no key in any directory.
{
    my $keys_dir = tempdir(CLEANUP => 1);
    my $cache_dir = tempdir(CLEANUP => 1);
    my $mirror = _mirror($cache_dir,
	version => '9.9', keys_dir => $keys_dir);

    is($mirror->key_path, undef, 'key_path returns undef without a key');
    like($mirror->error, qr/openbsd-99-base\.pub/, 'error names the file');
    like($mirror->error, qr/\Q$keys_dir\E/, 'error names keys_dir');
    like($mirror->error, qr{share/fuguvm/signify},
	'error names the share directory');

    # manifest stops on the key, before any fetch
    is($mirror->manifest('release'), undef,
	'manifest returns undef when the key does not resolve');
    like($mirror->error, qr/openbsd-99-base\.pub/,
	'and the reason is the key');
    is(scalar @{ _cache($cache_dir)->list }, 0, 'and it fetched nothing');
}

# ensure returns the cached path with no fetch when the cache holds
# the file. The verification runs on the way into the cache, not on
# every read.
{
    my $cache_dir = tempdir(CLEANUP => 1);
    my $seeded = _seed($cache_dir, '7.8/arm64/base78.tgz', 'set bytes');

    my $mirror = _mirror($cache_dir);
    is($mirror->ensure('release', 'base78.tgz'), $seeded,
	'ensure returns the cached path with no fetch');
}

# With verify set to 0, ensure stores the file and logs one warning,
# and the proof methods refuse: a method must not report a proof that
# it did not make.
{
    my $cache_dir = tempdir(CLEANUP => 1);
    my $mirror = _mirror($cache_dir, verify => 0);

    my $warned = _capture_stderr(sub {
	no warnings 'redefine';
	local *App::FuguVM::Mirror::fetch =
	    sub ($, $) { _temp_file('unproven bytes') };
	is($mirror->ensure('release', 'base78.tgz'),
	    _cache($cache_dir)->cache_path(
		'https://cdn.openbsd.org/pub/OpenBSD/7.8/arm64/base78.tgz'),
	    'ensure with verify 0 stores the file');
    });
    like($warned, qr/unproven/i, 'and it logs one warning');

    is($mirror->verify_file('release', 'base78.tgz', '/tmp/x'), undef,
	'verify_file with verify 0 returns undef');
    like($mirror->error, qr/verification is off/, 'with a reason');
    is($mirror->manifest('release'), undef,
	'manifest with verify 0 returns undef');
    like($mirror->error, qr/verification is off/, 'with the same reason');
}

# The signify(1) subtests. Each one generates its own key pair, writes
# a manifest in the sha256(1) line form, signs it, and seeds the
# cache at the mirror paths. The manifests are then local, so the
# module fetches nothing.
my $SIGNIFY = _find_signify();

subtest 'the release manifest and the file proofs' => sub {
    plan skip_all => 'signify(1) not available' if !defined $SIGNIFY;

    my ($cache_dir, $keys_dir) = _signed_fixture(
	release => { 'base78.tgz' => 'good bytes' });
    my $mirror = _mirror($cache_dir, keys_dir => $keys_dir);

    my $manifest = $mirror->manifest('release');
    ok(defined $manifest, 'manifest returns the path for a good signature');
    is($manifest, _cache($cache_dir)->cache_path(
	    'https://cdn.openbsd.org/pub/OpenBSD/7.8/arm64/SHA256'),
	'and the path is the cached manifest');
    is_deeply($mirror->manifest_names('release'), ['base78.tgz'],
	'manifest_names lists the named files');

    my $good = _write_file($cache_dir, 'good.local', 'good bytes');
    is($mirror->verify_file('release', 'base78.tgz', $good), 1,
	'verify_file returns 1 for a matching digest');

    my $bad = _write_file($cache_dir, 'bad.local', 'other bytes');
    is($mirror->verify_file('release', 'base78.tgz', $bad), undef,
	'verify_file returns undef for a digest that does not match');
    like($mirror->error, qr/base78\.tgz/, 'and the reason names the file');

    is($mirror->verify_file('release', 'absent.tgz', $good), undef,
	'verify_file returns undef for a name the manifest does not hold');
};

subtest 'a repeated identical manifest line changes nothing' => sub {
    plan skip_all => 'signify(1) not available' if !defined $SIGNIFY;

    # The real SHA256 of the OpenBSD 7.8 amd64 directory repeats the
    # install image lines. The digest check must accept a duplicate
    # whose digest is identical, and it must refuse a duplicate name
    # with two digests.
    my $work = tempdir(CLEANUP => 1);

t/fuguvm/mirror.t  view on Meta::CPAN

sub _signed_fixture
{
    my (%scopes) = @_;

    my $cache_dir = tempdir(CLEANUP => 1);
    my $keys_dir = tempdir(CLEANUP => 1);
    my $work = tempdir(CLEANUP => 1);

    # signify -G wants one basename for the pair, so the pair lands
    # in the work directory and the public half moves to the release
    # name.
    system($SIGNIFY, '-G', '-n',
	'-p', "$work/key.pub", '-s', "$work/key.sec") == 0
	or die "signify -G failed\n";
    require File::Copy;
    File::Copy::copy("$work/key.pub", "$keys_dir/openbsd-78-base.pub")
	or die "cannot copy the public key: $!\n";

    for my $scope (sort keys %scopes) {
	my $files = $scopes{$scope};
	my $dir = $scope eq 'release' ? '7.8/arm64' : '7.8';

	my $manifest = '';
	for my $name (sort keys %$files) {
	    my $digest = Digest::SHA->new(256);
	    $digest->add($files->{$name});
	    $manifest .= sprintf "SHA256 (%s) = %s\n",
		$name, $digest->hexdigest;
	}

	my $local = "$work/SHA256.$scope";
	open my $fh, '>', $local or die $!;
	print $fh $manifest;
	close $fh;

	system($SIGNIFY, '-S', '-s', "$work/key.sec",
	    '-m', $local, '-x', "$local.sig") == 0
	    or die "signify -S failed\n";

	_seed($cache_dir, "$dir/SHA256", $manifest);
	_seed($cache_dir, "$dir/SHA256.sig", _slurp("$local.sig"));
    }

    return ($cache_dir, $keys_dir);
}

# _slurp($path):
#	The whole file as bytes.
sub _slurp
{
    my ($path) = @_;

    open my $fh, '<', $path or die "read $path: $!";
    local $/;
    my $bytes = <$fh>;
    close $fh;

    return $bytes;
}

# _capture_stderr($code):
#	Run the code with the process default logger on standard
#	error, capture the stream, and put the quiet default back.
sub _capture_stderr
{
    my ($code) = @_;
    my $dir = tempdir(CLEANUP => 1);

    Fugu::TestLog->stderr;
    open my $saved, '>&', \*STDERR or die $!;
    open STDERR, '>', "$dir/err" or die $!;

    $code->();

    open STDERR, '>&', $saved or die $!;
    close $saved;
    Fugu::TestLog->quiet;

    return _slurp("$dir/err");
}



( run in 1.355 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )