diff --git a/NAMESPACE b/NAMESPACE index 192616b..9a71656 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,6 +20,7 @@ export(gfw_regions) export(gfw_sar_vessel_detections) export(gfw_user_agent) export(gfw_vessel_info) +export(gfw_vessel_insights) export(marine_regions) export(parse_response_error) export(test_shape) diff --git a/R/gfw_vessel_insights.R b/R/gfw_vessel_insights.R new file mode 100644 index 0000000..b617ee5 --- /dev/null +++ b/R/gfw_vessel_insights.R @@ -0,0 +1,267 @@ +#' Retrieve vessel insights for one or several vessel IDs +#' +#' @description +#' The Global Fishing Watch (GFW) Insights API provides a set of vessel-level +#' analytical indicators ("vessel insights") that combine information on a +#' vessel's observed activity (primarily derived from AIS), vessel identity +#' records, and publicly available authorizations. +#' +#' The primary objective of vessel insights is to support risk-based +#' decision-making, operational planning, and due diligence by helping users +#' identify vessel characteristics and behaviors that may indicate an increased +#' likelihood of involvement in Illegal, Unreported, or Unregulated (IUU) +#' fishing. +#' +#' This function retrieves vessel insights for one or more vessel identifiers (IDs) +#' over a specified time period. Users may specify which insight types to +#' include. The response summarizes detected events and indicators; detailed +#' event-level information can be retrieved separately using the +#' Events API: https://globalfishingwatch.org/our-apis/documentation#events-api. +#' +#' @details +#' The following insight types are supported via the `includes` argument: +#' - Any apparent fishing events in no-take MPAs (`"FISHING"`) +#' - Any apparent fishing events detected in areas with no known RFMO authorization (`"FISHING"`) +#' - The vessel's AIS coverage metric (`"COVERAGE"`) +#' - Any AIS off/disabling events (`"GAP"`) +#' - If the vessel is present on an RFMO IUU vessel list (`"VESSEL-IDENTITY-IUU-VESSEL-LIST"`) +#' - The vessel's flag changes (`"VESSEL-IDENTITY-FLAG-CHANGES"`) +#' - The vessel's flag state presence under the Tokyo/Paris MOU black or grey lists (`"VESSEL-IDENTITY-MOU-LIST"`) +#' +#' The function returns a single-row tibble with one list-column per insight +#' type requested. Each list-column contains the data corresponding to that insight type. +#' +#' For detailed information about the Insights API, please refer to the official +#' Global Fishing Watch API documentation: +#' - https://globalfishingwatch.org/our-apis/documentation#insights-api +#' +#' For more details on the Insights API data caveats, please refer to the official +#' Global Fishing Watch API documentation: +#' - https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas +#' - https://globalfishingwatch.org/our-apis/documentation#what-does-it-mean-that-an-api-dataset-is-in-prototype-stage +#' - https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas +#' - https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage +#' - https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list +#' +#' @param includes Required. Character vector of insight types to include in +#' the response. Allowed values: `"COVERAGE"`, `"FISHING"`, `"GAP"`, `"VESSEL-IDENTITY-FLAG-CHANGES"`, +#' `"VESSEL-IDENTITY-IUU-VESSEL-LIST"`, `"VESSEL-IDENTITY-MOU-LIST"`. Example: `c("FISHING", "GAP")`. +#' +#' @param start_date Required. The start date for the insights period in +#' `"YYYY-MM-DD"` format or Date. Example: `"2020-01-01"`. +#' +#' @param end_date Required. The end date for the insights period in +#' `"YYYY-MM-DD"` format or Date. Example: `"2025-03-03"`. +#' +#' @param vessels Required. Character vector of vessel IDs to retrieve insights for. +#' Each vessel ID must be a non-empty character string. +#' Example: `c("785101812-2127-e5d2-e8bf-7152c5259f5f", "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb")`. +#' +#' @param key Character, API token. Defaults to [gfw_auth()]. +#' +#' @param print_request Boolean. Whether to print the request, for debugging +#' purposes. When contacting the GFW team it will be useful to send this string. +#' +#' @return +#' A single-row tibble where each column corresponds to a requested insight +#' type. Columns are list-columns containing the data returned by the Insights API. +#' +#' @examples +#' \dontrun{ +#' library(gfwr) +#' +#' # Retrieve apparent fishing-related insights for a single vessel +#' fishing_insights <- gfw_vessel_insights( +#' includes = c("FISHING"), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f"), +#' print_request = TRUE +#' ) +#' +#' # Retrieve AIS gap (AIS-off) insights for a single vessel +#' gap_insights <- gfw_vessel_insights( +#' includes = c("GAP"), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c("2339c52c3-3a84-1603-f968-d8890f23e1ed") +#' ) +#' +#' # Retrieve AIS coverage metrics insights for a single vessel +#' coverage_insights <- gfw_vessel_insights( +#' includes = c("COVERAGE"), +#' start_date = as.Date("2020-01-01"), +#' end_date = as.Date("2025-03-03"), +#' vessels = c("2339c52c3-3a84-1603-f968-d8890f23e1ed") +#' ) +#' +#' # Retrieve being listed in IUU list insights for a single vessel +#' iuu_insights <- gfw_vessel_insights( +#' includes = c("VESSEL-IDENTITY-IUU-VESSEL-LIST"), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c("2d26aa452-2d4f-4cae-2ec4-377f85e88dcb") +#' ) +#' +#' # Retrieve flag changes insights for a single vessel +#' flag_changes_insights <- gfw_vessel_insights( +#' includes = c("VESSEL-IDENTITY-FLAG-CHANGES"), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c("2d26aa452-2d4f-4cae-2ec4-377f85e88dcb") +#' ) +#' +#' # Retrieve flag state presence under the Tokyo/Paris MOU black or grey lists insights for a single vessel +#' mou_insights <- gfw_vessel_insights( +#' includes = c("VESSEL-IDENTITY-MOU-LIST"), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c("2339c52c3-3a84-1603-f968-d8890f23e1ed") +#' ) +#' +#' # Retrieve all available insights for multiple vessels +#' all_insights <- gfw_vessel_insights( +#' includes = c( +#' "COVERAGE", +#' "FISHING", +#' "GAP", +#' "VESSEL-IDENTITY-FLAG-CHANGES", +#' "VESSEL-IDENTITY-IUU-VESSEL-LIST", +#' "VESSEL-IDENTITY-MOU-LIST" +#' ), +#' start_date = "2020-01-01", +#' end_date = "2025-03-03", +#' vessels = c( +#' "785101812-2127-e5d2-e8bf-7152c5259f5f", +#' "2339c52c3-3a84-1603-f968-d8890f23e1ed", +#' "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb" +#' ), +#' print_request = TRUE +#' ) +#' } +#' +#' @export +gfw_vessel_insights <- function(includes = NULL, + start_date = NULL, + end_date = NULL, + vessels = NULL, + key = gfw_auth(), + print_request = FALSE) { + # Validate includes --------------------------------------------------------- + + allowed_includes <- c( + "COVERAGE", + "FISHING", + "GAP", + "VESSEL-IDENTITY-FLAG-CHANGES", + "VESSEL-IDENTITY-IUU-VESSEL-LIST", + "VESSEL-IDENTITY-MOU-LIST" + ) + + if (!is.character(includes) || length(includes) == 0) { + rlang::abort("`includes` is required and must be a non-empty character vector.") + } + + includes <- trimws(toupper(includes)) + invalid_includes <- setdiff(includes, allowed_includes) + + if (length(invalid_includes) > 0) { + invalid_includes_message <- glue::glue( + "Invalid `includes` value(s): {paste(invalid_includes, collapse = ', ')}. ", + "Allowed value(s): {paste(allowed_includes, collapse = ', ')}." + ) + rlang::abort(invalid_includes_message) + } + + # Validate dates ------------------------------------------------------------ + + parse_date <- function(x, name) { + if (inherits(x, "Date")) { + return(x) + } + if (is.character(x)) { + parsed <- suppressWarnings(lubridate::ymd(x)) + if (!is.na(parsed)) { + return(parsed) + } + } + invalid_date_message <- glue::glue( + "Invalid `{name}`: {x}. ", + "`{name}` must be in YYYY-MM-DD format or Date object." + ) + rlang::abort(invalid_date_message) + } + + start_date <- parse_date(start_date, "start_date") + end_date <- parse_date(end_date, "end_date") + + if (start_date > end_date) { + rlang::abort("`start_date` must be less than or equal to `end_date`.") + } + + # Validate vessels ---------------------------------------------------------- + + if (!is.character(vessels) || length(vessels) == 0) { + rlang::abort("`vessels` is required and must be a non-empty character vector.") + } + + vessels <- trimws(vessels) + invalid_vessels <- vessels[is.na(vessels) | vessels == ""] + + if (length(invalid_vessels) > 0) { + invalid_vessels_message <- glue::glue( + "Invalid `vessels` value(s): {paste(invalid_vessels, collapse = ', ')}. ", + "Each vessel ID must be a non-empty, non-NA character string." + ) + rlang::abort(invalid_vessels_message) + } + + # Validate key -------------------------------------------------------------- + if (is.null(key) || identical(key, "") || is.na(key)) { + rlang::abort("No API token found. Set `GFW_TOKEN` or pass `key`.") + } + + # Build API request body ---------------------------------------------------- + dataset_id <- "public-global-vessel-identity:latest" + req_body <- list( + includes = as.list(includes), + startDate = format(start_date, "%Y-%m-%d"), + endDate = format(end_date, "%Y-%m-%d"), + vessels = purrr::map(vessels, \(x) list(datasetId = dataset_id, vesselId = x)) + ) + + # Build API request --------------------------------------------------------- + req <- httr2::request(gfw_base_url()) |> + httr2::req_url_path_append("insights/vessels") |> + httr2::req_headers( + Authorization = paste("Bearer", key), + `Content-Type` = "application/json" + ) |> + httr2::req_user_agent(gfw_user_agent()) |> + httr2::req_body_json(req_body) + + if (print_request) { + print(req) + } + + # Attach error parser + req <- req |> httr2::req_error(body = \(x) parse_response_error(x)$formatted) + + # Perform request + resp <- req |> httr2::req_perform() + + # Build API response -------------------------------------------------------- + + # Extract JSON response body + resp_body <- httr2::resp_body_json(resp, check_type = TRUE, simplifyVector = FALSE) + + # Normalize response body + resp_body <- resp_body |> + purrr::map(\(x) if (is.null(x) || identical(x, NA)) list() else x) |> + purrr::map(\(x) list(x)) + + # Transform response body to dataframe + resp_df <- tibble::tibble(!!!resp_body) + + return(resp_df) +} diff --git a/tests/testthat/fixtures/insights/vessel_insight_item.json b/tests/testthat/fixtures/insights/vessel_insight_item.json new file mode 100644 index 0000000..23d07aa --- /dev/null +++ b/tests/testthat/fixtures/insights/vessel_insight_item.json @@ -0,0 +1,99 @@ +{ + "period": { + "startDate": "2020-01-01", + "endDate": "2025-03-03" + }, + "vesselIdsWithoutIdentity": null, + "gap": { + "datasets": ["public-global-gaps-events:v3.0"], + "historicalCounters": { + "events": 1, + "eventsGapOff": 1, + "eventsInNoTakeMPAs": 5, + "eventsInRFMOWithoutKnownAuthorization": 10 + }, + "periodSelectedCounters": { + "events": 4, + "eventsGapOff": 4, + "eventsInNoTakeMPAs": 5, + "eventsInRFMOWithoutKnownAuthorization": 10 + }, + "aisOff": [ + "5c08c6e146315b98d8569dd253681ace", + "b058be4456f7d17f89f95643a1bfb375" + ] + }, + "coverage": { + "blocks": "53447", + "blocksWithPositions": "51355", + "percentage": 96.08584204913278 + }, + "apparentFishing": { + "datasets": ["public-global-fishing-events:v3.0"], + "historicalCounters": { + "events": 2546, + "eventsInRFMOWithoutKnownAuthorization": 0, + "eventsInNoTakeMPAs": 18 + }, + "periodSelectedCounters": { + "events": 2882, + "eventsInRFMOWithoutKnownAuthorization": 0, + "eventsInNoTakeMPAs": 19 + }, + "eventsInRfmoWithoutKnownAuthorization": [ + "6a2c2930d7eead8ed729f17922ce9b40" + ], + "eventsInNoTakeMpas": [ + "6a2c2930d7eead8ed729f17922ce9b40", + "28cb9b7899699dc362580e6c816f18d6" + ] + }, + "vesselIdentity": { + "datasets": ["public-global-vessel-identity:v3.0"], + "flagsChanges": { + "totalTimesListed": 10, + "totalTimesListedInThePeriod": 1, + "valuesInThePeriod": [ + { + "from": "2012-01-19T14:24:39Z", + "to": "2024-03-04T23:59:41Z", + "value": "CHL" + } + ] + }, + "iuuVesselList": { + "valuesInThePeriod": [ + { + "from": "2020-01-01T00:00:00Z", + "to": "2024-03-01T00:00:00Z" + } + ], + "totalTimesListed": 1, + "totalTimesListedInThePeriod": 0 + }, + "mouList": { + "paris": { + "totalTimesListed": 3, + "totalTimesListedInThePeriod": 3, + "valuesInThePeriod": [ + { + "from": "2022-12-08T13:38:20.000Z", + "to": "2023-11-30T22:27:07.000Z", + "value": "grey" + } + ] + }, + "tokyo": { + "totalTimesListed": 3, + "totalTimesListedInThePeriod": 3, + "valuesInThePeriod": [ + { + "from": "2022-12-08T13:38:20.000Z", + "to": "2023-11-30T22:27:07.000Z", + "value": "black" + } + ] + } + } + } +} diff --git a/tests/testthat/fixtures/insights/vessel_insight_request_body.json b/tests/testthat/fixtures/insights/vessel_insight_request_body.json new file mode 100644 index 0000000..11ba11c --- /dev/null +++ b/tests/testthat/fixtures/insights/vessel_insight_request_body.json @@ -0,0 +1,17 @@ +{ + "includes": [ + "COVERAGE", + "FISHING", + "GAP", + "VESSEL-IDENTITY-FLAG-CHANGES", + "VESSEL-IDENTITY-IUU-VESSEL-LIST", + "VESSEL-IDENTITY-MOU-LIST" + ], + "start_date": "2020-01-01", + "end_date": "2025-03-03", + "vessels": [ + "785101812-2127-e5d2-e8bf-7152c5259f5f", + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb" + ] +} diff --git a/tests/testthat/helper-gfwr.R b/tests/testthat/helper-gfwr.R index 5ed117c..be7c92f 100644 --- a/tests/testthat/helper-gfwr.R +++ b/tests/testthat/helper-gfwr.R @@ -72,3 +72,27 @@ with_gfw_mocked_envvar <- function(code) { GFW_TOKEN = MOCK_GFW_TOKEN ), code) } + +#' Load a JSON fixture for testing +#' +#' @description +#' Reads a JSON file from `tests/testthat/fixtures/` and converts it +#' into an R object. This is useful for mocking API responses or +#' providing consistent test data. +#' +#' @param filename Name of the JSON file in the `fixtures` folder. +#' +#' @return An R list or data frame representing the JSON contents. +#' @keywords testing internal +#' +#' @examples +#' \dontrun{ +#' fixture <- with_json_fixture("sample.json") +#' fixture$id # access elements +#' fixture$name +#' } +with_gfw_json_fixture <- function(filename) { + fixture_path <- testthat::test_path("fixtures", filename) + fixture_data <- jsonlite::fromJSON(fixture_path, simplifyVector = FALSE) + return(fixture_data) # nolint: return_linter. +} diff --git a/tests/testthat/test-gfw_vessel_insights.R b/tests/testthat/test-gfw_vessel_insights.R new file mode 100644 index 0000000..6725996 --- /dev/null +++ b/tests/testthat/test-gfw_vessel_insights.R @@ -0,0 +1,256 @@ +# Validate includes ----------------------------------------------------------- + +test_that("gfw_vessel_insights: missing includes triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = NULL, + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "`includes` is required" + ) + }) +}) + +test_that("gfw_vessel_insights: invalid includes triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "INVALID-INCLUDE", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "Invalid `includes` value" + ) + }) +}) + + +# Validate dates ----------------------------------------------------------- + +test_that("gfw_vessel_insights: invalid start_date triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "must be in YYYY-MM-DD" + ) + }) +}) + +test_that("gfw_vessel_insights: invalid end_date triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "must be in YYYY-MM-DD" + ) + }) +}) + +test_that("gfw_vessel_insights: start_date > end_date triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2025-03-03", + end_date = "2020-01-01", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "must be less than or equal to `end_date`." + ) + }) +}) + + +# Validate vessels ------------------------------------------------------------ + +test_that("gfw_vessel_insights: missing vessels triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = NULL + ), + regexp = "`vessels` is required" + ) + }) +}) + +test_that("gfw_vessel_insights: non-character vessels triggers error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c(1, NA, NULL, list(), c()) + ), + regexp = "must be a non-empty character vector" + ) + }) +}) + +test_that("gfw_vessel_insights: empty or NA or NULL vessels trigger error", { + with_gfw_mocked_envvar({ + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c(" ", "", NA, NULL) + ), + regexp = "Invalid `vessels` value" + ) + }) +}) + +# Validate key ---------------------------------------------------------------- + +test_that("gfw_vessel_insights: NULL key triggers error", { + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f"), + key = NULL + ), + regexp = "No API token found" + ) +}) + +test_that("gfw_vessel_insights: NA key triggers error", { + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f"), + key = NA_character_ + ), + regexp = "No API token found" + ) +}) + +test_that("gfw_vessel_insights: empty string key triggers error", { + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f"), + key = "" + ), + regexp = "No API token found" + ) +}) + +test_that("gfw_vessel_insights: NA `GFW_TOKEN` envvar key triggers error", { + withr::with_envvar(c(GFW_TOKEN = NA_character_), { + expect_error( + gfw_vessel_insights( + includes = "FISHING", + start_date = "2020-01-01", + end_date = "2025-03-03", + vessels = c("785101812-2127-e5d2-e8bf-7152c5259f5f") + ), + regexp = "No API token found" + ) + }) +}) + + +# API request ----------------------------------------------------------------- + +test_that("gfw_vessel_insights: returns vessel insights for multiple insight types", { + with_gfw_mocked_envvar({ + mocked_url <- curl::curl_modify_url(gfw_base_url(), path = "insights/vessels") + + mocked_req_body <- with_gfw_json_fixture("insights/vessel_insight_request_body.json") + mocked_req_body$includes <- unlist(mocked_req_body$includes) + mocked_req_body$vessels <- unlist(mocked_req_body$vessels) + + mocked_resp_body <- with_gfw_json_fixture("insights/vessel_insight_item.json") + + mocked_resp <- function(req) { + httr2::response_json( + status_code = 200, + url = mocked_url, + body = mocked_resp_body + ) + } + + httr2::with_mocked_responses(mocked_resp, { + resp <- gfw_vessel_insights( + includes = mocked_req_body$includes, + start_date = mocked_req_body$start_date, + end_date = mocked_req_body$end_date, + vessels = mocked_req_body$vessels + ) + + expect_s3_class(resp, "tbl_df") + expect_equal(nrow(resp), 1) + expect_setequal(colnames(resp), names(mocked_resp_body)) + + expect_true(!is.null(resp$period)) + expect_identical(resp$period[[1]], mocked_resp_body$period) + expect_identical(resp$period[[1]]$startDate, mocked_resp_body$period$startDate) + expect_identical(resp$period[[1]]$endDate, mocked_resp_body$period$endDate) + + expect_true(!is.null(resp$vesselIdsWithoutIdentity)) + + expect_true(!is.null(resp$gap)) + expect_identical(resp$gap[[1]], mocked_resp_body$gap) + expect_identical(resp$gap[[1]]$datasets, mocked_resp_body$gap$datasets) + expect_identical(resp$gap[[1]]$historicalCounters, mocked_resp_body$gap$historicalCounters) + expect_identical(resp$gap[[1]]$periodSelectedCounters, mocked_resp_body$gap$periodSelectedCounters) + expect_identical(resp$gap[[1]]$aisOff, mocked_resp_body$gap$aisOff) + + expect_true(!is.null(resp$coverage)) + expect_identical(resp$coverage[[1]]$blocks, mocked_resp_body$coverage$blocks) + expect_identical(resp$coverage[[1]]$blocksWithPositions, mocked_resp_body$coverage$blocksWithPositions) + expect_true(resp$coverage[[1]]$percentage <= mocked_resp_body$coverage$percentage) + + expect_true(!is.null(resp$apparentFishing)) + expect_identical(resp$apparentFishing[[1]], mocked_resp_body$apparentFishing) + expect_identical(resp$apparentFishing[[1]]$datasets, mocked_resp_body$apparentFishing$datasets) + expect_identical( + resp$apparentFishing[[1]]$historicalCounters, + mocked_resp_body$apparentFishing$historicalCounters + ) + expect_identical( + resp$apparentFishing[[1]]$periodSelectedCounters, + mocked_resp_body$apparentFishing$periodSelectedCounters + ) + expect_identical( + resp$apparentFishing[[1]]$eventsInRfmoWithoutKnownAuthorization, + mocked_resp_body$apparentFishing$eventsInRfmoWithoutKnownAuthorization + ) + expect_identical( + resp$apparentFishing[[1]]$eventsInNoTakeMpas, + mocked_resp_body$apparentFishing$eventsInNoTakeMpas + ) + + expect_true(!is.null(resp$vesselIdentity)) + expect_identical(resp$vesselIdentity[[1]], mocked_resp_body$vesselIdentity) + expect_identical(resp$vesselIdentity[[1]]$datasets, mocked_resp_body$vesselIdentity$datasets) + expect_identical(resp$vesselIdentity[[1]]$flagsChanges, mocked_resp_body$vesselIdentity$flagsChanges) + expect_identical(resp$vesselIdentity[[1]]$iuuVesselList, mocked_resp_body$vesselIdentity$iuuVesselList) + expect_identical(resp$vesselIdentity[[1]]$mouList, mocked_resp_body$vesselIdentity$mouList) + }) + }) +})