diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index e3dbb03816..dfa191bea1 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -5,6 +5,7 @@ import ftl.cli.FirebaseCommand import ftl.cli.firebase.CancelCommand import ftl.cli.firebase.RefreshCommand import ftl.cli.firebase.test.AndroidCommand +import ftl.cli.firebase.test.IPBlocksCommand import ftl.cli.firebase.test.IosCommand import ftl.cli.firebase.test.NetworkProfilesCommand import ftl.cli.firebase.test.ProvidedSoftwareCommand @@ -25,7 +26,8 @@ import picocli.CommandLine CancelCommand::class, AuthCommand::class, ProvidedSoftwareCommand::class, - NetworkProfilesCommand::class + NetworkProfilesCommand::class, + IPBlocksCommand::class ] ) class Main : Runnable { diff --git a/test_runner/src/main/kotlin/ftl/cli/firebase/TestCommand.kt b/test_runner/src/main/kotlin/ftl/cli/firebase/TestCommand.kt index b73fcc35eb..a22405277c 100644 --- a/test_runner/src/main/kotlin/ftl/cli/firebase/TestCommand.kt +++ b/test_runner/src/main/kotlin/ftl/cli/firebase/TestCommand.kt @@ -1,6 +1,7 @@ package ftl.cli.firebase import ftl.cli.firebase.test.AndroidCommand +import ftl.cli.firebase.test.IPBlocksCommand import ftl.cli.firebase.test.IosCommand import ftl.cli.firebase.test.NetworkProfilesCommand import ftl.cli.firebase.test.ProvidedSoftwareCommand @@ -14,7 +15,8 @@ import picocli.CommandLine.Command AndroidCommand::class, IosCommand::class, NetworkProfilesCommand::class, - ProvidedSoftwareCommand::class + ProvidedSoftwareCommand::class, + IPBlocksCommand::class ], usageHelpAutoWidth = true ) diff --git a/test_runner/src/main/kotlin/ftl/cli/firebase/test/IPBlocksCommand.kt b/test_runner/src/main/kotlin/ftl/cli/firebase/test/IPBlocksCommand.kt new file mode 100644 index 0000000000..d2091b72a2 --- /dev/null +++ b/test_runner/src/main/kotlin/ftl/cli/firebase/test/IPBlocksCommand.kt @@ -0,0 +1,17 @@ +package ftl.cli.firebase.test + +import ftl.cli.firebase.test.ipblocks.IPBlocksListCommand +import picocli.CommandLine + +@CommandLine.Command( + name = "ip-blocks", + synopsisHeading = "", + subcommands = [IPBlocksListCommand::class], + header = ["Explore IP blocks used by Firebase Test Lab devices."], + usageHelpAutoWidth = true +) +class IPBlocksCommand : Runnable { + override fun run() { + CommandLine.usage(IPBlocksCommand(), System.out) + } +} diff --git a/test_runner/src/main/kotlin/ftl/cli/firebase/test/NetworkProfilesCommand.kt b/test_runner/src/main/kotlin/ftl/cli/firebase/test/NetworkProfilesCommand.kt index 15f779737f..45617c4ae2 100644 --- a/test_runner/src/main/kotlin/ftl/cli/firebase/test/NetworkProfilesCommand.kt +++ b/test_runner/src/main/kotlin/ftl/cli/firebase/test/NetworkProfilesCommand.kt @@ -11,6 +11,7 @@ import picocli.CommandLine NetworkProfilesListCommand::class, NetworkProfilesDescribeCommand::class ], + header = ["Explore network profiles available for testing."], usageHelpAutoWidth = true ) class NetworkProfilesCommand : Runnable { diff --git a/test_runner/src/main/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommand.kt b/test_runner/src/main/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommand.kt new file mode 100644 index 0000000000..7e303ff3e8 --- /dev/null +++ b/test_runner/src/main/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommand.kt @@ -0,0 +1,21 @@ +package ftl.cli.firebase.test.ipblocks + +import ftl.environment.ipBlocksListAsTable +import picocli.CommandLine + +@CommandLine.Command( + name = "list", + sortOptions = false, + headerHeading = "", + synopsisHeading = "%n", + descriptionHeading = "%n@|bold,underline Description:|@%n%n", + parameterListHeading = "%n@|bold,underline Parameters:|@%n", + optionListHeading = "%n@|bold,underline Options:|@%n", + header = ["List all IP address blocks used by Firebase Test Lab devices"], + usageHelpAutoWidth = true +) +class IPBlocksListCommand : Runnable { + override fun run() { + println(ipBlocksListAsTable()) + } +} diff --git a/test_runner/src/main/kotlin/ftl/environment/ListIPBlocks.kt b/test_runner/src/main/kotlin/ftl/environment/ListIPBlocks.kt new file mode 100644 index 0000000000..a3f993332e --- /dev/null +++ b/test_runner/src/main/kotlin/ftl/environment/ListIPBlocks.kt @@ -0,0 +1,54 @@ +package ftl.environment + +import com.google.api.services.testing.model.Date +import com.google.api.services.testing.model.DeviceIpBlock +import ftl.gc.deviceIPBlocks +import ftl.reports.api.twoDigitString +import ftl.util.TableColumn +import ftl.util.TableStyle +import ftl.util.buildTable +import java.util.Objects.isNull + +fun ipBlocksListAsTable() = deviceIPBlocks() + .toNullProof() + .createDataMap() + .collectDataPerColumn() + .buildTable() + +private fun List.toNullProof() = + map { IpBlocksNonNull(it.block.orUnable, it.form.orUnable, addedDate = it.addedDate.prettyDate) } + +private fun List.createDataMap() = fold(mutableMapOf>()) { map, ipBlock -> + map.apply { + getOrCreateList(IP_BLOCK).add(ipBlock.block) + getOrCreateList(IP_FORM).add(ipBlock.form) + getOrCreateList(IP_ADDED_DATE).add(ipBlock.addedDate) + } +} + +private fun Map>.collectDataPerColumn() = map { (header, data) -> TableColumn(header, data) } + +private fun List.buildTable() = + if (isNotEmpty()) buildTable(*toTypedArray(), tableStyle = TableStyle.DEFAULT) + else "--Flank was unable to get data from TestLab--" + +// yyyy-mm-dd +private val Date?.prettyDate + get() = this?.run { if (allNotNull(year, month, day)) "$year-${month.twoDigitString()}-${day.twoDigitString()}" else null } + ?: UNABLE + +private fun allNotNull(vararg nullable: Any?) = nullable.none { isNull(it) } + +private val String?.orUnable + get() = this ?: UNABLE + +private const val IP_BLOCK = "BLOCK" +private const val IP_FORM = "FORM" +private const val IP_ADDED_DATE = "ADDED_DATE" +private const val UNABLE = "[Unable to fetch]" + +private data class IpBlocksNonNull( + val block: String, + val form: String, + val addedDate: String +) diff --git a/test_runner/src/main/kotlin/ftl/gc/GcTesting.kt b/test_runner/src/main/kotlin/ftl/gc/GcTesting.kt index 2cc1976a8e..c36f2fb098 100644 --- a/test_runner/src/main/kotlin/ftl/gc/GcTesting.kt +++ b/test_runner/src/main/kotlin/ftl/gc/GcTesting.kt @@ -6,6 +6,7 @@ import ftl.config.FtlConstants.JSON_FACTORY import ftl.config.FtlConstants.applicationName import ftl.config.FtlConstants.httpCredential import ftl.config.FtlConstants.httpTransport +import ftl.http.executeWithRetry object GcTesting { val get: Testing by lazy { @@ -17,3 +18,10 @@ object GcTesting { builder.build() } } + +fun deviceIPBlocks() = GcTesting.get.testEnvironmentCatalog() + .get("DEVICE_IP_BLOCKS") + .executeWithRetry() + ?.deviceIpBlockCatalog + ?.ipBlocks + .orEmpty() diff --git a/test_runner/src/test/kotlin/ftl/cli/firebase/test/IPBlocksCommandTest.kt b/test_runner/src/test/kotlin/ftl/cli/firebase/test/IPBlocksCommandTest.kt new file mode 100644 index 0000000000..c99c3e437b --- /dev/null +++ b/test_runner/src/test/kotlin/ftl/cli/firebase/test/IPBlocksCommandTest.kt @@ -0,0 +1,28 @@ +package ftl.cli.firebase.test + +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.contrib.java.lang.system.SystemOutRule + +class IPBlocksCommandTest { + + @get:Rule + val out = SystemOutRule().enableLog().muteForSuccessfulTests() as SystemOutRule + + @Test + fun printHelp() { + IPBlocksCommand().run() + + val expected = """ +Explore IP blocks used by Firebase Test Lab devices. +ip-blocks [COMMAND] +Commands: + list List all IP address blocks used by Firebase Test Lab devices + """.trimIndent() + + val actual = out.log.trim() + + assertEquals(expected, actual) + } +} diff --git a/test_runner/src/test/kotlin/ftl/cli/firebase/test/NetworkProfilesCommandTest.kt b/test_runner/src/test/kotlin/ftl/cli/firebase/test/NetworkProfilesCommandTest.kt index 1a9b20272b..d6ab57c178 100644 --- a/test_runner/src/test/kotlin/ftl/cli/firebase/test/NetworkProfilesCommandTest.kt +++ b/test_runner/src/test/kotlin/ftl/cli/firebase/test/NetworkProfilesCommandTest.kt @@ -17,11 +17,12 @@ class NetworkProfilesCommandTest { NetworkProfilesCommand().run() val expected = listOf( - "network-profiles [COMMAND]", - "Commands:", - " list List all network profiles available for testing", - " describe Describe a network profile", - "" + "Explore network profiles available for testing.", + "network-profiles [COMMAND]", + "Commands:", + " list List all network profiles available for testing", + " describe Describe a network profile", + "" ).joinToString("\n") val actual = systemOutRule.log.normalizeLineEnding() diff --git a/test_runner/src/test/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommandTest.kt b/test_runner/src/test/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommandTest.kt new file mode 100644 index 0000000000..27109e8e66 --- /dev/null +++ b/test_runner/src/test/kotlin/ftl/cli/firebase/test/ipblocks/IPBlocksListCommandTest.kt @@ -0,0 +1,152 @@ +package ftl.cli.firebase.test.ipblocks + +import com.google.api.services.testing.model.Date +import com.google.api.services.testing.model.DeviceIpBlock +import ftl.gc.deviceIPBlocks +import ftl.test.util.runMainCommand +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.contrib.java.lang.system.SystemOutRule + +class IPBlocksListCommandTest { + + @get:Rule + val out = SystemOutRule().enableLog().muteForSuccessfulTests() as SystemOutRule + + @Before + fun setup() = mockkStatic("ftl.gc.GcTestingKt") + + @After + fun tearDown() = unmockkAll() + + @Test + fun `should print error message if no data was provided from FTL`() { + val expected = "--Flank was unable to get data from TestLab--" + every { deviceIPBlocks() } returns emptyList() + + out.clearLog() + runMainCommand("ip-blocks", "list") + + val result = out.log.trim() + + assertEquals(expected, result) + } + + @Test + fun `should print ips for devices`() { + val ips = listOf( + DeviceIpBlock().apply { + block = "1.1.1.1/1" + form = "AnyForm" + addedDate = Date().apply { + day = 1 + month = 1 + year = 1111 + } + }, + DeviceIpBlock().apply { + block = "2.2.2.2/2" + form = "OtherForm" + addedDate = Date().apply { + day = 12 + month = 12 + year = 1212 + } + } + ) + + every { deviceIPBlocks() } returns ips + val expected = """ + ┌───────────┬───────────┬────────────┐ + │ BLOCK │ FORM │ ADDED_DATE │ + ├───────────┼───────────┼────────────┤ + │ 1.1.1.1/1 │ AnyForm │ 1111-01-01 │ + │ 2.2.2.2/2 │ OtherForm │ 1212-12-12 │ + └───────────┴───────────┴────────────┘ + """.trimIndent() + out.clearLog() + + runMainCommand("ip-blocks", "list") + + val result = out.log.trim() + + assertEquals(expected, result) + } + + @Test + fun `should not fail when FTL returns null in any of values`() { + val ips = listOf( + DeviceIpBlock().apply { + block = "1.1.1.1/1" + form = "AnyForm" + addedDate = Date().apply { + day = null + month = 1 + year = 1111 + } + }, + DeviceIpBlock().apply { + block = null + form = "MissingIpForm" + addedDate = Date().apply { + day = 12 + month = 2 + year = 1212 + } + }, + DeviceIpBlock().apply { + block = "2.2.2.2/2" + form = "OtherForm" + addedDate = Date().apply { + day = 12 + month = null + year = 1212 + } + }, + DeviceIpBlock().apply { + block = "3.3.3.3/4" + form = "FunnyForm" + addedDate = Date().apply { + day = 8 + month = 2 + year = null + } + }, + DeviceIpBlock().apply { + block = "4.4.4.4/4" + form = null + addedDate = Date().apply { + day = 8 + month = 2 + year = 1523 + } + } + ) + + every { deviceIPBlocks() } returns ips + val expected = """ + ┌───────────────────┬───────────────────┬───────────────────┐ + │ BLOCK │ FORM │ ADDED_DATE │ + ├───────────────────┼───────────────────┼───────────────────┤ + │ 1.1.1.1/1 │ AnyForm │ [Unable to fetch] │ + │ [Unable to fetch] │ MissingIpForm │ 1212-02-12 │ + │ 2.2.2.2/2 │ OtherForm │ [Unable to fetch] │ + │ 3.3.3.3/4 │ FunnyForm │ [Unable to fetch] │ + │ 4.4.4.4/4 │ [Unable to fetch] │ 1523-02-08 │ + └───────────────────┴───────────────────┴───────────────────┘ + """.trimIndent() + out.clearLog() + + runMainCommand("ip-blocks", "list") + + val result = out.log.trim() + + assertEquals(expected, result) + } +}