App-FuguWeb

 view release on metacpan or  search on metacpan

t/fuguweb/keys.t  view on Meta::CPAN

	spew( "$out/photos/deep/more.jpg", "mine\n" );

	ok( site( $config, $out )->build, 'a second build succeeds' );
	ok( -e "$out/photos/holiday.jpg", 'and it keeps the file' );
	ok( -e "$out/photos/deep/more.jpg", 'and the file below it' );
	ok( -d "$out/photos/deep",          'and the directory' );

	# The prune must never remove what the clean refuses to.
	ok( !site( $config, $out )->clean, 'the clean refuses the tree' );

	my @problems =
	    App::FuguWeb::Check->new( config => $config, out => $out )->run;
	ok(
		( grep { m{^photos/holiday\.jpg: in the output} } @problems ),
		'and the check reports it'
	) or diag join "\n", @problems;
};

subtest 'the checks see an empty directory that no build made' => sub {
	my ( $config, $reason ) = load( project() );
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = tempdir( CLEANUP => 1 ) . '/out';
	ok( site( $config, $out )->build, 'the build succeeds' );

	mkdir "$out/archive" or die "Cannot make the directory: $!";

	# An empty directory is a leaf of the walk, so the checks and
	# the clean agree about the same tree.
	my @problems =
	    App::FuguWeb::Check->new( config => $config, out => $out )->run;
	ok( ( grep { m{^archive: in the output} } @problems ),
		'the check reports it' )
	    or diag join "\n", @problems;

	# The clean refuses this directory, so the build must keep it.
	# WEB-OUTPUT-6 holds for a directory as it holds for a file.
	ok( site( $config, $out )->build, 'a second build succeeds' );
	ok( -d "$out/archive", 'and the build keeps it' );
	ok( !site( $config, $out )->clean, 'the clean refuses it too' );

	spew( "$out/archive/notes.txt", "mine\n" );
	ok( site( $config, $out )->build, 'a third build succeeds' );
	ok( -e "$out/archive/notes.txt", 'and it keeps a directory of files' );
};

subtest 'the build reports a stray directory' => sub {
	my $root = project();
	my ( $config, $reason ) = load($root);
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = "$root/out";
	ok( site( $config, $out )->build, 'the build succeeds' );

	spew( "$out/archive/notes.txt", "mine\n" );

	# The build keeps a directory that no build made, and it says
	# so. An empty one gets the same report, because the build
	# keeps that one too.
	my $said = '';
	open my $saved, '>&', \*STDERR or die "Cannot save stderr: $!";
	close STDERR;
	open STDERR, '>', \$said or die 'Cannot capture stderr';

	my $again = App::FuguWeb::Site->new(
		config => $config,
		out    => $out,
		render => App::FuguWeb::Render->new(
			config  => $config,
			mandoc  => '/bin/true',
			lowdown => '/bin/true',
		),
	)->build;

	close STDERR;
	open STDERR, '>&', $saved or die "Cannot restore stderr: $!";

	ok( $again, 'a second build succeeds' );
	ok( -e "$out/archive/notes.txt", 'and it keeps the file' );
	like( $said, qr{archive/notes\.txt is in the output},
		'and it reports the file' );

	# An empty directory below the key directory is nobody's
	# build, so it stays. The report of it has its own subtest.
	mkdir "$out/keys/stale" or die "Cannot make the directory: $!";
	ok( site( $config, $out )->build, 'a third build succeeds' );
	ok( -d "$out/keys/stale", 'and it keeps an empty one' );
	ok( !site( $config, $out )->clean, 'the clean refuses it too' );
};

subtest 'list_tree walks the leaves and no symlink' => sub {
	my $dir = tempdir( CLEANUP => 1 );

	spew( "$dir/top.txt",           "a\n" );
	spew( "$dir/below/deep/one.txt", "b\n" );
	mkdir "$dir/empty" or die "Cannot make the directory: $!";

	my $linked = -e '/etc/hostname' ? '/etc/hostname' : '/etc/passwd';
	my $made = symlink $linked, "$dir/link";
	my $tree = symlink $dir . '/below', "$dir/tree";

	my $paths = App::FuguWeb::list_tree($dir);
	ok( $paths, 'the walk reads the directory' );

	my %found = map { $_ => 1 } @$paths;
	ok( $found{'top.txt'},            'a file of the top level' );
	ok( $found{'below/deep/one.txt'}, 'a file below it' );
	ok( $found{'empty'}, 'an empty directory is a leaf of its own' );

	SKIP: {
		skip 'cannot make a symlink here', 2 unless $made && $tree;

		ok( $found{'link'}, 'a symlink is one entry' );
		ok( $found{'tree'},
			'and a symlinked directory is one entry, not a walk' );
	}

	is( App::FuguWeb::list_tree("$dir/no-such-directory"),
		undef, 'a directory that it cannot read gives undef' );
};

# renderers():
#	Whether every renderer of a page is installed. A subtest that
#	drives the real command needs them, and the rest of this file
#	needs none.
sub renderers ()
{
	for my $tool (qw(mandoc lowdown pod2man)) {
		return 0 unless system("command -v $tool >/dev/null 2>&1") == 0;
	}

	return 1;
}

# cli($root, @argv):
#	Run one command of the tool from the project root, with the
#	output captured, and return the exit code.
sub cli ( $root, @argv )
{
	my ( $out, $err ) = ( '', '' );

	my $here = Cwd::getcwd();
	chdir $root or die "Cannot chdir to $root: $!";

	open my $saved_out, '>&', \*STDOUT or die "Cannot save stdout: $!";
	open my $saved_err, '>&', \*STDERR or die "Cannot save stderr: $!";
	close STDOUT;
	close STDERR;
	open STDOUT, '>', \$out or die 'Cannot capture stdout';
	open STDERR, '>', \$err or die 'Cannot capture stderr';

	my $exit = eval { App::FuguWeb::CLI->run(@argv) };
	my $died = $@;

	close STDOUT;
	close STDERR;
	open STDOUT, '>&', $saved_out or die "Cannot restore stdout: $!";
	open STDERR, '>&', $saved_err or die "Cannot restore stderr: $!";

	chdir $here or die "Cannot chdir back: $!";
	die $died if $died;

	return ( $exit, $err );
}

subtest 'the clean command removes a key directory' => sub {
	my $root = project();
	my ( $config, $reason ) = load($root);
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = "$root/out";
	ok( site( $config, $out )->build, 'the build succeeds' );
	ok( -d "$out/keys", 'the key directory is there' );

	# The command names --out, so it loads no description of its
	# own by the older rule. The description is what names the key
	# directory, and without it the clean refuses the whole site.
	my ( $exit, $err ) = cli( $root, 'clean', '--out', $out );
	is( $exit, 0, 'the clean succeeds' ) or diag $err;
	ok( !-e $out, 'and the whole tree is gone' );
};

subtest 'the clean command still refuses a tree that no build made' => sub {
	my $root = project();

	# A description that does not load must not stop the clean.
	# It is the command an operator reaches for when a description
	# is broken.
	spew( "$root/.fuguwebrc", "site = Example\nkeys \"keys\" {\n" );

	my $victim = "$root/victim";
	spew( "$victim/deep/keep.txt", "important\n" );

	my ( $exit, $err ) = cli( $root, 'clean', '--out', $victim );
	isnt( $exit, 0, 'the clean fails' );
	ok( -e "$victim/deep/keep.txt", 'and removes nothing' );
	like( $err, qr/refusing to remove it/, 'and says why' );
};

subtest 'clean refuses a file that the site does not name' => sub {
	my $root = project();
	my ( $config, $reason ) = load($root);
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = "$root/out";
	ok( site( $config, $out )->build, 'the build succeeds' );

	# The site names the key directory, so a walk that read a
	# prefix would take every name below it. The rule is the
	# shape, and no key file is named notes.txt.
	spew( "$out/keys/notes.txt", "mine\n" );

	ok( !site( $config, $out )->clean, 'the clean refuses' );
	ok( -e "$out/keys/notes.txt", 'and removes nothing' );
};

subtest 'the top level takes any plain file that a build could write' =>
    sub {

t/fuguweb/keys.t  view on Meta::CPAN

		"mine\n" );

	my $hash = '.well-known/openpgpkey/hu/'
	    . 'ybndrfg8ejkmcpqxot1uwisza345h769';

	ok( site( $config, $out )->build, 'a second build succeeds' );
	ok( -e "$out/.well-known/security.txt",      'it keeps security.txt' );
	ok( -e "$out/.well-known/openpgpkey/policy", 'and the policy' );
	ok( -e "$out/$hash",                         'and the key of a hash' );

	ok( !site( $config, $out )->clean, 'the clean refuses the tree' );
	ok( -e "$out/.well-known/security.txt", 'and removes nothing' );
	ok( -e "$out/$hash",                    'the key of a hash as well' );
};

subtest 'a keyless description takes no foreign well-known tree' => sub {
	# The clean of a foreign target reads the description that it
	# can load. A .well-known directory alone must not make a tree
	# read like a built site.
	my ( $config, $reason ) = load( keyless() );
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = tempdir( CLEANUP => 1 ) . '/out';
	make_path("$out/.well-known");
	spew( "$out/index.html", "<h1>Someone else</h1>\n" );
	spew( "$out/style.css",  "body{}\n" );
	spew( "$out/.well-known/security.txt", "Contact: theirs\n" );

	ok( !site( $config, $out )->clean, 'the clean refuses' );
	ok( -e "$out/.well-known/security.txt", 'and removes nothing' );
};

subtest 'a keyless description owns no well-known directory' => sub {
	# The directory half of the rule. An empty tree carries no
	# file, so the file guard never reaches it.
	my ( $config, $reason ) = load( keyless() );
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = tempdir( CLEANUP => 1 ) . '/out';
	make_path("$out/.well-known/openpgpkey/hu");
	spew( "$out/index.html", "<h1>Someone else</h1>\n" );

	ok( !site( $config, $out )->clean, 'the clean refuses the tree' );
	ok( -d "$out/.well-known/openpgpkey/hu", 'and removes nothing' );
};

subtest 'the build names the stray directory that it keeps' => sub {
	# WEB-OUTPUT-4: the build keeps an entry that it may not
	# write, and it reports it. A silent build would leave the
	# operator to find the tree by hand.
	my $root = project();
	my ( $config, $reason ) = load($root);
	ok( $config, 'the description loads' ) or diag $reason;

	my $out = "$root/out";
	ok( site( $config, $out )->build, 'the build succeeds' );

	make_path("$out/photos/2024/raw");

	my $said = '';
	open my $saved, '>&', \*STDERR or die "Cannot save stderr: $!";
	close STDERR;
	open STDERR, '>', \$said or die 'Cannot capture stderr';

	my $again = App::FuguWeb::Site->new(
		config => $config,
		out    => $out,
		render => App::FuguWeb::Render->new(
			config  => $config,
			mandoc  => '/bin/true',
			lowdown => '/bin/true',
		),
	)->build;

	close STDERR;
	open STDERR, '>&', $saved or die "Cannot restore stderr: $!";

	ok( $again, 'a second build succeeds' );
	ok( -d "$out/photos/2024/raw", 'and it keeps the tree' );
	like( $said, qr{photos/2024/raw is in the output},
		'and it names the tree' );
};

subtest 'the clean refuses a directory of the source' => sub {
	# The build renders, so the skip comes before the first
	# assertion. A plan that arrives after one is not a plan.
	plan skip_all => 'a renderer is not installed' unless renderers();

	# The key files are the trust anchor of every release, and
	# each one sits at the top level of the key directory, where
	# the clean takes a plain file.
	my $root = project();
	my ( $config, $reason ) = load($root);
	ok( $config, 'the description loads' ) or diag $reason;

	for my $target (qw(web web/keys web/keys/deep)) {
		my ( $exit, $err ) = cli( $root, 'clean', '--out', $target );
		isnt( $exit, 0, "the clean refuses $target" );
		like( $err, qr{is the (?:source|key) directory},
			"and the target guard is the reason for $target" );
	}

	ok( -e "$root/web/keys/fugubsd-1-release.pub", 'the key survives' );
	ok( -e "$root/web/keys/SHA256",       'the manifest survives' );
	ok( -e "$root/web/keys/SHA256.sig",   'the signature survives' );
	ok( -e "$root/web/index.body.html",   'the source survives' );

	# The output directory that the description names is the one
	# exception below the source. This description names out, so
	# web/build is a directory of the source like any other.
	my ($exit) = cli( $root, 'build', '--out', 'web/build' );
	isnt( $exit, 0, 'the build refuses another directory of the source' );

	($exit) = cli( $root, 'build' );
	is( $exit, 0, 'and it takes the output that the site names' );

	# Every directory of the source, and not the key directory
	# alone. An operator directory there is content of the project.
	make_path("$root/web/img");
	spew( "$root/web/img/logo.svg", "logo\n" );

	for my $command (qw(build clean)) {
		my ($code) = cli( $root, $command, '--out', 'web/img' );
		isnt( $code, 0, "the $command refuses web/img" );
	}
	ok( -e "$root/web/img/logo.svg", 'the operator file survives' );
};

subtest 'a source that holds its own stylesheet' => sub {
	# The stylesheet guard alone would take this target, because
	# every build writes style.css and this source holds one. The
	# target guard is the rule that refuses it.
	my $root = project();
	spew( "$root/web/style.css", "body{}\n" );
	spew( "$root/.fuguwebrc", "site = Example\nnav \"index.html\" {\n" );



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