Database-BI

 view release on metacpan or  search on metacpan

t/edge_cases.t  view on Meta::CPAN

	# Opening should yield 200 (no crash), even though there are 0 rows.
	$t->get_ok($res->{url})->status_is(200);
};

# ---------------------------------------------------------------------------
# Section 8: Empty CSV file opened via /open
# ---------------------------------------------------------------------------

subtest 'GET /open -- 0-byte CSV file renders without crashing' => sub {
	my ($fh, $fname) = tempfile(SUFFIX => '.csv', DIR => $TMPDIR);
	close $fh;  # 0 bytes written
	$t->get_ok('/open?path=' . url_escape($fname))
	  ->status_is(200, '200 (no crash for empty CSV)');
};

subtest 'GET /open -- headers-only CSV renders with 0 data rows' => sub {
	my $path = Mojo::File->new($TMPDIR)->child('headers_only.csv');
	$path->spew("id,name,amount\n");  # header line only
	$t->get_ok('/open?path=' . url_escape($path->to_string))
	  ->status_is(200)
	  ->content_unlike(qr/500 Internal Server Error/, 'no 500 error for headers-only file');
};

# ---------------------------------------------------------------------------
# Section 9: Mocked upstream failures -- fetch_all croak is caught gracefully
#
# Strategy: use Test::Mockingbird to replace DataSource::fetch_all with a
# croaking stub.  Verify the eval in the view action catches the croak and
# renders the friendly error template (200, not 500).
# ---------------------------------------------------------------------------

subtest 'GET /view/sales -- fetch_all croak is caught, friendly error rendered' => sub {
	mock 'Database::BI::Model::DataSource::fetch_all' => sub {
		die "Simulated upstream database failure\n"
	};

	$t->get_ok('/view/sales')
	  ->status_is(200, 'controller returns 200 (error page), not 500')
	  ->content_like(qr/Could not open table/i,
	      'error_table_open message appears in error page');

	restore_all();
};

subtest 'GET /open -- fetch_all croak is caught, friendly error rendered' => sub {
	my $path = Mojo::File->new($TMPDIR)->child('sales.csv');
	$path->spew("id,name\n1,Widget\n");

	mock 'Database::BI::Model::DataSource::fetch_all' => sub {
		die "Simulated read failure\n"
	};

	$t->get_ok('/open?path=' . url_escape($path->to_string))
	  ->status_is(200)
	  ->content_like(qr/Could not open/i, 'error_file_open message in error page');

	restore_all();
};

# ---------------------------------------------------------------------------
# Section 10: SSRF via GET /import endpoint -- HTTP-level verification
#
# For blocked IPs the controller short-circuits before any LWP call, so
# these are fast and deterministic (no network required).
# ---------------------------------------------------------------------------

subtest 'GET /import -- private IP in URL triggers SSRF error response' => sub {
	for my $ip ('127.0.0.1', '10.0.0.1', '192.168.1.1', '172.16.0.1', '169.254.169.254', '100.64.0.0') {
		$t->get_ok("/import?url=" . url_escape("http://$ip/"))
		  ->status_is(200)
		  ->content_like(qr/private or reserved address/i,
		      "$ip: SSRF error shown");
	}
};

subtest 'GET /import -- localhost by name triggers SSRF error' => sub {
	$t->get_ok('/import?url=' . url_escape('http://localhost/'))
	  ->status_is(200)
	  ->content_like(qr/private or reserved address/i, 'localhost SSRF error shown');
};

subtest 'GET /import -- missing URL triggers required-field error' => sub {
	$t->get_ok('/import')
	  ->status_is(200)
	  ->content_like(qr/Please enter a URL/i, 'empty url error shown');
};

subtest 'GET /import -- non-http:// URL triggers invalid-URL error' => sub {
	$t->get_ok('/import?url=' . url_escape('ftp://example.com/data.csv'))
	  ->status_is(200)
	  ->content_like(qr/not a valid http/i, 'invalid scheme error shown');
};

# ---------------------------------------------------------------------------
# Section 11: Filter via HTTP -- malformed f= params do not break the view
# ---------------------------------------------------------------------------

subtest 'GET /view/sales?f= -- empty filter spec is harmless' => sub {
	$t->get_ok('/view/sales?f=')
	  ->status_is(200)
	  ->content_unlike(qr/500 Internal Server Error/, 'no 500 for empty f=');
};

subtest 'GET /view/sales?f=:eq:val -- empty column name is harmless' => sub {
	$t->get_ok('/view/sales?f=' . url_escape(':eq:val'))
	  ->status_is(200)
	  ->content_unlike(qr/500 Internal Server Error/, 'no 500 for missing column in f=');
};

subtest 'GET /view/sales?f=col:badop:val -- unknown operator is harmless' => sub {
	$t->get_ok('/view/sales?f=' . url_escape('region:badop:North'))
	  ->status_is(200)
	  ->content_unlike(qr/500 Internal Server Error/, 'no 500 for unknown operator');
};

subtest 'GET /view/sales -- many f= params are handled without crash' => sub {
	my $url = '/view/sales?' . join('&', map { 'f=' . url_escape("region:eq:North$_") } 1 .. 50);
	$t->get_ok($url)
	  ->status_is(200)
	  ->content_unlike(qr/500 Internal Server Error/, 'no 500 for 50 filter conditions');
};



( run in 3.018 seconds using v1.01-cache-2.11-cpan-9789f410c06 )