use Mojo::Base -strict; use Test2::V0; use FindBin; use lib "${FindBin::Bin}/../lib/"; use lib "${FindBin::Bin}/lib/"; use Dashboard::Test qw(test_app test_empty_app); my $t = test_app(); subtest 'weather model stores and retrieves data' => sub { my $weather = $t->app->weather; ok $weather, 'weather helper is available'; my $now = time; my @rows = map { { epoch => ( int( $now / 3600 ) - $_ ) * 3600, temperature => 20 + $_ * 0.5, humidity => 60 + $_, rain => $_ % 3 == 0 ? 0.2 : 0, } } 1 .. 12; my $stored = $weather->store_batch( \@rows ); is $stored, 12, 'stored 12 weather points'; my $range = $weather->get_range( $rows[-1]{epoch}, $rows[0]{epoch} ); is scalar @$range, 12, 'get_range returns all 12 points'; ok $range->[0]{temperature}, 'points have temperature'; ok $range->[0]{humidity}, 'points have humidity'; my $latest = $weather->latest; ok $latest, 'latest returns a row'; is $latest->{epoch}, $rows[0]{epoch}, 'latest is the most recent epoch'; }; subtest 'weather model deduplicates on epoch' => sub { my $weather = $t->app->weather; my $epoch = int( time / 3600 ) * 3600; $weather->store_batch( [ { epoch => $epoch, temperature => 25, humidity => 70, rain => 0 } ] ); $weather->store_batch( [ { epoch => $epoch, temperature => 26, humidity => 71, rain => 0.1 } ] ); my $range = $weather->get_range( $epoch, $epoch ); is scalar @$range, 1, 'only one row for the same epoch'; is $range->[0]{temperature}, 26, 'second insert replaces first'; }; subtest 'weather API hourly endpoint' => sub { $t->get_ok('/api/weather/hourly?hours=24') ->status_is(200) ->json_has('/points') ->json_is( '/hours' => 24 ); my $points = $t->tx->res->json->{points}; ok @$points > 0, 'hourly returns seeded weather points'; ok exists $points->[0]{temperature}, 'points have temperature'; ok exists $points->[0]{humidity}, 'points have humidity'; ok exists $points->[0]{rain}, 'points have rain'; ok exists $points->[0]{timestamp}, 'points have timestamp'; }; subtest 'weather API forecast endpoint' => sub { # Seed some future data my $future = ( int( time / 3600 ) + 2 ) * 3600; $t->app->weather->store_batch( [ { epoch => $future, temperature => 28, humidity => 55, rain => 0 }, { epoch => $future + 3600, temperature => 27, humidity => 58, rain => 0.5 }, ] ); $t->get_ok('/api/weather/forecast') ->status_is(200) ->json_has('/points') ->json_has('/from'); my $points = $t->tx->res->json->{points}; ok @$points > 0, 'forecast returns points'; }; subtest 'weather API current endpoint excludes future forecasts' => sub { my $current_hour = int( time / 3600 ) * 3600; my $future = $current_hour + 3600; $t->app->weather->store_batch( [ { epoch => $current_hour, temperature => 22, humidity => 65, rain => 0, }, { epoch => $future, temperature => 99, humidity => 1, rain => 10, }, ] ); my $latest = $t->app->weather->latest_at(time); is $latest->{epoch}, $current_hour, 'model selects the latest point at or before now'; $t->get_ok('/api/weather/current') ->status_is(200) ->json_is( '/temperature' => 22 ) ->json_is( '/timestamp' => utc_timestamp($current_hour) ); }; subtest 'weather API refresh endpoint is POST-only' => sub { local $t->app->config->{weather}{enabled} = 0; $t->get_ok('/api/weather/refresh')->status_is(404); $t->post_ok('/api/weather/refresh') ->status_is(200) ->json_is( '/status' => 'disabled' ); }; subtest 'weather page renders' => sub { $t->get_ok('/weather') ->status_is(200) ->text_is( 'title', 'FAPG DAQ Weather' ) ->element_exists('canvas#chart-weather-combined') ->element_exists('canvas#chart-weather-solar') ->element_exists('canvas#chart-weather-ground') ->element_exists( 'main section[data-controls-panel][aria-labelledby="weather-controls-title"]' ) ->text_is( '#weather-controls-title', 'Weather controls' ) ->element_exists( '#weather-controls .graph-control[data-default-timeframe="day"]') ->element_exists('[data-default-timeframe="day"]'); }; subtest 'weather page has nav link' => sub { $t->get_ok('/weather') ->status_is(200) ->element_exists('.navbar-nav a.nav-link.active[href="/weather"]'); }; subtest 'index page has weather forecast card' => sub { $t->get_ok('/') ->status_is(200) ->element_exists('[data-weather-forecast]') ->element_exists('canvas#chart-weather-forecast'); }; subtest 'empty database weather endpoints' => sub { my $empty = test_empty_app(); $empty->get_ok('/api/weather/hourly') ->status_is(200) ->json_is( '/points' => [] ); $empty->get_ok('/api/weather/current') ->status_is(200) ->json_is( '/temperature' => undef ); $empty->get_ok('/weather')->status_is(200); }; done_testing; sub utc_timestamp { my ($epoch) = @_; require POSIX; return POSIX::strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime $epoch ); }