|
|
|
@@ -0,0 +1,115 @@
|
|
|
|
|
package com.m57.hermescontrol.ui.cron
|
|
|
|
|
|
|
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
|
|
import androidx.compose.runtime.collectAsState
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
import androidx.compose.ui.test.hasContentDescription
|
|
|
|
|
import androidx.compose.ui.test.junit4.createComposeRule
|
|
|
|
|
import androidx.compose.ui.test.onAllNodesWithText
|
|
|
|
|
import androidx.compose.ui.test.onNodeWithText
|
|
|
|
|
import androidx.compose.ui.test.performClick
|
|
|
|
|
import androidx.compose.ui.test.performScrollTo
|
|
|
|
|
import androidx.lifecycle.ViewModelProvider
|
|
|
|
|
import androidx.lifecycle.ViewModelStore
|
|
|
|
|
import androidx.lifecycle.ViewModelStoreOwner
|
|
|
|
|
import androidx.test.platform.app.InstrumentationRegistry
|
|
|
|
|
import com.m57.hermescontrol.BuildConfig
|
|
|
|
|
import com.m57.hermescontrol.R
|
|
|
|
|
import com.m57.hermescontrol.data.local.AuthManager
|
|
|
|
|
import com.m57.hermescontrol.data.remote.ApiClient
|
|
|
|
|
import java.net.URL
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
import org.junit.Assert.assertEquals
|
|
|
|
|
import org.junit.Assert.assertTrue
|
|
|
|
|
import org.junit.Assume.assumeTrue
|
|
|
|
|
import org.junit.Rule
|
|
|
|
|
import org.junit.Test
|
|
|
|
|
|
|
|
|
|
/** Opt-in test against a fresh, isolated real cron backend, never a user server. */
|
|
|
|
|
class ScheduleHttpAcceptanceTest {
|
|
|
|
|
@get:Rule val compose = createComposeRule()
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
fun editorCreatesEditsAndScheduledJobExecutesViaHttp() {
|
|
|
|
|
val args = InstrumentationRegistry.getArguments()
|
|
|
|
|
assumeTrue(args.getString("isolatedCronAcceptance") == "true")
|
|
|
|
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
|
|
|
check(BuildConfig.DEBUG && context.packageName.endsWith(".dev"))
|
|
|
|
|
val url = "http://127.0.0.1:18765"
|
|
|
|
|
AuthManager.init(context)
|
|
|
|
|
kotlinx.coroutines.runBlocking { AuthManager.awaitInitialization() }
|
|
|
|
|
val oldUrl = AuthManager.getBaseUrl()
|
|
|
|
|
val oldToken = AuthManager.getToken()
|
|
|
|
|
val store = ViewModelStore()
|
|
|
|
|
val owner = object : ViewModelStoreOwner { override val viewModelStore = store }
|
|
|
|
|
try {
|
|
|
|
|
AuthManager.setBaseUrl(url)
|
|
|
|
|
AuthManager.setToken(null)
|
|
|
|
|
ApiClient.rebuild()
|
|
|
|
|
val vm = ViewModelProvider(owner)[CronJobsViewModel::class.java]
|
|
|
|
|
compose.setContent {
|
|
|
|
|
val state by vm.uiState.collectAsState()
|
|
|
|
|
MaterialTheme {
|
|
|
|
|
if (state.editorState.isOpen) {
|
|
|
|
|
CronJobEditorDialog(
|
|
|
|
|
state = state.editorState,
|
|
|
|
|
onFieldChange = vm::updateEditorField,
|
|
|
|
|
onToggleNoAgent = vm::toggleNoAgent,
|
|
|
|
|
onToggleRunContinuity = vm::toggleRunContinuity,
|
|
|
|
|
onSetMonitorMode = vm::setMonitorMode,
|
|
|
|
|
onSelectBlueprint = vm::selectBlueprint,
|
|
|
|
|
onBlueprintFieldChange = vm::updateBlueprintValue,
|
|
|
|
|
onSave = vm::saveEditor,
|
|
|
|
|
onDismiss = vm::closeEditor,
|
|
|
|
|
onClearToast = vm::clearEditorToast,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Harmless execution fixture. Schedule is changed using the real clock below.
|
|
|
|
|
compose.runOnIdle {
|
|
|
|
|
vm.openNewJobDialog()
|
|
|
|
|
vm.updateEditorField("name", "Android HTTP clock acceptance")
|
|
|
|
|
vm.updateEditorField("script", "probe.py")
|
|
|
|
|
vm.updateEditorField("schedule", "59 23 * * *")
|
|
|
|
|
vm.toggleNoAgent()
|
|
|
|
|
}
|
|
|
|
|
compose.onNodeWithText("23:59").performScrollTo().performClick()
|
|
|
|
|
compose.onNodeWithText("59").performClick()
|
|
|
|
|
compose.onNode(hasContentDescription("55", substring = true)).performClick()
|
|
|
|
|
compose.onNodeWithText(context.getString(R.string.action_ok)).performClick()
|
|
|
|
|
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
|
|
|
|
|
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
|
|
|
|
|
compose.waitUntil(30_000) { vm.uiState.value.jobs.size == 1 }
|
|
|
|
|
val id = vm.uiState.value.jobs.single().id
|
|
|
|
|
compose.runOnIdle { vm.openEditJobDialog(id) }
|
|
|
|
|
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isLoading }
|
|
|
|
|
compose.runOnIdle { assertEquals("55 23 * * *", vm.uiState.value.editorState.schedule) }
|
|
|
|
|
compose.onNodeWithText("23:55").performScrollTo()
|
|
|
|
|
compose.onNodeWithText(context.getString(R.string.cron_repeat_label)).performClick()
|
|
|
|
|
compose.onAllNodesWithText(context.getString(R.string.cron_repeat_minutely))[0].performClick()
|
|
|
|
|
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
|
|
|
|
|
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
|
|
|
|
|
val deadline = android.os.SystemClock.elapsedRealtime() + 90_000
|
|
|
|
|
var evidence = JSONObject()
|
|
|
|
|
while (android.os.SystemClock.elapsedRealtime() < deadline) {
|
|
|
|
|
evidence = JSONObject(URL("$url/acceptance/evidence").readText())
|
|
|
|
|
if (evidence.optString("result") == "PASS") break
|
|
|
|
|
android.os.SystemClock.sleep(500)
|
|
|
|
|
}
|
|
|
|
|
assertEquals(evidence.toString(), "PASS", evidence.optString("result"))
|
|
|
|
|
assertEquals(id, evidence.getString("job_id"))
|
|
|
|
|
assertEquals(1, evidence.getInt("runs"))
|
|
|
|
|
val requests = evidence.getJSONArray("requests")
|
|
|
|
|
assertEquals(2, requests.length())
|
|
|
|
|
assertEquals("55 23 * * *", requests.getJSONObject(0).getJSONObject("body").getString("schedule"))
|
|
|
|
|
assertEquals("* * * * *", requests.getJSONObject(1).getJSONObject("body").getJSONObject("updates").getString("schedule"))
|
|
|
|
|
assertTrue(requests.getJSONObject(1).getString("path").endsWith(id))
|
|
|
|
|
} finally {
|
|
|
|
|
compose.runOnIdle { store.clear() }
|
|
|
|
|
AuthManager.setBaseUrl(oldUrl)
|
|
|
|
|
AuthManager.setToken(oldToken)
|
|
|
|
|
ApiClient.rebuild()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|