tests/testthat/test-metno_api.R

dummy_email <- "test@example.com"

load_metno_fixture <- function() {
  # locate canonical fixture under tests/fixtures
  fp <- testthat::test_path("../fixtures/metno_forecast_compact.json")
  cassette <- jsonlite::fromJSON(fp, simplifyVector = FALSE)
  interaction <- cassette$http_interactions[[1]]
  interaction$response$headers <- lapply(interaction$response$headers, identity)
  interaction
}

mock_metno_call <- function(latitude = -27.5, longitude = 153.0, format = "compact") {
  interaction <- load_metno_fixture()
  body_string <- interaction$response$body$string
  headers <- interaction$response$headers
  status_code <- interaction$response$status
  if (is.null(status_code)) status_code <- 200

  temp_response <- list(
    status_code = status_code,
    parse = function(encoding = "UTF-8") body_string,
    response_headers = headers
  )

  temp_client <- list(
    get = function(query, headers = NULL) temp_response
  )

  # create a fake factory that returns our fake client
  temp_factory <- function(endpoint, user_agent, timeout = 30) {
    temp_client
  }

  # mock the package-internal factory so we don't bind into crul
  testthat::with_mocked_bindings(
    get_metno_forecast(
      latitude = latitude,
      longitude = longitude,
      format = format,
      api_key = dummy_email,
      use_cache = FALSE
    ),
    `.metno_http_client` = temp_factory,
    .package = "weatherOz"
  )
}


test_that("get_metno_forecast returns structured forecast with metadata (AWST)", {
  forecast <- mock_metno_call()

  expect_type(forecast, "list")
  expect_s3_class(forecast$data, "data.table")
  expect_true("time" %in% names(forecast$data))
  expect_gt(nrow(forecast$data), 0)
  # times should be in Australia/Perth
  tz_attr <- attr(forecast$data$time, "tzone")
  expect_true(!is.null(tz_attr) && tz_attr == "Australia/Perth")
  expect_type(forecast$metadata, "list")
  expect_true(!is.null(forecast$metadata$retrieved_at))
})

test_that("get_metno_forecast validates inputs", {
  # latitude out of bounds
  expect_error(get_metno_forecast(latitude = -100, longitude = 153, api_key = dummy_email))
  # longitude out of bounds
  expect_error(get_metno_forecast(latitude = -27.5, longitude = 300, api_key = dummy_email))
  # invalid email
  expect_error(get_metno_forecast(latitude = -27.5, longitude = 153, api_key = "invalid"))
})

test_that("metno_timeseries_to_data_table converts timeseries to data.table and AWST", {
  interaction <- load_metno_fixture()
  body_string <- interaction$response$body$string
  parsed <- fromJSON(body_string, simplifyVector = FALSE)
  ts <- parsed$properties$timeseries
  dt_hourly <- metno_timeseries_to_data_table(ts)
  expect_s3_class(dt_hourly, "data.table")
  expect_true(nrow(dt_hourly) > 0)
  expect_true(all(attr(dt_hourly$time, "tzone") == "Australia/Perth"))
})

test_that("metno_timeseries_to_data_table handles empty input", {
  empty_dt <- metno_timeseries_to_data_table(list())
  expect_equal(nrow(empty_dt), 0)
})

test_that("metno_get_dominant_symbol identifies dominant code", {
  expect_equal(weatherOz:::metno_get_dominant_symbol(c()), NA_character_)
})

test_that("metno_resample_data_table aggregates to daily correctly", {
  forecast <- mock_metno_call()
  dt_hourly <- forecast$data
  dt_daily <- metno_resample_data_table(dt_hourly, "daily")
  expect_s3_class(dt_daily, "data.table")
  expect_true("date" %in% names(dt_daily))
  expect_true(inherits(dt_daily$date, "Date"))
})

test_that(".parse and .format metno http dates round trip", {
  # load headers from fixture
  interaction <- load_metno_fixture()
  headers <- interaction$response$headers
  # headers from VCR fixtures are lower-case; use 'expires' key
  expires_raw <- headers$expires[[1]]
  parsed <- weatherOz:::.parse_metno_http_date(expires_raw)
  expect_true(inherits(parsed, "POSIXct"))
  formatted <- weatherOz:::.format_metno_http_date(parsed)
  expect_true(is.character(formatted))
  expect_null(weatherOz:::.format_metno_http_date(NULL))
})

test_that("metno_timeseries_to_data_table converts timeseries to data.table", {
  forecast <- mock_metno_call()
  timeseries <- forecast$raw$properties$timeseries
  dt_hourly <- weatherOz:::metno_timeseries_to_data_table(timeseries)

  expect_s3_class(dt_hourly, "data.table")
  expect_true("time" %in% names(dt_hourly))
  expect_true(inherits(dt_hourly$time, "POSIXct"))
  expect_true("air_temperature" %in% names(dt_hourly))
  expect_true("precipitation_amount" %in% names(dt_hourly))
  expect_true("symbol_code" %in% names(dt_hourly))
  expect_true(all(lubridate::tz(dt_hourly$time) == "Australia/Perth")) # timezone aware (AWST)
})

test_that("metno_timeseries_to_data_table handles empty input", {
  empty_dt <- weatherOz:::metno_timeseries_to_data_table(list())
  expect_s3_class(empty_dt, "data.table")
  expect_equal(nrow(empty_dt), 0)
})

test_that("metno_get_dominant_symbol identifies dominant code", {
  expect_equal(weatherOz:::metno_get_dominant_symbol(c("clearsky_day", "fair_day", "partlycloudy_day")), "partlycloudy_day")
  expect_equal(weatherOz:::metno_get_dominant_symbol(c("rain", "clearsky_day", "thunder")), "thunder")
  expect_equal(weatherOz:::metno_get_dominant_symbol(c()), NA_character_)
})

test_that("metno_resample_data_table aggregates correctly", {
  forecast <- mock_metno_call()
  dt_hourly <- forecast$data
  dt_daily <- weatherOz:::metno_resample_data_table(dt_hourly, "daily")

  expect_s3_class(dt_daily, "data.table")
  expect_true("date" %in% names(dt_daily))
  expect_true("min_temperature" %in% names(dt_daily))
  expect_lt(nrow(dt_daily), nrow(dt_hourly))

  dt_hourly_again <- weatherOz:::metno_resample_data_table(dt_hourly, "hourly")
  expect_s3_class(dt_hourly_again, "data.table")
  expect_equal(dt_hourly_again$date, dt_hourly$time)
})

test_that("get_metno_daily_forecast aggregates daily data", {
  mock_result <- mock_metno_call()
  with_mocked_bindings(
    {
      daily <- get_metno_daily_forecast(
        latitude = -27.5,
        longitude = 153.0,
        days = 5,
        api_key = dummy_email
      )
      expect_s3_class(daily, "data.table")
      expect_equal(nrow(daily), 5)
      expect_true("date" %in% names(daily))
    },
    get_metno_forecast = function(...) mock_result,
    .package = "weatherOz"
  )
})

test_that("get_metno_forecast reuses cached forecast when fresh", {
  cache_dir <- withr::local_tempdir()
  vcr::use_cassette("metno_cache_reuse_compact", {
    skip_if_offline()
    first <- get_metno_forecast(
      latitude = -27.5,
      longitude = 153.0,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
    second <- get_metno_forecast(
      latitude = -27.5,
      longitude = 153.0,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
  })
  expect_identical(first$metadata$cache$source, "network")
  expect_identical(second$metadata$cache$source, "cache")
  expect_identical(second$metadata$cache$used_if_modified_since, FALSE)
})

test_that("get_metno_forecast sends conditional revalidation for stale cache", {
  cache_dir <- withr::local_tempdir()

  vcr::use_cassette("metno_cache_revalidate_compact", {
    skip_if_offline()
    first <- get_metno_forecast(
      latitude = -27.5,
      longitude = 153.0,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )

    cache_path <- first$metadata$cache$cache_path
    expect_true(file.exists(cache_path))

    cached <- readRDS(cache_path)
    cached$metadata$expires <- lubridate::now(tzone = "Australia/Perth") - lubridate::minutes(1)
    saveRDS(cached, file = cache_path)

    second <- get_metno_forecast(
      latitude = -27.5,
      longitude = 153.0,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
  })
  expect_identical(second$metadata$cache$source, "revalidated")
  expect_identical(second$metadata$cache$used_if_modified_since, TRUE)
})

test_that("get_metno_daily_forecast reuses hourly session cache", {
  cache_dir <- withr::local_tempdir()
  vcr::use_cassette("metno_daily_cache_reuse_compact", {
    skip_if_offline()
    get_metno_forecast(
      latitude = -27.5,
      longitude = 153.0,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
    first <- get_metno_daily_forecast(
      latitude = -27.5,
      longitude = 153.0,
      days = 3,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
    second <- get_metno_daily_forecast(
      latitude = -27.5,
      longitude = 153.0,
      days = 3,
      api_key = dummy_email,
      use_cache = TRUE,
      cache_dir = cache_dir
    )
  })
  expect_s3_class(first, "data.table")
  expect_s3_class(second, "data.table")
  expect_equal(nrow(first), 3)
  expect_equal(nrow(second), 3)
})

test_that(".parse and .format metno http dates round trip", {
  timestamp <- "Wed, 29 Oct 2025 01:43:37 GMT"
  parsed <- weatherOz:::.parse_metno_http_date(timestamp)
  expect_true(inherits(parsed, "POSIXct"))
  expect_equal(weatherOz:::.format_metno_http_date(parsed), timestamp)
  expect_null(weatherOz:::.parse_metno_http_date(NULL))
  expect_null(weatherOz:::.format_metno_http_date(NULL))
})

Try the weatherOz package in your browser

Any scripts or data that you put into this service are public.

weatherOz documentation built on April 8, 2026, 9:09 a.m.