App-Netdisco
view release on metacpan or search on metacpan
xt/55-ports-search-shadow.t view on Meta::CPAN
use Test::More 0.88;
use NetAddr::MAC ();
use lib 'xt/lib';
use Test::Netdisco::Snapshot 'render_template';
{
package NetdiscoSchema;
use App::Netdisco;
use Dancer qw/:moose :script/;
use Dancer::Plugin::DBIC 'schema';
sub connected { my $s = schema('netdisco'); $s->storage->dbh_do(sub { $_[1]->do('SELECT 1') }); return $s }
}
my $schema = eval { NetdiscoSchema::connected() };
my $why = $@;
SKIP: {
skip "no usable netdisco database: $why", 3 if not $schema;
require App::Netdisco::Web::Plugin::Device::Ports;
# Joined to node_ip rather than counting node alone: on a port whose
# nodes carry no IPs, the IP assertion below would pass vacuously.
my ($switch, $port) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT n.switch, n.port
FROM node n
JOIN node_ip i ON i.mac = n.mac AND i.active = n.active
WHERE n.active
GROUP BY n.switch, n.port
HAVING count(*) > 20
ORDER BY count(*) DESC
LIMIT 1
});
});
skip 'no port in this database has more than 20 active nodes with an IP', 3
if not defined $switch;
my ($a_mac, $an_ip) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT n.mac::text, i.ip::text
FROM node n
JOIN node_ip i ON i.mac = n.mac AND i.active = n.active
WHERE n.switch = ? AND n.port = ? AND n.active
LIMIT 1
}, undef, $switch, $port);
});
my $device = $schema->resultset('Device')->find($switch);
my @results = $device->ports->with_properties->order_by_port_name->all;
App::Netdisco::Web::Plugin::Device::Ports::_stitch_nodes(
$schema, $device->ip, \@results, 'active_nodes', 'ips', [], []);
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $device->ip, \@results,
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'));
my ($row) = grep { $_->port eq $port } @results;
like $row->{nodes_search}, qr/\Q$a_mac\E/,
'the shadow carries a MAC from the port';
like $row->{nodes_search}, qr/\Q$an_ip\E/,
'the shadow carries an IP from the port';
is scalar(grep { !exists $_->{nodes_search} } @results), 0,
'every port has a shadow, including ports with no nodes';
}
# vlan has no n_* guard in the template, so it is always in the shadow.
SKIP: {
skip "no usable netdisco database: $why", 1 if not $schema;
# length(vlan) >= 3 avoids a false pass: a one- or two-digit vlan is a
# substring of nearly any mac or IP text already in the shadow, so the
# assertion would hold even if vlan were never selected. Matching it as
# its own whitespace-delimited token is part of the same guard.
my ($vlan_switch, $vlan_port, $a_vlan) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT switch, port, vlan
FROM node
WHERE active AND vlan IS NOT NULL AND length(vlan) >= 3
LIMIT 1
});
});
skip 'no active node in this database carries a 3+ digit vlan', 1
if not defined $vlan_switch;
my $vlan_device = $schema->resultset('Device')->find($vlan_switch);
my ($vlan_row) = grep { $_->port eq $vlan_port }
$vlan_device->ports->with_properties->order_by_port_name->all;
skip 'port lookup for the vlan fixture came back empty', 1
if not $vlan_row;
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $vlan_device->ip, [$vlan_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0);
like $vlan_row->{nodes_search}, qr/(?:^|\s)\Q$a_vlan\E(?:\s|$)/,
'the shadow carries a node VLAN as its own token, which has no n_* guard';
}
# SSID comes from node_wireless, keyed on mac alone, and its lateral is joined
# only when want_ssid is true, mirroring the template's n_ssid guard.
SKIP: {
skip "no usable netdisco database: $why", 2 if not $schema;
# single-token SSID only: the shadow joins fields with a bare space, so a
# multi-word SSID cannot satisfy a whitespace-anchored match.
my ($ssid_switch, $ssid_port, $an_ssid) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT n.switch, n.port, w.ssid
FROM node n
JOIN node_wireless w ON w.mac = n.mac
WHERE n.active AND length(w.ssid) > 0 AND w.ssid !~ '\s'
LIMIT 1
});
});
skip 'no active node in this database carries a single-token SSID', 2
if not defined $ssid_switch;
my $ssid_device = $schema->resultset('Device')->find($ssid_switch);
my ($ssid_row) = grep { $_->port eq $ssid_port }
$ssid_device->ports->with_properties->order_by_port_name->all;
skip 'port lookup for the ssid fixture came back empty', 2
if not $ssid_row;
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $ssid_device->ip, [$ssid_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
1, 0);
like $ssid_row->{nodes_search}, qr/(?:^|\s)\Q$an_ssid\E(?:\s|$)/,
'the shadow carries an SSID when n_ssid is on';
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $ssid_device->ip, [$ssid_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0);
unlike $ssid_row->{nodes_search}, qr/(?:^|\s)\Q$an_ssid\E(?:\s|$)/,
'the shadow does not carry the SSID when n_ssid is off, the conditional join is worth having';
}
# NetBIOS comes from node_nbt, also keyed on mac alone, joined only when
# want_netbios is true, mirroring the template's n_netbios guard.
SKIP: {
skip "no usable netdisco database: $why", 7 if not $schema;
# All four fields render under the one n_netbios block, so the fixture
# row must carry all four. Two exclusions avoid a false pass: nbuser
# equal to nbname would hold even if nbuser were never added to the
# shadow, and an nbt.ip already among the node's own IPs is covered by
# the node_ip lateral regardless.
my ($nbt_switch, $nbt_port, $an_nbname, $a_domain, $a_nbuser, $an_ip) =
$schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT n.switch, n.port, b.nbname, b.domain, b.nbuser, b.ip::text
FROM node n
JOIN node_nbt b ON b.mac = n.mac
WHERE n.active AND length(b.nbname) > 0 AND length(b.domain) > 0
AND length(b.nbuser) > 0 AND b.ip IS NOT NULL
AND b.nbuser <> b.nbname
AND NOT EXISTS (
SELECT 1 FROM node_ip i
WHERE i.mac = n.mac AND i.active = n.active AND i.ip = b.ip)
LIMIT 1
});
});
skip 'no active node in this database carries a NetBIOS name, domain, user and IP', 7
if not defined $nbt_switch;
my $nbt_device = $schema->resultset('Device')->find($nbt_switch);
my ($nbt_row) = grep { $_->port eq $nbt_port }
$nbt_device->ports->with_properties->order_by_port_name->all;
skip 'port lookup for the netbios fixture came back empty', 7
if not $nbt_row;
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $nbt_device->ip, [$nbt_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 1);
like $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$an_nbname\E(?:\s|$)/,
'the shadow carries a NetBIOS name when n_netbios is on';
like $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$a_domain\E(?:\s|$)/,
'the shadow carries a NetBIOS domain when n_netbios is on';
like $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$a_nbuser\E(?:\s|$)/,
'the shadow carries a NetBIOS user when n_netbios is on';
like $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$an_ip\E(?:\s|$)/,
'the shadow carries a NetBIOS-reported IP when n_netbios is on';
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $nbt_device->ip, [$nbt_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0);
unlike $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$an_nbname\E(?:\s|$)/,
'the shadow does not carry the NetBIOS name when n_netbios is off, the conditional join is worth having';
unlike $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$a_nbuser\E(?:\s|$)/,
'the shadow does not carry the NetBIOS user when n_netbios is off';
unlike $nbt_row->{nodes_search}, qr/(?:^|\s)\Q$an_ip\E(?:\s|$)/,
'the shadow does not carry the NetBIOS-reported IP when n_netbios is off';
}
# Outside the SKIP block deliberately: inside it, CI would skip this and still
# report PASS.
require App::Netdisco::DB::Result::DevicePort;
ok !App::Netdisco::DB::Result::DevicePort->can('nodes_search'),
'nodes_search is not a method, so Template Toolkit reaches the hash key';
# _shadow_active_fragment reads $nodes_name alone, so it needs no database.
require App::Netdisco::Web::Plugin::Device::Ports;
is App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
'AND n.active', 'active_nodes gets the active-only fragment';
is App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes_with_age'),
'AND n.active', 'active_nodes_with_age gets the active-only fragment too';
is App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('nodes'),
'', 'nodes (the archived view) gets no fragment, the whole table';
is App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('nodes_with_age'),
'', 'nodes_with_age gets no fragment either';
# _augment_neighbor_search keeps a port's own neighbor identity findable once
# c_nodes is on, the two cells being one <td> whose search text data-search
# replaces wholesale. A fake row only needs the accessors it reads.
{
package Fake::NeighborRow;
sub new { my ($class, %args) = @_; return bless { %args }, $class }
sub remote_ip { return $_[0]->{remote_ip} }
sub remote_port { return $_[0]->{remote_port} }
sub remote_dns { return $_[0]->{remote_dns} }
sub remote_id { return $_[0]->{remote_id} }
sub remote_type { return $_[0]->{remote_type} }
sub remote_inventory { return $_[0]->{remote_inventory} }
sub get_column { my ($self, $col) = @_; return $self->{$col} }
}
my $aliased = Fake::NeighborRow->new(
remote_ip => '10.0.0.9', remote_port => 'Gi0/1', remote_dns => 'switch-a',
neighbor_ip => '10.0.0.9', neighbor_dns => 'switch-a.example.com',
nodes_search => 'aa:bb:cc:dd:ee:ff',
);
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search([$aliased], 1, 0);
like $aliased->{nodes_search}, qr/switch-a\.example\.com/,
'a discovered neighbor alias name is appended when c_neighbors is on';
like $aliased->{nodes_search}, qr/\Qaa:bb:cc:dd:ee:ff\E/,
'the existing shadow text is kept, not replaced';
# With c_neighbors off the +select supplying neighbor_ip/neighbor_dns is not
# in the query, so they must not be read at all.
my $unaliased = Fake::NeighborRow->new(
remote_ip => '10.0.0.9', remote_port => 'Gi0/1', remote_dns => 'switch-a',
nodes_search => '',
);
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search([$unaliased], 0, 0);
like $unaliased->{nodes_search}, qr/switch-a/,
'remote_dns is appended even with c_neighbors off, it is a real column';
unlike $unaliased->{nodes_search}, qr/example\.com/,
'neighbor_ip/neighbor_dns are never read when c_neighbors is off';
my $plain = Fake::NeighborRow->new(nodes_search => 'untouched');
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search([$plain], 1, 0);
is $plain->{nodes_search}, 'untouched',
'a port with no neighbor data keeps its shadow text unchanged';
# remote_id and remote_type are device_port columns, appended unconditionally:
# the template renders remote_type with no n_detailed_inventory guard.
my $identified = Fake::NeighborRow->new(
remote_ip => '10.0.0.9', remote_id => 'chassis-42', remote_type => 'wap-model-x',
nodes_search => '',
);
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search([$identified], 0, 0);
like $identified->{nodes_search}, qr/chassis-42/,
'remote_id is appended unconditionally';
like $identified->{nodes_search}, qr/wap-model-x/,
'remote_type is appended unconditionally';
# remote_inventory is a method reading columns only with_remote_inventory
# selects, which the route applies only under n_inventory. The fake row dies
# if it is called, so this proves the guard prevents the call rather than
# merely skipping the append: an unlike assertion would also pass on an
xt/55-ports-search-shadow.t view on Meta::CPAN
sub new { my ($class, %args) = @_; return bless { %args }, $class }
sub remote_ip { return $_[0]->{remote_ip} }
sub remote_port { return $_[0]->{remote_port} }
sub remote_dns { return $_[0]->{remote_dns} }
sub remote_id { return $_[0]->{remote_id} }
sub remote_type { return $_[0]->{remote_type} }
sub get_column { my ($self, $col) = @_; return $self->{$col} }
sub remote_inventory {
die "remote_inventory called with n_inventory off, simulating a column that was never selected\n";
}
}
my $uninventoried = Fake::NeighborRowInventoryDies->new(
remote_ip => '10.0.0.9', nodes_search => '',
);
eval {
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search(
[$uninventoried], 0, 0);
};
is $@, '', 'remote_inventory is never called when n_inventory is off, so it cannot 500 the route';
my $inventoried = Fake::NeighborRow->new(
remote_ip => '10.0.0.9',
remote_inventory => 'Cisco WS-C3560 running 15.2(4)E (SN123456)',
nodes_search => '',
);
App::Netdisco::Web::Plugin::Device::Ports::_augment_neighbor_search([$inventoried], 0, 1);
like $inventoried->{nodes_search}, qr/WS-C3560/,
'the inventory string is appended when n_inventory is on';
# The nodes cell renders manufacturer.abbrev under n_vendor, and the shadow
# replaces that cell's search text wholesale, so the abbrev has to be in it or
# a vendor stops being findable on any port. Its own fixture, deliberately: the
# node needs an oui that joins manufacturer, which the fixtures above do not ask
# for.
SKIP: {
skip "no usable netdisco database: $why", 2 if not $schema;
my ($v_switch, $v_port, $an_abbrev) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT n.switch, n.port, m.abbrev
FROM node n
JOIN manufacturer m ON m.base = n.oui
WHERE n.active AND length(m.abbrev) > 2
LIMIT 1
});
});
skip 'no active node in this database has a manufacturer', 2
if not defined $v_switch;
my $v_device = $schema->resultset('Device')->find($v_switch);
my ($v_row) = grep { $_->port eq $v_port }
$v_device->ports->with_properties->order_by_port_name->all;
skip 'port lookup for the vendor fixture came back empty', 2 if not $v_row;
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $v_device->ip, [$v_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0, '', 1);
like $v_row->{nodes_search}, qr/(?:^|\s)\Q$an_abbrev\E(?:\s|$)/,
'the shadow carries the manufacturer abbreviation when n_vendor is on';
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $v_device->ip, [$v_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0, '', 0);
unlike $v_row->{nodes_search}, qr/(?:^|\s)\Q$an_abbrev\E(?:\s|$)/,
'the shadow leaves the vendor out when n_vendor is off, the join is worth having';
}
# The cell renders the MAC through mac_format_call, so on any setting but IEEE
# the string on screen is not the one Postgres stores. The shadow has to carry
# the displayed form or a user who has picked Cisco cannot filter on the MAC
# they can see. IEEE needs nothing extra: it is what mac::text already gives.
SKIP: {
skip "no usable netdisco database: $why", 3 if not $schema;
my ($m_switch, $m_port, $a_mac) = $schema->storage->dbh_do(sub {
my (undef, $dbh) = @_;
$dbh->selectrow_array(q{
SELECT switch, port, mac::text FROM node WHERE active LIMIT 1
});
});
skip 'no active node in this database', 3 if not defined $m_switch;
my $m_device = $schema->resultset('Device')->find($m_switch);
my ($m_row) = grep { $_->port eq $m_port }
$m_device->ports->with_properties->order_by_port_name->all;
skip 'port lookup for the mac_format fixture came back empty', 3 if not $m_row;
my $cisco = NetAddr::MAC->new(mac => $a_mac)->as_cisco;
my $sun = NetAddr::MAC->new(mac => $a_mac)->as_sun;
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $m_device->ip, [$m_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0, 'Cisco', 0);
like $m_row->{nodes_search}, qr/(?:^|\s)\Q$cisco\E(?:\s|$)/,
'the shadow carries the Cisco form when that is the chosen mac_format';
like $m_row->{nodes_search}, qr/(?:^|\s)\Q$a_mac\E(?:\s|$)/,
'and still carries the canonical form beside it';
App::Netdisco::Web::Plugin::Device::Ports::_attach_search_shadow(
$schema, $m_device->ip, [$m_row],
App::Netdisco::Web::Plugin::Device::Ports::_shadow_active_fragment('active_nodes'),
0, 0, 'Sun', 0);
like $m_row->{nodes_search}, qr/(?:^|\s)\Q$sun\E(?:\s|$)/,
'and the Sun form, whose octets lose their leading zeros';
}
# The shadow is only reachable if DataTables sources the column's filter text
# from data-search, and it decides that from tbody tr:first-child alone, for the
# whole column. Emitting the attribute on the collapsed rows only, which is what
# this cell used to do, left the column reading rendered text and every shadow
# ignored, while every Perl assertion above still passed. The fixture puts one
# row over the collapse threshold and one under it, so a condition creeping back
# onto the attribute fails here.
subtest 'ports_tt__every_connected_nodes_cell__carries_data_search' => sub {
my ($html, $error) = render_template('ajax/device/ports.tt');
is $error, undef, 'renders' or return;
my (undef, @rows) = split m/<tr\b/, $html;
my @node_cells = grep { m/nd_nodes-total|nd_div-closer|data-search/ } @rows;
cmp_ok scalar @node_cells, '>', 1,
'the fixture renders more than one row carrying a nodes cell';
my $with_attr = grep { m/<td[^>]*\sdata-search="/ } @node_cells;
is $with_attr, scalar @node_cells,
'every row with a nodes cell carries data-search, not just the collapsed ones';
};
done_testing;
( run in 1.090 second using v1.01-cache-2.11-cpan-b16cb0d3907 )