From dd532da82e1fc668117e39a14d68aab8caba2c05 Mon Sep 17 00:00:00 2001 From: Ryan Stelly Date: Tue, 21 Oct 2025 14:36:57 -0500 Subject: [PATCH] MOB-948 add programmatic test user reset step to e2e --- e2e/helpers.js | 6 + e2e/sharedFlows/dismissAnnouncements.js | 20 ---- e2e/sharedFlows/resetUserForTesting.js | 142 ++++++++++++++++++++++++ e2e/sharedFlows/signIn.js | 5 - 4 files changed, 148 insertions(+), 25 deletions(-) delete mode 100644 e2e/sharedFlows/dismissAnnouncements.js create mode 100644 e2e/sharedFlows/resetUserForTesting.js diff --git a/e2e/helpers.js b/e2e/helpers.js index 4c0b51c7f..439ddc15c 100644 --- a/e2e/helpers.js +++ b/e2e/helpers.js @@ -1,6 +1,10 @@ import { exec, execSync } from "child_process"; +import resetUserForTesting from "./sharedFlows/resetUserForTesting"; + export async function iNatE2eBeforeAll( device ) { + await resetUserForTesting(); + if ( device.getPlatform() === "android" ) { await device.launchApp( { newInstance: true, @@ -109,6 +113,8 @@ export function terminateApp( deviceId, bundleId ) { } export async function iNatE2eAfterEach( device ) { + await resetUserForTesting(); + if ( device && device.getPlatform() === "android" ) { return; } diff --git a/e2e/sharedFlows/dismissAnnouncements.js b/e2e/sharedFlows/dismissAnnouncements.js deleted file mode 100644 index a05e247a5..000000000 --- a/e2e/sharedFlows/dismissAnnouncements.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - by, element, waitFor -} from "detox"; - -const TIMEOUT = 10_000; - -export default async function dismissAnnouncements() { - try { - // wait briefly to see if the announcement appears - await waitFor( element( by.id( "announcements-container" ) ) ) - .toBeVisible() - .withTimeout( TIMEOUT ); - - // if we get here, the announcement is visible, so dismiss it - await element( by.id( "announcements-dismiss" ) ).tap(); - } catch ( error ) { - // if timeout occurs, the element isn't visible, so continue with test - console.log( "No announcement present, continuing with test", error ); - } -} diff --git a/e2e/sharedFlows/resetUserForTesting.js b/e2e/sharedFlows/resetUserForTesting.js new file mode 100644 index 000000000..619d3174a --- /dev/null +++ b/e2e/sharedFlows/resetUserForTesting.js @@ -0,0 +1,142 @@ +import { create } from "apisauce"; +import inatjs from "inaturalistjs"; +import Config from "react-native-config-node"; +import * as uuid from "uuid"; + +const apiHost = Config.OAUTH_API_URL; + +const testUsernameAllowlist = [ + "inaturalist-test" +]; + +const userAgent = "iNaturalistRN/e2e"; +const installId = uuid.v4( ).toString( ); + +inatjs.setConfig( { + apiURL: Config.API_URL, + writeApiURL: Config.API_URL, + userAgent, + headers: { + "X-Installation-ID": installId + } +} ); + +// programatically dismisses announcements for user and deletes all existing observations +// in order to set up consistent testing conditions and remove need to wait for announcements +export default async function resetUserForTesting() { + console.log( + "Test user reset: dismissing announcements and deleting observations..." + ); + + if ( !testUsernameAllowlist.includes( Config.E2E_TEST_USERNAME ) ) { + const message = "This e2e test deletes observations of the user under test." + + "Add this username to the `testUsernameAllowlist` if that's really what you want."; + throw new Error( message ); + } + + const apiClient = create( { + baseURL: apiHost, + headers: { + "User-Agent": userAgent, + "X-Installation-ID": installId + } + } ); + + await apiClient.get( "/logout" ); + + const formData = { + format: "json", + grant_type: "password", + client_id: Config.OAUTH_CLIENT_ID, + client_secret: Config.OAUTH_CLIENT_SECRET, + username: Config.E2E_TEST_USERNAME, + password: Config.E2E_TEST_PASSWORD, + locale: "en" + }; + + const tokenResponse = await apiClient.post( "/oauth/token", formData ); + const accessToken = tokenResponse.data.access_token; + + apiClient.setHeader( "Authorization", `Bearer ${accessToken}` ); + + const jwtResponse = await apiClient.get( "/users/api_token.json" ); + + const opts = { + api_token: jwtResponse.data.api_token + }; + + const announcementSearchParams = { + placement: "mobile", + locale: "en-US", + per_page: 20, + fields: { + id: true, + body: true, + dismissible: true, + start: true, + placement: true + } + }; + + const announcementResponse = await inatjs.announcements.search( + announcementSearchParams, + opts + ); + + const announcementIdsToDismiss = announcementResponse + .results + .filter( a => a.dismissable ) + .map( a => a.id ); + + console.log( `Dismissing ${announcementIdsToDismiss.length} announcements` ); + + await Promise.all( announcementIdsToDismiss.map( async id => { + await inatjs.announcements.dismiss( + { id }, + opts + ); + } ) ); + + const usersEditResponse = await apiClient.get( + "/users/edit.json", + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "User-Agent": userAgent + } + } + ); + + const userId = usersEditResponse.data.id; + + const observationResponse = await inatjs.observations.search( { user_id: userId }, opts ); + + // is this _really_ an e2e test user if they have more than expected observations? + const suspiciousObservationThreshold = 10; + if ( typeof observationResponse.total_results !== "number" + || observationResponse.total_results > suspiciousObservationThreshold ) { + const message + = `More than ${suspiciousObservationThreshold} observations found for test user. Aborting.`; + throw new Error( message ); + } + + const observationIdsToDelete = observationResponse.results.map( a => a.uuid ); + + console.log( `Deleting ${observationIdsToDelete.length} observations` ); + + await Promise.all( observationIdsToDelete.map( async uuid => { + console.log( { uuid } ); + const deleteResponse = await inatjs.observations.delete( + { uuid }, + opts + ); + console.log( { uuid, deleteResponse, stringify: JSON.stringify( deleteResponse ) } ); + } ) ); + + await apiClient.get( "/logout" ); + + console.log( + "Test user reset: announcements dismissed and observations deleted" + ); +} diff --git a/e2e/sharedFlows/signIn.js b/e2e/sharedFlows/signIn.js index 0ae767c23..0158ecaf7 100644 --- a/e2e/sharedFlows/signIn.js +++ b/e2e/sharedFlows/signIn.js @@ -3,7 +3,6 @@ import { } from "detox"; import Config from "react-native-config-node"; -import dismissAnnouncements from "./dismissAnnouncements"; import switchPowerMode from "./switchPowerMode"; const TIMEOUT = 10_000; @@ -42,9 +41,5 @@ export default async function signIn() { await waitFor( username ).toBeVisible().withTimeout( TIMEOUT ); await expect( username ).toBeVisible(); - /* - Dismiss announcements if they're blocking the UI - */ - await dismissAnnouncements(); return username; }