Database-BI
view release on metacpan or search on metacpan
t/edge_cases.t view on Meta::CPAN
};
# ---------------------------------------------------------------------------
# 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 -- selectall_arrayref croak is caught gracefully
#
# Strategy: use Test::Mockingbird to replace DataSource::selectall_arrayref with
# a croaking stub. Verify the eval in the view action catches the croak and
# renders the friendly error template (200, not 500).
# (The controller calls count() then selectall_arrayref() since pagination was
# introduced; fetch_all() is no longer called on the /view or /open paths.)
# ---------------------------------------------------------------------------
subtest 'GET /view/sales -- fetch_all croak is caught, friendly error rendered' => sub {
mock 'Database::BI::Model::DataSource::selectall_arrayref' => 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::selectall_arrayref' => 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');
};
t/edge_cases.t view on Meta::CPAN
# A file with a blank first line is effectively empty: _detect_file_info
# returns the _file_is_empty sentinel (same path as a 0-byte file), so
# _init_backend skips Database::Abstraction entirely and fetch_all returns [].
# ---------------------------------------------------------------------------
# 0-byte CSV via DataSource constructor
subtest 'DataSource -- 0-byte CSV: fetch_all returns [] without croak' => sub {
require Database::BI::Model::DataSource;
my $dir = tempdir(CLEANUP => 1);
Mojo::File->new("$dir/emptysrc.csv")->spew('');
my $src;
lives_ok {
$src = Database::BI::Model::DataSource->new(directory => $dir, table => 'emptysrc');
} '0-byte CSV: constructor succeeds';
my $rows = $src->fetch_all;
is_deeply $rows, [], '0-byte CSV: fetch_all returns []';
};
# 0-byte PSV via DataSource constructor
subtest 'DataSource -- 0-byte PSV: fetch_all returns [] without croak' => sub {
require Database::BI::Model::DataSource;
my $dir = tempdir(CLEANUP => 1);
Mojo::File->new("$dir/emptypsv.psv")->spew('');
my $src;
lives_ok {
$src = Database::BI::Model::DataSource->new(directory => $dir, table => 'emptypsv');
} '0-byte PSV: constructor succeeds';
my $rows = $src->fetch_all;
is_deeply $rows, [], '0-byte PSV: fetch_all returns []';
};
# Newline-only CSV via DataSource constructor
subtest 'DataSource -- newline-only CSV: fetch_all returns [] without croak' => sub {
# A file containing exactly "\n" has a blank first line. After chomp the
# line is "", yielding zero column names. This must be treated as an empty
# file (returning []), not as "no safe identifier" (which would croak).
require Database::BI::Model::DataSource;
my $dir = tempdir(CLEANUP => 1);
Mojo::File->new("$dir/newlinecsv.csv")->spew("\n");
my $src;
lives_ok {
$src = Database::BI::Model::DataSource->new(directory => $dir, table => 'newlinecsv');
} 'newline-only CSV: constructor succeeds (no croak)';
my $rows = $src->fetch_all;
is_deeply $rows, [], 'newline-only CSV: fetch_all returns []';
};
# Newline-only PSV via DataSource constructor
subtest 'DataSource -- newline-only PSV: fetch_all returns [] without croak' => sub {
require Database::BI::Model::DataSource;
my $dir = tempdir(CLEANUP => 1);
Mojo::File->new("$dir/newlinepsv.psv")->spew("\n");
my $src;
lives_ok {
$src = Database::BI::Model::DataSource->new(directory => $dir, table => 'newlinepsv');
} 'newline-only PSV: constructor succeeds (no croak)';
my $rows = $src->fetch_all;
is_deeply $rows, [], 'newline-only PSV: fetch_all returns []';
};
# 0-byte CSV via HTTP /open endpoint
subtest 'HTTP -- 0-byte CSV via /open: 200 with empty-table message' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = "$dir/emptyhttp.csv";
Mojo::File->new($path)->spew('');
$t->get_ok('/open?path=' . url_escape($path))
->status_is(200, '0-byte CSV: /open returns 200')
->content_like(qr/No records found/i, '0-byte CSV: empty-state message present');
};
# 0-byte PSV via HTTP /open endpoint
subtest 'HTTP -- 0-byte PSV via /open: 200 with empty-table message' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = "$dir/emptyhttppsv.psv";
Mojo::File->new($path)->spew('');
$t->get_ok('/open?path=' . url_escape($path))
->status_is(200, '0-byte PSV: /open returns 200')
->content_like(qr/No records found/i, '0-byte PSV: empty-state message present');
};
# Newline-only CSV via HTTP /open endpoint
subtest 'HTTP -- newline-only CSV via /open: 200 with empty-table message' => sub {
# Regression guard: before the _file_is_empty fix, a blank first line caused
# _init_backend to croak "no safe identifier", and the controller rendered a
# 200 error page â but the error message was confusing for what is simply an
# empty file. After the fix, the response is a clean empty-table view.
my $dir = tempdir(CLEANUP => 1);
my $path = "$dir/nlcsv.csv";
Mojo::File->new($path)->spew("\n");
$t->get_ok('/open?path=' . url_escape($path))
->status_is(200, 'newline-only CSV: /open returns 200')
->content_like(qr/No records found/i, 'newline-only CSV: empty-state message present');
};
# Newline-only PSV via HTTP /open endpoint
subtest 'HTTP -- newline-only PSV via /open: 200 with empty-table message' => sub {
my $dir = tempdir(CLEANUP => 1);
my $path = "$dir/nlpsv.psv";
Mojo::File->new($path)->spew("\n");
$t->get_ok('/open?path=' . url_escape($path))
->status_is(200, 'newline-only PSV: /open returns 200')
->content_like(qr/No records found/i, 'newline-only PSV: empty-state message present');
};
# ---------------------------------------------------------------------------
# Section 16: _safe_back_url -- XSS / scheme injection guard
#
# Strategy: Verify that the _safe_back_url helper (added to fix the reflected
# XSS where javascript: URIs passed through | html unchanged and executed on
# click) correctly allows safe URLs and blocks hostile schemes.
# ---------------------------------------------------------------------------
Readonly my $SAFE_BACK_URL => \&Database::BI::Controller::Dashboard::_safe_back_url;
subtest '_safe_back_url -- javascript: scheme is blocked' => sub {
is $SAFE_BACK_URL->('javascript:alert(1)'), undef, 'bare javascript: blocked';
is $SAFE_BACK_URL->('javascript:void(0)'), undef, 'javascript:void(0) blocked';
is $SAFE_BACK_URL->('JAVASCRIPT:alert(1)'), undef, 'JAVASCRIPT: (caps) blocked';
is $SAFE_BACK_URL->('Javascript:alert(1)'), undef, 'Javascript: (mixed) blocked';
};
subtest '_safe_back_url -- data: and vbscript: schemes are blocked' => sub {
is $SAFE_BACK_URL->('data:text/html,<h1>XSS'), undef, 'data: scheme blocked';
is $SAFE_BACK_URL->('vbscript:MsgBox(1)'), undef, 'vbscript: scheme blocked';
is $SAFE_BACK_URL->('DATA:text/html,foo'), undef, 'DATA: (caps) blocked';
};
subtest '_safe_back_url -- relative paths are allowed' => sub {
is $SAFE_BACK_URL->('/view/sales'), '/view/sales', 'root-relative path allowed';
is $SAFE_BACK_URL->('/pie?l=table:sales'), '/pie?l=table:sales', 'root-relative with query allowed';
is $SAFE_BACK_URL->('/open?path=/tmp/f.csv'), '/open?path=/tmp/f.csv', 'root-relative open path allowed';
};
subtest '_safe_back_url -- http/https absolute URLs are allowed' => sub {
is $SAFE_BACK_URL->('https://example.com/path'), 'https://example.com/path', 'https: allowed';
is $SAFE_BACK_URL->('http://example.com/'), 'http://example.com/', 'http: allowed';
is $SAFE_BACK_URL->('HTTPS://EXAMPLE.COM/'), 'HTTPS://EXAMPLE.COM/', 'HTTPS (caps) allowed';
};
subtest '_safe_back_url -- undef, empty, and non-path strings are blocked' => sub {
is $SAFE_BACK_URL->(undef), undef, 'undef blocked';
is $SAFE_BACK_URL->(''), undef, 'empty string blocked';
is $SAFE_BACK_URL->(' '), undef, 'whitespace-only blocked';
is $SAFE_BACK_URL->('alert(1)'), undef, 'bare JS expression blocked';
is $SAFE_BACK_URL->('//evil.com'), undef, 'protocol-relative URL blocked';
};
# ---------------------------------------------------------------------------
# Section 17: HTTP back= and back2= XSS reflected in the rendered page
#
# Strategy: Send requests with javascript: payloads in back= (pie, graph) and
# back2= (dashboard) params. Verify the rendered HTML does NOT contain the
# raw javascript: string as an href value -- it must be sanitized away
# (rendered as "/" fallback or entirely absent from the breadcrumb).
# ---------------------------------------------------------------------------
( run in 1.158 second using v1.01-cache-2.11-cpan-16aabcbfbcb )