View raw

1 use Mojo::Base -strict; 2 3 use Test2::V0; 4 5 use FindBin; 6 use lib "${FindBin::Bin}/../lib/"; 7 use lib "${FindBin::Bin}/lib/"; 8 use Dashboard::Test qw(test_app test_empty_app); 9 10 my $t = test_app(); 11 12 subtest 'weather model stores and retrieves data' => sub { 13 my $weather = $t->app->weather; 14 ok $weather, 'weather helper is available'; 15 16 my $now = time; 17 my @rows = map { 18 { epoch => ( int( $now / 3600 ) - $_ ) * 3600, 19 temperature => 20 + $_ * 0.5, 20 humidity => 60 + $_, 21 rain => $_ % 3 == 0 ? 0.2 : 0, 22 } 23 } 1 .. 12; 24 25 my $stored = $weather->store_batch( \@rows ); 26 is $stored, 12, 'stored 12 weather points'; 27 28 my $range = $weather->get_range( $rows[-1]{epoch}, $rows[0]{epoch} ); 29 is scalar @$range, 12, 'get_range returns all 12 points'; 30 ok $range->[0]{temperature}, 'points have temperature'; 31 ok $range->[0]{humidity}, 'points have humidity'; 32 33 my $latest = $weather->latest; 34 ok $latest, 'latest returns a row'; 35 is $latest->{epoch}, $rows[0]{epoch}, 'latest is the most recent epoch'; 36 }; 37 38 subtest 'weather model deduplicates on epoch' => sub { 39 my $weather = $t->app->weather; 40 my $epoch = int( time / 3600 ) * 3600; 41 42 $weather->store_batch( 43 [ { epoch => $epoch, temperature => 25, humidity => 70, rain => 0 } ] 44 ); 45 $weather->store_batch( 46 [ { epoch => $epoch, 47 temperature => 26, 48 humidity => 71, 49 rain => 0.1 50 } 51 ] 52 ); 53 54 my $range = $weather->get_range( $epoch, $epoch ); 55 is scalar @$range, 1, 'only one row for the same epoch'; 56 is $range->[0]{temperature}, 26, 'second insert replaces first'; 57 }; 58 59 subtest 'weather API hourly endpoint' => sub { 60 $t->get_ok('/api/weather/hourly?hours=24') 61 ->status_is(200) 62 ->json_has('/points') 63 ->json_is( '/hours' => 24 ); 64 65 my $points = $t->tx->res->json->{points}; 66 ok @$points > 0, 'hourly returns seeded weather points'; 67 ok exists $points->[0]{temperature}, 'points have temperature'; 68 ok exists $points->[0]{humidity}, 'points have humidity'; 69 ok exists $points->[0]{rain}, 'points have rain'; 70 ok exists $points->[0]{timestamp}, 'points have timestamp'; 71 }; 72 73 subtest 'weather API forecast endpoint' => sub { 74 75 # Seed some future data 76 my $future = ( int( time / 3600 ) + 2 ) * 3600; 77 $t->app->weather->store_batch( 78 [ { epoch => $future, 79 temperature => 28, 80 humidity => 55, 81 rain => 0 82 }, 83 { epoch => $future + 3600, 84 temperature => 27, 85 humidity => 58, 86 rain => 0.5 87 }, 88 ] 89 ); 90 91 $t->get_ok('/api/weather/forecast') 92 ->status_is(200) 93 ->json_has('/points') 94 ->json_has('/from'); 95 96 my $points = $t->tx->res->json->{points}; 97 ok @$points > 0, 'forecast returns points'; 98 }; 99 100 subtest 'weather API current endpoint excludes future forecasts' => sub { 101 my $current_hour = int( time / 3600 ) * 3600; 102 my $future = $current_hour + 3600; 103 104 $t->app->weather->store_batch( 105 [ { epoch => $current_hour, 106 temperature => 22, 107 humidity => 65, 108 rain => 0, 109 }, 110 { epoch => $future, 111 temperature => 99, 112 humidity => 1, 113 rain => 10, 114 }, 115 ] 116 ); 117 118 my $latest = $t->app->weather->latest_at(time); 119 is $latest->{epoch}, $current_hour, 120 'model selects the latest point at or before now'; 121 122 $t->get_ok('/api/weather/current') 123 ->status_is(200) 124 ->json_is( '/temperature' => 22 ) 125 ->json_is( '/timestamp' => utc_timestamp($current_hour) ); 126 }; 127 128 subtest 'weather API refresh endpoint is POST-only' => sub { 129 local $t->app->config->{weather}{enabled} = 0; 130 131 $t->get_ok('/api/weather/refresh')->status_is(404); 132 $t->post_ok('/api/weather/refresh') 133 ->status_is(200) 134 ->json_is( '/status' => 'disabled' ); 135 }; 136 137 subtest 'weather page renders' => sub { 138 $t->get_ok('/weather') 139 ->status_is(200) 140 ->text_is( 'title', 'FAPG DAQ Weather' ) 141 ->element_exists('canvas#chart-weather-combined') 142 ->element_exists('canvas#chart-weather-solar') 143 ->element_exists('canvas#chart-weather-ground') 144 ->element_exists( 145 'main section[data-controls-panel][aria-labelledby="weather-controls-title"]' 146 ) 147 ->text_is( '#weather-controls-title', 'Weather controls' ) 148 ->element_exists( 149 '#weather-controls .graph-control[data-default-timeframe="day"]') 150 ->element_exists('[data-default-timeframe="day"]'); 151 }; 152 153 subtest 'weather page has nav link' => sub { 154 $t->get_ok('/weather') 155 ->status_is(200) 156 ->element_exists('.navbar-nav a.nav-link.active[href="/weather"]'); 157 }; 158 159 subtest 'index page has weather forecast card' => sub { 160 $t->get_ok('/') 161 ->status_is(200) 162 ->element_exists('[data-weather-forecast]') 163 ->element_exists('canvas#chart-weather-forecast'); 164 }; 165 166 subtest 'empty database weather endpoints' => sub { 167 my $empty = test_empty_app(); 168 $empty->get_ok('/api/weather/hourly') 169 ->status_is(200) 170 ->json_is( '/points' => [] ); 171 $empty->get_ok('/api/weather/current') 172 ->status_is(200) 173 ->json_is( '/temperature' => undef ); 174 $empty->get_ok('/weather')->status_is(200); 175 }; 176 177 done_testing; 178 179 sub utc_timestamp { 180 my ($epoch) = @_; 181 require POSIX; 182 return POSIX::strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime $epoch ); 183 } 184