Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ val kartaViewAccessToken: String = System.getenv("KARTAVIEW_ACCESS_TOKEN")
}
?: ""

/** Bearer token for photo uploads to the dev Panoramax instance (DEV environment): from env var, else root secrets.properties
* (git-ignored), else empty (build succeeds but Panoramax photo uploads fail auth at runtime).
* Never commit the token. */
val panoramaxDevAccessToken: String = System.getenv("PANORAMAX_DEV_ACCESS_TOKEN")
?: rootProject.file("secrets.properties").takeIf { it.exists() }?.let { file ->
Properties().apply { FileInputStream(file).use { load(it) } }.getProperty("panoramaxDevAccessToken")
}
?: ""

/** Same, for the stage Panoramax instance (used in the STAGE environment) */
val panoramaxStageAccessToken: String = System.getenv("PANORAMAX_STAGE_ACCESS_TOKEN")
?: rootProject.file("secrets.properties").takeIf { it.exists() }?.let { file ->
Properties().apply { FileInputStream(file).use { load(it) } }.getProperty("panoramaxStageAccessToken")
}
?: ""

/** Same, for the prod Panoramax instance (used in the PROD environment) */
val panoramaxProdAccessToken: String = System.getenv("PANORAMAX_PROD_ACCESS_TOKEN")
?: rootProject.file("secrets.properties").takeIf { it.exists() }?.let { file ->
Properties().apply { FileInputStream(file).use { load(it) } }.getProperty("panoramaxProdAccessToken")
}
?: ""

buildkonfig {
packageName = "de.westnordost.streetcomplete"
objectName = "BuildConfig"
Expand All @@ -71,6 +94,9 @@ buildkonfig {
buildConfigField(STRING, "VERSION_NAME", appVersionName)
buildConfigField(BOOLEAN, "DEBUG", "true")
buildConfigField(STRING, "KARTAVIEW_ACCESS_TOKEN", kartaViewAccessToken)
buildConfigField(STRING, "PANORAMAX_DEV_ACCESS_TOKEN", panoramaxDevAccessToken)
buildConfigField(STRING, "PANORAMAX_STAGE_ACCESS_TOKEN", panoramaxStageAccessToken)
buildConfigField(STRING, "PANORAMAX_PROD_ACCESS_TOKEN", panoramaxProdAccessToken)
}

targetConfigs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class NoteEditsDaoTest : ApplicationDbTestCase() {
listOf(
Trackpoint(LatLon(3.0, 4.0), 1234L, 1f, 2f),
Trackpoint(LatLon(1.0, 5.0), 12345L, 2f, 3f),
)
),
imageBearings = listOf(90f, null, 359f),
imageCaptureTimes = listOf(100L, 200L, null),
)
dao.add(edit)
assertEquals(edit, dao.get(1L))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import de.westnordost.streetcomplete.data.osm.edits.create_feature.FeaturePhoto
import de.westnordost.streetcomplete.data.osm.edits.create_feature.FeaturePhotosController
import de.westnordost.streetcomplete.quests.sidewalk_long_form.data.KARTAVIEW_URL_TAG
import de.westnordost.streetcomplete.quests.sidewalk_long_form.data.STREET_IMAGES_TAG
import org.hamcrest.Matchers.allOf
import java.io.File
import android.os.ParcelFileDescriptor
Expand Down Expand Up @@ -381,11 +382,14 @@ class LongFormFormTest {
val photos = waitForPhotos(firstEditId)
assertEquals(1, photos.size)
assertTrue(File(photos.single().path).exists())
// stored with the question that asked for it - sent along to Panoramax on upload
assertEquals(OBSTRUCTION_TYPE_Q, photos.single().question)
assertEquals(105, photos.single().questId)
}

@Test
fun existingPhotoCanBeMarkedForRemovalUndoneAndRemoved() {
open(mapOf("ext:obstruction" to "yes", "ext:obstruction:type" to "other", KARTAVIEW_URL_TAG to PHOTO_URL))
open(mapOf("ext:obstruction" to "yes", "ext:obstruction:type" to "other", STREET_IMAGES_TAG to PHOTO_URL))
onView(inRowOf(OBSTRUCTION_TYPE_Q, R.id.photo_title)).perform(scrollIntoView()).check(matches(withText("Photo from last visit")))

onView(inRowOf(OBSTRUCTION_TYPE_Q, R.id.photo_delete)).perform(scrollIntoView(), click())
Expand All @@ -395,7 +399,7 @@ class LongFormFormTest {

onView(inRowOf(OBSTRUCTION_TYPE_Q, R.id.photo_delete)).perform(scrollIntoView(), click())
submit()
assertFalse(KARTAVIEW_URL_TAG in edits.resultingTags(originalTags))
assertFalse(STREET_IMAGES_TAG in edits.resultingTags(originalTags))
}

@Test
Expand Down Expand Up @@ -451,7 +455,7 @@ class LongFormFormTest {
assertEquals(capturedUris[1].lastPathSegment, File(photo.path).name)
}

// long form photos go to KartaView, often over mobile data - a camera's full size photo is cut down
// long form photos go to Panoramax, often over mobile data - a camera's full size photo is cut down
@Test
fun capturedPhotoIsScaledDownForUpload() {
open(mapOf("ext:obstruction" to "yes", "ext:obstruction:type" to "bollard;other"))
Expand All @@ -477,7 +481,7 @@ class LongFormFormTest {

@Test
fun tappingThePhotoFromLastVisitShowsItFullScreen() {
open(mapOf("ext:obstruction" to "yes", "ext:obstruction:type" to "other", KARTAVIEW_URL_TAG to PHOTO_URL))
open(mapOf("ext:obstruction" to "yes", "ext:obstruction:type" to "other", STREET_IMAGES_TAG to PHOTO_URL))
intending(hasAction(Intent.ACTION_VIEW)).respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))

onView(inRowOf(OBSTRUCTION_TYPE_Q, R.id.photo_thumb)).perform(scrollIntoView(), click())
Expand Down Expand Up @@ -686,7 +690,7 @@ class LongFormFormTest {
private companion object {
val ANIMATION_SCALES = listOf("animator_duration_scale", "transition_animation_scale", "window_animation_scale")
const val QUEST_TYPE_NAME = "LongFormUiTestSidewalks"
const val PHOTO_URL = "https://kartaview.org/details/1/2/track-info"
const val PHOTO_URL = "https://panoramax.example/api/pictures/1/hd.jpg"
const val EDIT_TIMEOUT_MS = 5_000L
const val NO_EDIT_GRACE_MS = 1_000L
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import de.westnordost.streetcomplete.screens.user.UserActivity
import de.westnordost.streetcomplete.screens.workspaces.FakeWorkspaceRepository
import de.westnordost.streetcomplete.screens.workspaces.WorkSpaceActivity
import de.westnordost.streetcomplete.testutils.A11yScanner
import de.westnordost.streetcomplete.testutils.MockKartaView
import de.westnordost.streetcomplete.testutils.MockPanoramax
import de.westnordost.streetcomplete.testutils.MockOsmServer
import de.westnordost.streetcomplete.testutils.MockOsmServer.Failure
import io.ktor.http.HttpStatusCode
Expand Down Expand Up @@ -247,7 +247,7 @@ class AccessibilityScanTest : MapSyncTestBase() {

@Test
fun longFormPhotoFromLastVisit() {
MockOsmServer.put(SIDEWALK.copy(tags = SIDEWALK.tags + mapOf("obstacle" to "other", "ext:kartaview_url" to "https://storage.kartaview.org/files/photo/lth/x.jpg")))
MockOsmServer.put(SIDEWALK.copy(tags = SIDEWALK.tags + mapOf("obstacle" to "other", "ext:street_images" to MockPanoramax.PHOTO_URL)))
openWorkspaceAndSidewalkQuest()
expandSheet()
onView(inRowOf(OBSTRUCTION_Q, R.id.photo_title)).perform(scrollIntoView())
Expand Down Expand Up @@ -304,15 +304,15 @@ class AccessibilityScanTest : MapSyncTestBase() {
fun stuckPhotoNotice() {
Intents.init()
try {
MockKartaView.failure = MockKartaView.Failure.Status(HttpStatusCode.ServiceUnavailable)
MockPanoramax.failure = MockPanoramax.Failure.Status(HttpStatusCode.ServiceUnavailable)
openWorkspaceAndSidewalkQuest()
tapTile(OBSTRUCTION_Q, "Other obstruction")
capturePhoto()
submit()
val uploads: UploadProgressSource = koin.get()
for (run in 1..3) {
if (run > 1) tapUploadButton()
waitUntil("photo upload attempt $run") { MockKartaView.requests.size >= run && !uploads.isUploadInProgress }
waitUntil("photo upload attempt $run") { MockPanoramax.requests.size >= run && !uploads.isUploadInProgress }
}
waitForText("Photo upload failed")
scan("Photo upload failed notice")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import de.westnordost.streetcomplete.data.quest.VisibleQuestsSource
import de.westnordost.streetcomplete.quests.sidewalk_long_form.inRowOf
import de.westnordost.streetcomplete.quests.sidewalk_long_form.scrollIntoView
import de.westnordost.streetcomplete.testutils.MockOsmServer
import de.westnordost.streetcomplete.testutils.MockPanoramax
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
Expand Down Expand Up @@ -40,7 +41,7 @@ class MapSyncAnswersTest : MapSyncTestBase() {

@Test
fun removingThePhotoFromTheLastVisit_removesItsTagOnOsm() {
val tags = SIDEWALK.tags + mapOf("obstacle" to "other", "ext:kartaview_url" to OLD_PHOTO_URL)
val tags = SIDEWALK.tags + mapOf("obstacle" to "other", "ext:street_images" to OLD_PHOTO_URL)
MockOsmServer.put(SIDEWALK.copy(tags = tags))
openWorkspaceAndSidewalkQuest()

Expand All @@ -51,7 +52,7 @@ class MapSyncAnswersTest : MapSyncTestBase() {
submit()

val way = awaitUploads(1).single().modifiedWays.single()
assertEquals(tags - "ext:kartaview_url", way.tags)
assertEquals(tags - "ext:street_images", way.tags)
}

// last edited long ago (the mock's default: 2025-01-01) - due for a recheck
Expand Down Expand Up @@ -79,7 +80,7 @@ class MapSyncAnswersTest : MapSyncTestBase() {
}

private companion object {
const val OLD_PHOTO_URL = "https://storage.kartaview.org/files/photo/lth/from-last-visit.jpg"
val OLD_PHOTO_URL get() = "${MockPanoramax.PHOTO_URL}?from-last-visit"
val FULLY_ANSWERED = SIDEWALK.tags + mapOf(
"surface" to "asphalt", "width" to "60", "obstacle" to "bollard", "note" to "all good",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import de.westnordost.streetcomplete.data.osm.edits.ElementEditsController
import de.westnordost.streetcomplete.data.osm.edits.create_feature.FeaturePhotosController
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.upload.UploadProgressSource
import de.westnordost.streetcomplete.testutils.MockKartaView
import de.westnordost.streetcomplete.testutils.MockPanoramax
import de.westnordost.streetcomplete.testutils.MockOsmServer
import de.westnordost.streetcomplete.testutils.MockOsmServer.MockNode
import io.ktor.http.HttpStatusCode
Expand Down Expand Up @@ -69,31 +69,31 @@ class MapSyncMultiSelectTest : MapSyncTestBase() {
onView(withId(R.id.yesButton)).perform(click())
waitUntil("long form opened") { isDisplayed(SURFACE_Q) }

// KartaView down at first, so the photos stay around to be looked at
MockKartaView.failure = MockKartaView.Failure.Status(HttpStatusCode.ServiceUnavailable)
// Panoramax down at first, so the photos stay around to be looked at
MockPanoramax.failure = MockPanoramax.Failure.Status(HttpStatusCode.ServiceUnavailable)
tapTile(SURFACE_Q, "Concrete")
tapTile(OBSTRUCTION_Q, "Other obstruction")
capturePhoto()
submit()

val uploads: UploadProgressSource = koin.get()
waitUntil("photo upload attempted") { MockKartaView.requests.isNotEmpty() && !uploads.isUploadInProgress }
waitUntil("photo upload attempted") { MockPanoramax.requests.isNotEmpty() && !uploads.isUploadInProgress }
val edits = koin.get<ElementEditsController>().getAllUnsynced()
assertEquals("one edit per selected sidewalk", 2, edits.size)
val photos = edits.map { koin.get<FeaturePhotosController>().get(it.id).single() }
assertEquals("each edit owns its own photo file", 2, photos.map { it.path }.toSet().size)
assertTrue(photos.all { File(it.path).exists() })

// KartaView back - both go up, each carrying its photo's URL
MockKartaView.failure = null
// Panoramax back - both go up, each carrying its photo's URL
MockPanoramax.failure = null
tapUploadButton()
waitUntil("both sidewalks uploaded") {
MockOsmServer.snapshot().uploads.flatMap { it.modifiedWays }.map { it.id }.toSet() == setOf(SIDEWALK.id, SECOND_SIDEWALK_ID)
}
for (way in MockOsmServer.snapshot().uploads.flatMap { it.modifiedWays }) {
assertEquals(
"way ${way.id}",
SIDEWALK.tags + mapOf("surface" to "concrete", "obstacle" to "other", "ext:kartaview_url" to MockKartaView.PHOTO_URL),
SIDEWALK.tags + mapOf("surface" to "concrete", "obstacle" to "other", "ext:street_images" to MockPanoramax.PHOTO_URL),
way.tags
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import de.westnordost.streetcomplete.data.osm.edits.create_feature.FeaturePhotos
import de.westnordost.streetcomplete.data.upload.UploadProgressSource
import de.westnordost.streetcomplete.quests.sidewalk_long_form.inRowOf
import de.westnordost.streetcomplete.quests.sidewalk_long_form.scrollIntoView
import de.westnordost.streetcomplete.testutils.MockKartaView
import de.westnordost.streetcomplete.testutils.MockPanoramax
import de.westnordost.streetcomplete.testutils.MockOsmServer
import io.ktor.http.HttpStatusCode
import org.junit.After
Expand All @@ -35,9 +35,9 @@ import org.junit.Test
import org.junit.runner.RunWith

/**
* A long-form answer with a photo, when the photo can't be uploaded to KartaView: after three
* A long-form answer with a photo, when the photo can't be uploaded to Panoramax: after three
* failed upload runs the user is told and can remove the photo (the answer then uploads without
* it) or keep trying. KartaView is [MockKartaView], hooked into the app's own client - no photo
* it) or keep trying. Panoramax is [MockPanoramax], hooked into the app's own client - no photo
* ever reaches the real service. See [MapSyncTestBase] for the rest of the setup.
*/
@RunWith(AndroidJUnit4::class)
Expand All @@ -53,23 +53,34 @@ class MapSyncPhotoTest : MapSyncTestBase() {
Intents.release()
}

// the normal case: KartaView takes the photo, and the answer goes up carrying its public URL
// the normal case: Panoramax takes the photo, and the answer goes up carrying its public URL
@Test
fun photoUploads_andTheAnswerCarriesItsUrl() {
MockKartaView.failure = null
MockPanoramax.failure = null
openWorkspaceAndSidewalkQuest()
answerWithPhoto()

val way = awaitUploads(1).single().modifiedWays.single()
assertEquals(
SIDEWALK.tags + mapOf("surface" to "concrete", "obstacle" to "other", "ext:kartaview_url" to MockKartaView.PHOTO_URL),
SIDEWALK.tags + mapOf("surface" to "concrete", "obstacle" to "other", "ext:street_images" to MockPanoramax.PHOTO_URL),
way.tags
)
// uploaded the way the real API expects it, before the answer
assertEquals(
listOf("POST /1.0/sequence/", "POST /1.0/photo/", "POST /1.0/sequence/finished-uploading/", "GET /2.0/photo/mock-photo-1"),
MockKartaView.requests.toList()
listOf(
"POST /api/upload_sets",
"POST /api/upload_sets/mock-upload-set-1/files",
"POST /api/upload_sets/mock-upload-set-1/complete",
),
MockPanoramax.requests.toList()
)
// with what the photo is of: the sidewalk, and the question that asked for it
val photoBody = MockPanoramax.fileBodies.single()
assertTrue(photoBody, photoBody.contains("""{"key":"element_id","value":"way/${SIDEWALK.id}"}"""))
assertTrue(photoBody, photoBody.contains("""{"key":"question","value":"$OBSTRUCTION_Q"}"""))
assertTrue(photoBody, photoBody.contains("""{"key":"quest_id","value":"103"}"""))
// nothing identifying the user, the pictures are public (MapSyncTestBase logs in with an email)
assertFalse(photoBody, photoBody.contains("user_email"))
// the photo is done with: its record (and file) are gone, nothing is left queued
waitUntil("answer synced") { unsyncedEditsCount() == 0 }
assertEquals(0, koin.get<Database>().query(FeaturePhotosTable.NAME) { 1 }.size)
Expand All @@ -79,7 +90,7 @@ class MapSyncPhotoTest : MapSyncTestBase() {

@Test
fun photoKeepsFailingToUpload_userIsTold_andCanRemoveIt_thenTheAnswerUploads() {
MockKartaView.failure = MockKartaView.Failure.Status(HttpStatusCode.ServiceUnavailable)
MockPanoramax.failure = MockPanoramax.Failure.Status(HttpStatusCode.ServiceUnavailable)
openWorkspaceAndSidewalkQuest()
answerWithPhoto()

Expand All @@ -90,12 +101,12 @@ class MapSyncPhotoTest : MapSyncTestBase() {

val way = awaitUploads(1).single().modifiedWays.single()
assertEquals(SIDEWALK.tags + mapOf("surface" to "concrete", "obstacle" to "other"), way.tags)
assertFalse(way.tags.keys.any { it.startsWith("ext:kartaview") })
assertFalse(way.tags.keys.any { it == "ext:street_images" || it.startsWith("ext:kartaview") })
}

@Test
fun keepTrying_closesTheNotice_andTheAnswerStaysQueued() {
MockKartaView.failure = MockKartaView.Failure.Status(HttpStatusCode.ServiceUnavailable)
MockPanoramax.failure = MockPanoramax.Failure.Status(HttpStatusCode.ServiceUnavailable)
openWorkspaceAndSidewalkQuest()
answerWithPhoto()

Expand All @@ -109,12 +120,12 @@ class MapSyncPhotoTest : MapSyncTestBase() {
assertEquals(1, unsyncedEditsCount())
}

// KartaView not answering at all (no connection, DNS, timeout) must be handled like it
// Panoramax not answering at all (no connection, DNS, timeout) must be handled like it
// answering with an error: the photo's answer is held, the others aren't, and after three
// runs the user is told
@Test
fun kartaViewUnreachable_isHandledLikeAKartaViewError() {
MockKartaView.failure = MockKartaView.Failure.Unreachable
fun panoramaxUnreachable_isHandledLikeAPanoramaxError() {
MockPanoramax.failure = MockPanoramax.Failure.Unreachable
openWorkspaceAndSidewalkQuest()
answerWithPhoto()

Expand All @@ -139,7 +150,7 @@ class MapSyncPhotoTest : MapSyncTestBase() {
for (run in 1..3) {
if (run > 1) tapUploadButton()
waitUntil("photo upload attempt $run") {
MockKartaView.requests.size >= run && !uploads.isUploadInProgress
MockPanoramax.requests.size >= run && !uploads.isUploadInProgress
}
}
assertTrue(MockOsmServer.snapshot().uploads.isEmpty())
Expand Down
Loading