diff --git a/test/controllers/observations/inat_imports_controller_test.rb b/test/controllers/observations/inat_imports_controller_test.rb index 396f6a7bfc..3939490139 100644 --- a/test/controllers/observations/inat_imports_controller_test.rb +++ b/test/controllers/observations/inat_imports_controller_test.rb @@ -135,7 +135,6 @@ def test_import_authorized inat_import.save inat_authorization_callback_params = { code: "MockCode" } - # stub_token_requests login(user.login) assert_difference( @@ -150,253 +149,8 @@ def test_import_authorized assert_redirected_to(observations_path) end - def test_import_lycoperdon - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - obs = import_mock_observation("lycoperdon") - - assert(obs.images.any?, "Obs should have images") - assert(obs.sequences.one?, "Obs should have a sequence") - end - - # Prove that Namings, Votes, Identification are correct - # When iNat obs has provisional name that's in MO - def test_import_arrhenia_sp_ny02_old_name - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - name = Name.create( - text_name: 'Arrhenia "sp-NY02"', - author: "S.D. Russell crypt. temp.", - display_name: '**__Arrhenia "sp-NY02"__** S.D. Russell crypt. temp.', - rank: "Species", - user: rolf - ) - - obs = import_mock_observation("arrhenia_sp_NY02") - - namings = obs.namings - naming = namings.find_by(name: name) - assert(naming.present?, "Missing Naming for provisional name") - assert_equal(inat_manager, naming.user, - "Naming without iNat ID should have user: inat_manager") - vote = Vote.find_by(naming: naming, user: naming.user) - assert(vote.present?, "Naming is missing a Vote") - assert_equal(name, obs.name, "Consensus ID should be provisional name") - assert(vote.value.positive?, "Vote for MO consensus should be positive") - end - - # Prove that Namings, Votes, Identification are correct - # when iNat obs has provisional name that wasn't in MO - def test_import_arrhenia_sp_ny02_new_name - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - assert_nil(Name.find_by(text_name: 'Arrhenia "sp-NY02"'), - "Test requires that MO not yest have provisional name") - - obs = import_mock_observation("arrhenia_sp_NY02") - - name = Name.find_by(text_name: 'Arrhenia "sp-NY02"') - assert(name.rss_log_id.present?) - - assert(name.present?, "Failed to create provisional name") - namings = obs.namings - naming = namings.find_by(name: name) - assert(naming.present?, "Missing Naming for provisional name") - assert_equal(inat_manager, naming.user, - "Naming without iNat ID should have user: inat_manager") - vote = Vote.find_by(naming: naming, user: naming.user) - assert(vote.present?, "Naming is missing a Vote") - assert_equal(name, obs.name, "Consensus ID should be provisional name") - assert(vote.value.positive?, "Vote for MO consensus should be positive") - end - - def test_import_plant - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - user = rolf - filename = "ceanothus_cordulatus" - mock_search_result = File.read("test/inat/#{filename}.txt") - inat_import_ids = InatObs.new( - JSON.generate(JSON.parse(mock_search_result)["results"].first) - ).inat_id - - simulate_all_inat_interactions(user: user, - inat_import_ids: inat_import_ids, - mock_inat_response: mock_search_result) - - params = { inat_ids: inat_import_ids, code: "MockCode" } - login(user.login) - - assert_no_difference( - "Observation.count", "Should not import iNat Plant observations" - ) do - post(:authorization_response, params: params) - end - - assert_flash_text(:inat_taxon_not_importable.l(id: inat_import_ids)) - end - - def test_import_zero_results - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - user = rolf - filename = "zero_results" - mock_search_result = File.read("test/inat/#{filename}.txt") - inat_import_ids = "123" - - simulate_all_inat_interactions( - user: user, inat_import_ids: inat_import_ids, - mock_inat_response: mock_search_result - ) - - params = { inat_ids: inat_import_ids, code: "MockCode" } - login(user.login) - - assert_no_difference( - "Observation.count", - "Should not import if there's no iNat obs with a matching id" - ) do - post(:authorization_response, params: params) - end - end - - def test_import_multiple - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - # NOTE: using obss without photos to avoid stubbing photo import - # amanita_flavorubens, calostoma lutescens - inat_obss = "231104466,195434438" - inat_import_ids = inat_obss - user = users(:rolf) - filename = "listed_ids" - mock_inat_response = File.read("test/inat/#{filename}.txt") - # prove that mock was constructed properly - json = JSON.parse(mock_inat_response) - assert_equal(2, json["total_results"]) - assert_equal(1, json["page"]) - assert_equal(30, json["per_page"]) - # mock is sorted by id, asc - assert_equal(195_434_438, json["results"].first["id"]) - assert_equal(231_104_466, json["results"].second["id"]) - - simulate_all_inat_interactions( - user: user, inat_import_ids: inat_import_ids, - mock_inat_response: mock_inat_response - ) - - params = { inat_ids: inat_import_ids, code: "MockCode" } - login(user.login) - - assert_difference( - "Observation.count", 2, "Failed to create multiple observations" - ) do - post(:authorization_response, params: params) - end - end - - def test_import_all - # TODO: Move to InatImportJobTest - skip("To be moved to InatImportJobTest") - user = users(:rolf) - login(user.login) - - filename = "import_all" - mock_search_result = File.read("test/inat/#{filename}.txt") - # shorten it to one page to avoid stubbing multiple inat api requests - mock_search_result = limited_to_first_page(mock_search_result) - # delete the photos in order to avoid stubbing photo imports - mock_search_result = result_without_photos(mock_search_result) - - inat_import_ids = "" - - simulate_all_inat_interactions( - user: user, inat_import_ids: inat_import_ids, - mock_inat_response: mock_search_result, - import_all: true - ) - - params = { inat_ids: inat_import_ids, code: "MockCode" } - - assert_difference( - "Observation.count", 2, "Failed to create multiple observations" - ) do - post(:authorization_response, params: params) - end - end - ########## Utilities - def inat_manager - User.find_by(login: "MO Webmaster") - end - - def import_mock_observation(filename) - user = users(:rolf) - mock_search_result = File.read("test/inat/#{filename}.txt") - inat_obs = InatObs.new( - JSON.generate( - JSON.parse(mock_search_result)["results"].first - ) - ) - inat_import_ids = inat_obs.inat_id - - simulate_all_inat_interactions( - user: user, inat_username: inat_obs.inat_user_login, - inat_import_ids: inat_import_ids, - mock_inat_response: mock_search_result - ) - - params = { inat_ids: inat_import_ids, code: "MockCode" } - login(user.login) - - # NOTE: Stubs the importer's return value, but not its side-effect -- - # i.e., doesn't add Image(s) to the MO Observation. - # Enables testing everything except Observation.images. jdc 2024-06-23 - InatPhotoImporter.stub(:new, mock_photo_importer(inat_obs)) do - assert_difference("Observation.count", 1, "Failed to create Obs") do - post(:authorization_response, params: params) - end - end - - Observation.order(created_at: :asc).last - end - - def simulate_all_inat_interactions( - mock_inat_response:, user: rolf, inat_username: nil, inat_import_ids: "", - import_all: false, id_above: 0 - ) - simulate_inat_accredications( - user: user, inat_username: inat_username, - inat_import_ids: inat_import_ids, import_all: import_all - ) - stub_inat_api_request(inat_import_ids, mock_inat_response, - id_above: id_above, - inat_user_login: inat_username) - end - - def simulate_inat_accredications( - user: rolf, inat_username: nil, inat_import_ids: "", import_all: false - ) - simulate_authorization( - user: user, inat_username: inat_username, - inat_import_ids: inat_import_ids, import_all: import_all - ) - stub_token_requests - end - - def simulate_authorization( - user: rolf, inat_username: nil, inat_import_ids: "", import_all: false - ) - inat_import = InatImport.find_or_create_by(user: user) - inat_import.import_all = import_all - # ignore list of ids if importing all a user's iNat obss - inat_import.inat_ids = import_all == true ? "" : inat_import_ids - inat_import.state = "Authorizing" - inat_import.inat_username = inat_username - inat_import.save - stub_request(:any, authorization_url) - end - # iNat url where user is sent in order to authorize MO access # to iNat confidential data # https://www.inaturalist.org/pages/api+reference#authorization_code_flow @@ -406,115 +160,5 @@ def authorization_url "&redirect_uri=#{REDIRECT_URI}" \ "&response_type=code" end - - def stub_token_requests - stub_oauth_token_request - # must trade oauth access token for a JWT in order to use iNat API v1 - stub_jwt_request - end - - # stub exchanging iNat code for oauth token - # https://www.inaturalist.org/pages/api+reference#authorization_code_flow - def stub_oauth_token_request - stub_request(:post, "#{SITE}/oauth/token"). - with( - body: { "client_id" => Rails.application.credentials.inat.id, - "client_secret" => Rails.application.credentials.inat.secret, - "code" => "MockCode", - "grant_type" => "authorization_code", - "redirect_uri" => REDIRECT_URI } - ). - to_return(status: 200, - body: { access_token: "MockAccessToken" }.to_json, - headers: {}) - end - - def stub_jwt_request - stub_request(:get, "#{SITE}/users/api_token"). - with( - headers: { - "Accept" => "application/json", - "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", - "Authorization" => "Bearer MockAccessToken", - "Host" => "www.inaturalist.org" - } - ). - to_return(status: 200, - body: { access_token: "MockJWT" }.to_json, - headers: {}) - end - - def stub_inat_api_request(inat_obs_ids, mock_inat_response, id_above: 0, - inat_user_login: nil) - # params must be in same order as in the controller - # omit trailing "=" since the controller omits it (via `merge`) - params = <<~PARAMS.delete("\n").chomp("=") - ?iconic_taxa=#{Observations::InatImportsController::ICONIC_TAXA} - &id=#{inat_obs_ids} - &id_above=#{id_above} - &per_page=200 - &only_id=false - &order=asc&order_by=id - &user_login=#{inat_user_login} - PARAMS - stub_request(:get, "#{API_BASE}/observations#{params}"). - with(headers: - { "Accept" => "application/json", - "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", - "Authorization" => "Bearer", - "Host" => "api.inaturalist.org" }). - to_return(body: mock_inat_response) - end - - def mock_photo_importer(inat_obs) - mock_inat_photo = inat_obs.inat_obs_photos.first - mock_image_api = MockImageAPI.new( - results: [ - expected_mo_image(mock_inat_photo: mock_inat_photo, - user: User.current) - ] - ) - mock_photo_importer = Minitest::Mock.new - inat_obs.inat_obs_photos.length.times do - mock_photo_importer.expect(:api, mock_image_api) - end - mock_photo_importer - end - - def expected_mo_image(mock_inat_photo:, user:) - Image.create( - content_type: "image/jpeg", - user_id: user.id, - notes: "Imported from iNat " \ - "#{DateTime.now.utc.strftime("%Y-%m-%d %H:%M:%S %z")}", - copyright_holder: mock_inat_photo[:photo][:attribution], - license_id: expected_mo_photo_license(mock_inat_photo), - width: 2048, - height: 1534, - original_name: "iNat photo uuid #{mock_inat_photo[:uuid]}" - ) - end - - def expected_mo_photo_license(mock_inat_photo) - InatLicense.new(mock_inat_photo[:photo][:license_code]). - mo_license.id - end - - def result_without_photos(mock_search_result) - ms_hash = JSON.parse(mock_search_result) - ms_hash["results"].each do |result| - result["observation_photos"] = [] - result["photos"] = [] - end - JSON.generate(ms_hash) - end - - # Turn results with many pages into results with one page - # By ignoring all pages but the first - def limited_to_first_page(mock_search_result) - ms_hash = JSON.parse(mock_search_result) - ms_hash["total_results"] = ms_hash["results"].length - JSON.generate(ms_hash) - end end end diff --git a/test/jobs/inat_import_job_test.rb b/test/jobs/inat_import_job_test.rb index 109103091f..0e7a570dd9 100644 --- a/test/jobs/inat_import_job_test.rb +++ b/test/jobs/inat_import_job_test.rb @@ -149,6 +149,180 @@ def test_import_job_obs_with_many_namings assert(obs.sequences.none?) end + def test_import_lycoperdon + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + obs = import_mock_observation("lycoperdon") + + assert(obs.images.any?, "Obs should have images") + assert(obs.sequences.one?, "Obs should have a sequence") + end + + # Prove that Namings, Votes, Identification are correct + # When iNat obs has provisional name that's in MO + def test_import_arrhenia_sp_ny02_old_name + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + name = Name.create( + text_name: 'Arrhenia "sp-NY02"', + author: "S.D. Russell crypt. temp.", + display_name: '**__Arrhenia "sp-NY02"__** S.D. Russell crypt. temp.', + rank: "Species", + user: rolf + ) + + obs = import_mock_observation("arrhenia_sp_NY02") + + namings = obs.namings + naming = namings.find_by(name: name) + assert(naming.present?, "Missing Naming for provisional name") + assert_equal(inat_manager, naming.user, + "Naming without iNat ID should have user: inat_manager") + vote = Vote.find_by(naming: naming, user: naming.user) + assert(vote.present?, "Naming is missing a Vote") + assert_equal(name, obs.name, "Consensus ID should be provisional name") + assert(vote.value.positive?, "Vote for MO consensus should be positive") + end + + # Prove that Namings, Votes, Identification are correct + # when iNat obs has provisional name that wasn't in MO + def test_import_arrhenia_sp_ny02_new_name + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + assert_nil(Name.find_by(text_name: 'Arrhenia "sp-NY02"'), + "Test requires that MO not yest have provisional name") + + obs = import_mock_observation("arrhenia_sp_NY02") + + name = Name.find_by(text_name: 'Arrhenia "sp-NY02"') + assert(name.rss_log_id.present?) + + assert(name.present?, "Failed to create provisional name") + namings = obs.namings + naming = namings.find_by(name: name) + assert(naming.present?, "Missing Naming for provisional name") + assert_equal(inat_manager, naming.user, + "Naming without iNat ID should have user: inat_manager") + vote = Vote.find_by(naming: naming, user: naming.user) + assert(vote.present?, "Naming is missing a Vote") + assert_equal(name, obs.name, "Consensus ID should be provisional name") + assert(vote.value.positive?, "Vote for MO consensus should be positive") + end + + def test_import_plant + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + user = rolf + filename = "ceanothus_cordulatus" + mock_search_result = File.read("test/inat/#{filename}.txt") + inat_import_ids = InatObs.new( + JSON.generate(JSON.parse(mock_search_result)["results"].first) + ).inat_id + + simulate_all_inat_interactions(user: user, + inat_import_ids: inat_import_ids, + mock_inat_response: mock_search_result) + + params = { inat_ids: inat_import_ids, code: "MockCode" } + login(user.login) + + assert_no_difference( + "Observation.count", "Should not import iNat Plant observations" + ) do + post(:authorization_response, params: params) + end + + assert_flash_text(:inat_taxon_not_importable.l(id: inat_import_ids)) + end + + def test_import_zero_results + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + user = rolf + filename = "zero_results" + mock_search_result = File.read("test/inat/#{filename}.txt") + inat_import_ids = "123" + + simulate_all_inat_interactions( + user: user, inat_import_ids: inat_import_ids, + mock_inat_response: mock_search_result + ) + + params = { inat_ids: inat_import_ids, code: "MockCode" } + login(user.login) + + assert_no_difference( + "Observation.count", + "Should not import if there's no iNat obs with a matching id" + ) do + post(:authorization_response, params: params) + end + end + + def test_import_multiple + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + # NOTE: using obss without photos to avoid stubbing photo import + # amanita_flavorubens, calostoma lutescens + inat_obss = "231104466,195434438" + inat_import_ids = inat_obss + user = users(:rolf) + filename = "listed_ids" + mock_inat_response = File.read("test/inat/#{filename}.txt") + # prove that mock was constructed properly + json = JSON.parse(mock_inat_response) + assert_equal(2, json["total_results"]) + assert_equal(1, json["page"]) + assert_equal(30, json["per_page"]) + # mock is sorted by id, asc + assert_equal(195_434_438, json["results"].first["id"]) + assert_equal(231_104_466, json["results"].second["id"]) + + simulate_all_inat_interactions( + user: user, inat_import_ids: inat_import_ids, + mock_inat_response: mock_inat_response + ) + + params = { inat_ids: inat_import_ids, code: "MockCode" } + login(user.login) + + assert_difference( + "Observation.count", 2, "Failed to create multiple observations" + ) do + post(:authorization_response, params: params) + end + end + + def test_import_all + # TODO: Move to InatImportJobTest + skip("To be moved to InatImportJobTest") + user = users(:rolf) + login(user.login) + + filename = "import_all" + mock_search_result = File.read("test/inat/#{filename}.txt") + # shorten it to one page to avoid stubbing multiple inat api requests + mock_search_result = limited_to_first_page(mock_search_result) + # delete the photos in order to avoid stubbing photo imports + mock_search_result = result_without_photos(mock_search_result) + + inat_import_ids = "" + + simulate_all_inat_interactions( + user: user, inat_import_ids: inat_import_ids, + mock_inat_response: mock_search_result, + import_all: true + ) + + params = { inat_ids: inat_import_ids, code: "MockCode" } + + assert_difference( + "Observation.count", 2, "Failed to create multiple observations" + ) do + post(:authorization_response, params: params) + end + end + ########## Utilities # The InatImport object which is created in InatImportController#create