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 )