Bring up to date with Connection exporting and file transfer updates
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package haus.nightmare.a3270.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
enum class ConnectChoice {
|
||||
NEW_WINDOW,
|
||||
ABORT_CURRENT,
|
||||
CANCEL
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectChoiceDialog(
|
||||
currentTarget: String,
|
||||
onChoice: (ConnectChoice) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val targetText = currentTarget.ifBlank { "the host" }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
onChoice(ConnectChoice.CANCEL)
|
||||
onDismiss()
|
||||
},
|
||||
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color(0xFF1E1E1E),
|
||||
tonalElevation = 6.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.95f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Connection Already Active",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "a3270 is currently connected to $targetText.",
|
||||
fontSize = 13.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Would you like to open the connection in a new window or abort the current connection?",
|
||||
fontSize = 13.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onChoice(ConnectChoice.CANCEL)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text("Cancel", color = Color.Gray)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onChoice(ConnectChoice.ABORT_CURRENT)
|
||||
onDismiss()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC92A2A))
|
||||
) {
|
||||
Text("Abort Current Connection", fontSize = 11.sp)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onChoice(ConnectChoice.NEW_WINDOW)
|
||||
onDismiss()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||
) {
|
||||
Text("New Window", fontSize = 11.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="exports" path="exports/" />
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="files" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,318 @@
|
||||
package haus.nightmare.a3270
|
||||
|
||||
import haus.nightmare.a3270.storage.AppSettings
|
||||
import haus.nightmare.a3270.storage.HostStorage
|
||||
import haus.nightmare.a3270.storage.SavedHost
|
||||
import haus.nightmare.a3270.ui.ConnectChoice
|
||||
import haus.nightmare.a3270.ui.LastTransferState
|
||||
import haus.nightmare.a3270.ui.formatCMSFilter
|
||||
import haus.nightmare.a3270.ui.matchesCmsFilemode
|
||||
import haus.nightmare.a3270.ui.matchesCmsFilter
|
||||
import haus.nightmare.a3270.ui.matchesFilter
|
||||
import haus.nightmare.a3270.ui.matchesTsoFilter
|
||||
import haus.nightmare.a3270.ui.matchesWildcard
|
||||
import haus.nightmare.a3270.ui.resolveDefaultQuery
|
||||
import haus.nightmare.lib3270j.ConnectionConfig
|
||||
import haus.nightmare.lib3270j.TerminalModel
|
||||
import haus.nightmare.lib3270j.ft.FTConfig
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.StringReader
|
||||
import java.util.UUID
|
||||
|
||||
class ConnectionAndHostDirectoryTest {
|
||||
|
||||
@Test
|
||||
fun testSavedHostToConnectionConfigPreservesMetadata() {
|
||||
val hostId = UUID.randomUUID().toString()
|
||||
val host = SavedHost(
|
||||
id = hostId,
|
||||
name = "MVS Production LPAR",
|
||||
host = "mvs.prod.corp",
|
||||
port = 992,
|
||||
model = 4,
|
||||
dynamicRows = 62,
|
||||
dynamicCols = 160,
|
||||
luName = "LU3270A",
|
||||
autoConnect = false,
|
||||
hostType = "TSO",
|
||||
useTls = true,
|
||||
tlsVerifyCert = true,
|
||||
tn3270e = true,
|
||||
graphicsMode = "VECTOR_GRAPHICS",
|
||||
codePage = "1047",
|
||||
proxyType = "SOCKS5",
|
||||
proxyHost = "proxy.corp",
|
||||
proxyPort = 1080,
|
||||
proxyUsername = "mvsadmin",
|
||||
proxyPassword = "mypassword"
|
||||
)
|
||||
|
||||
val config = host.toConnectionConfig()
|
||||
|
||||
assertEquals("mvs.prod.corp", config.host)
|
||||
assertEquals(992, config.port)
|
||||
assertEquals(TerminalModel.IBM_3279_4, config.model)
|
||||
assertEquals("LU3270A", config.luName)
|
||||
assertTrue(config.isUseTls)
|
||||
assertTrue(config.isTlsVerifyCert)
|
||||
assertTrue(config.isTn3270eEnabled)
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, config.graphicsMode)
|
||||
assertEquals("1047", config.codePage)
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, config.proxyType)
|
||||
assertEquals("proxy.corp", config.proxyHost)
|
||||
assertEquals(1080, config.proxyPort)
|
||||
assertEquals("mvsadmin", config.proxyUsername)
|
||||
assertEquals("mypassword", config.proxyPassword)
|
||||
|
||||
// Metadata preservation added in connection hardening
|
||||
assertTrue("savedConnection flag must be true", config.isSavedConnection)
|
||||
assertEquals(hostId, config.savedProfileId)
|
||||
assertEquals("MVS Production LPAR", config.savedProfileName)
|
||||
assertEquals("TSO", config.hostType)
|
||||
assertEquals(FTConfig.HostType.TSO, config.ftHostType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSavedHostToConnectionConfigCmsHostType() {
|
||||
val host = SavedHost(
|
||||
id = "cms-1",
|
||||
name = "VM/CMS Guest",
|
||||
host = "vm.local",
|
||||
port = 23,
|
||||
hostType = "CMS"
|
||||
)
|
||||
val config = host.toConnectionConfig()
|
||||
assertEquals("CMS", config.hostType)
|
||||
assertEquals(FTConfig.HostType.CMS, config.ftHostType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResolveDefaultQuery() {
|
||||
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, null))
|
||||
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, ""))
|
||||
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, " "))
|
||||
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, "*"))
|
||||
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"))
|
||||
assertEquals("PROFILE EXEC A", resolveDefaultQuery(FTConfig.HostType.CMS, "PROFILE EXEC A"))
|
||||
|
||||
assertEquals("", resolveDefaultQuery(FTConfig.HostType.TSO, null))
|
||||
assertEquals("", resolveDefaultQuery(FTConfig.HostType.TSO, ""))
|
||||
assertEquals("SYS1.*", resolveDefaultQuery(FTConfig.HostType.TSO, "SYS1.*"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFormatCMSFilter() {
|
||||
assertEquals("* * A", formatCMSFilter(null))
|
||||
assertEquals("* * A", formatCMSFilter(""))
|
||||
assertEquals("* * A", formatCMSFilter(" "))
|
||||
assertEquals("* * A", formatCMSFilter("*"))
|
||||
assertEquals("* * A", formatCMSFilter("* * *"))
|
||||
assertEquals("MYFILE * A", formatCMSFilter("MYFILE"))
|
||||
assertEquals("MYFILE EXEC A", formatCMSFilter("MYFILE EXEC"))
|
||||
assertEquals("MYFILE EXEC B", formatCMSFilter("MYFILE EXEC B"))
|
||||
assertEquals("PROFILE EXEC A", formatCMSFilter("PROFILE.EXEC"))
|
||||
assertEquals("PROFILE EXEC A", formatCMSFilter("'PROFILE.EXEC'"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchesWildcard() {
|
||||
assertTrue(matchesWildcard("HELLO", "*"))
|
||||
assertTrue(matchesWildcard("HELLO", "H*"))
|
||||
assertTrue(matchesWildcard("HELLO", "H*O"))
|
||||
assertTrue(matchesWildcard("HELLO", "H?LLO"))
|
||||
assertTrue(matchesWildcard("HELLO", "?????"))
|
||||
assertFalse(matchesWildcard("HELLO", "H?L"))
|
||||
assertFalse(matchesWildcard("HELLO", "WORLD"))
|
||||
|
||||
assertTrue(matchesWildcard("DATA.SET.NAME", "*.NAME"))
|
||||
assertTrue(matchesWildcard("DATA.SET.NAME", "DATA.*"))
|
||||
assertFalse(matchesWildcard("DATA.SET.NAME", "SYS1.*"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchesCmsFilterAndFilemode() {
|
||||
val entry = CMSDirectoryEntry("PROFILE", "EXEC", "A1").apply {
|
||||
recfm = "F"
|
||||
lrecl = 80
|
||||
blksize = 80
|
||||
}
|
||||
|
||||
assertTrue(matchesFilter(entry, null))
|
||||
assertTrue(matchesFilter(entry, ""))
|
||||
assertTrue(matchesFilter(entry, "* * *"))
|
||||
assertTrue(matchesFilter(entry, "* * A"))
|
||||
assertTrue(matchesFilter(entry, "PROFILE * A"))
|
||||
assertTrue(matchesFilter(entry, "PROFILE EXEC *"))
|
||||
assertTrue(matchesFilter(entry, "PROFILE EXEC A1"))
|
||||
assertTrue(matchesFilter(entry, "PROFILE.EXEC"))
|
||||
assertTrue(matchesFilter(entry, "PROFILE"))
|
||||
assertTrue(matchesFilter(entry, "EXEC"))
|
||||
|
||||
assertFalse(matchesFilter(entry, "* * B"))
|
||||
assertFalse(matchesFilter(entry, "TEST * *"))
|
||||
assertFalse(matchesFilter(entry, "*.COBOL"))
|
||||
assertFalse(matchesFilter(entry, "PROFILE EXEC A2"))
|
||||
|
||||
assertTrue(matchesCmsFilemode("A1", "A"))
|
||||
assertTrue(matchesCmsFilemode("A1", "A1"))
|
||||
assertTrue(matchesCmsFilemode("A1", "*"))
|
||||
assertFalse(matchesCmsFilemode("A1", "B"))
|
||||
assertFalse(matchesCmsFilemode("A1", "A2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchesTsoFilter() {
|
||||
val entry = TSODirectoryEntry("USER.SOURCE.COBOL").apply {
|
||||
volume = "VOL001"
|
||||
dsorg = "PO"
|
||||
}
|
||||
|
||||
assertTrue(matchesFilter(entry, null))
|
||||
assertTrue(matchesFilter(entry, ""))
|
||||
assertTrue(matchesFilter(entry, "*"))
|
||||
assertTrue(matchesFilter(entry, "*SOURCE*"))
|
||||
assertTrue(matchesFilter(entry, "USER.*.COBOL"))
|
||||
assertTrue(matchesFilter(entry, "COBOL")) // substring search
|
||||
assertTrue(matchesFilter(entry, "USER.SOURCE"))
|
||||
|
||||
assertFalse(matchesFilter(entry, "SYS1.*"))
|
||||
assertFalse(matchesFilter(entry, "*.PLI"))
|
||||
assertFalse(matchesFilter(entry, "FORTRAN"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAppSettingsIniHostTypeParsing() {
|
||||
val testIni = """
|
||||
[Connection]
|
||||
hostType = CMS
|
||||
|
||||
[General]
|
||||
defaultHostType = CICS
|
||||
""".trimIndent()
|
||||
|
||||
var parsedHostType: String? = null
|
||||
AppSettings.parseIni(StringReader(testIni)) { _, key, value ->
|
||||
if (key.equals("hostType", ignoreCase = true) ||
|
||||
key.equals("hosttype", ignoreCase = true) ||
|
||||
key.equals("host_type", ignoreCase = true) ||
|
||||
key.equals("defaulthosttype", ignoreCase = true)
|
||||
) {
|
||||
parsedHostType = value
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(parsedHostType)
|
||||
// First or second value parsed
|
||||
assertTrue(parsedHostType.equals("CMS", ignoreCase = true) || parsedHostType.equals("CICS", ignoreCase = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLastTransferStateUnitsAndDirBlocks() {
|
||||
val state = LastTransferState()
|
||||
assertEquals(FTConfig.HostType.TSO, state.hostType)
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, state.units)
|
||||
assertEquals("", state.dirBlocks)
|
||||
|
||||
state.units = FTConfig.AllocationUnit.TRACKS
|
||||
state.dirBlocks = "20"
|
||||
state.hostType = FTConfig.HostType.CMS
|
||||
|
||||
assertEquals(FTConfig.AllocationUnit.TRACKS, state.units)
|
||||
assertEquals("20", state.dirBlocks)
|
||||
assertEquals(FTConfig.HostType.CMS, state.hostType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConnectChoiceEnum() {
|
||||
val choices = ConnectChoice.values()
|
||||
assertEquals(3, choices.size)
|
||||
assertTrue(choices.contains(ConnectChoice.NEW_WINDOW))
|
||||
assertTrue(choices.contains(ConnectChoice.ABORT_CURRENT))
|
||||
assertTrue(choices.contains(ConnectChoice.CANCEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHostStorageExportAndLoadIniRoundtrip() {
|
||||
val host1 = SavedHost(
|
||||
id = "test-h1",
|
||||
name = "Test Mainframe MVS",
|
||||
host = "mvs.corp.local",
|
||||
port = 992,
|
||||
model = 4,
|
||||
dynamicRows = 62,
|
||||
dynamicCols = 160,
|
||||
luName = "LU01",
|
||||
autoConnect = true,
|
||||
hostType = "TSO",
|
||||
useTls = true,
|
||||
tlsVerifyCert = false,
|
||||
tn3270e = true,
|
||||
graphicsMode = "VECTOR_GRAPHICS",
|
||||
codePage = "1047",
|
||||
proxyType = "SOCKS5",
|
||||
proxyHost = "proxy.corp",
|
||||
proxyPort = 1080,
|
||||
proxyUsername = "admin",
|
||||
proxyPassword = "secret"
|
||||
)
|
||||
val host2 = SavedHost(
|
||||
id = "test-h2",
|
||||
name = "VM CMS Guest",
|
||||
host = "vm.corp.local",
|
||||
port = 23,
|
||||
hostType = "CMS"
|
||||
)
|
||||
|
||||
val sw = java.io.StringWriter()
|
||||
HostStorage.exportToIni(listOf(host1, host2), sw)
|
||||
val exportedIni = sw.toString()
|
||||
|
||||
assertTrue(exportedIni.contains("[Test Mainframe MVS]"))
|
||||
assertTrue(exportedIni.contains("host = mvs.corp.local"))
|
||||
assertTrue(exportedIni.contains("port = 992"))
|
||||
assertTrue(exportedIni.contains("autoConnect = true"))
|
||||
assertTrue(exportedIni.contains("hostType = TSO"))
|
||||
assertTrue(exportedIni.contains("useTls = true"))
|
||||
assertTrue(exportedIni.contains("[VM CMS Guest]"))
|
||||
assertTrue(exportedIni.contains("hostType = CMS"))
|
||||
|
||||
val loaded = HostStorage.loadFromIni(StringReader(exportedIni))
|
||||
assertEquals(2, loaded.size)
|
||||
|
||||
val loadedH1 = loaded.find { it.name == "Test Mainframe MVS" }
|
||||
assertNotNull(loadedH1)
|
||||
assertEquals("test-h1", loadedH1!!.id)
|
||||
assertEquals("mvs.corp.local", loadedH1.host)
|
||||
assertEquals(992, loadedH1.port)
|
||||
assertEquals(4, loadedH1.model)
|
||||
assertEquals(62, loadedH1.dynamicRows)
|
||||
assertEquals(160, loadedH1.dynamicCols)
|
||||
assertEquals("LU01", loadedH1.luName)
|
||||
assertTrue(loadedH1.autoConnect)
|
||||
assertEquals("TSO", loadedH1.hostType)
|
||||
assertTrue(loadedH1.useTls)
|
||||
assertFalse(loadedH1.tlsVerifyCert)
|
||||
assertTrue(loadedH1.tn3270e)
|
||||
assertEquals("VECTOR_GRAPHICS", loadedH1.graphicsMode)
|
||||
assertEquals("1047", loadedH1.codePage)
|
||||
assertEquals("SOCKS5", loadedH1.proxyType)
|
||||
assertEquals("proxy.corp", loadedH1.proxyHost)
|
||||
assertEquals(1080, loadedH1.proxyPort)
|
||||
assertEquals("admin", loadedH1.proxyUsername)
|
||||
assertEquals("secret", loadedH1.proxyPassword)
|
||||
|
||||
val loadedH2 = loaded.find { it.name == "VM CMS Guest" }
|
||||
assertNotNull(loadedH2)
|
||||
assertEquals("test-h2", loadedH2!!.id)
|
||||
assertEquals("vm.corp.local", loadedH2.host)
|
||||
assertEquals(23, loadedH2.port)
|
||||
assertEquals("CMS", loadedH2.hostType)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user