From 8fe88fbc93f48e12e5a7c90149ed177798fc48af Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Sat, 26 Sep 2026 18:33:59 +0800 Subject: [PATCH] test(lotw): PBES2 reader and region-field config tests --- .../look4sat/core/data/lotw/LoTWConfigTest.kt | 78 ++++++++++++++++++ .../core/data/lotw/Pkcs12ReaderTest.kt | 73 ++++++++++++++++ core/data/src/test/resources/test_pbes2.p12 | Bin 0 -> 2457 bytes 3 files changed, 151 insertions(+) create mode 100644 core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/LoTWConfigTest.kt create mode 100644 core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt create mode 100644 core/data/src/test/resources/test_pbes2.p12 diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/LoTWConfigTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/LoTWConfigTest.kt new file mode 100644 index 00000000..647e93d8 --- /dev/null +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/LoTWConfigTest.kt @@ -0,0 +1,78 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ +package com.rtbishop.look4sat.core.data.lotw + +import com.rtbishop.look4sat.core.domain.repository.LoTWZonePair +import java.io.File +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Parses the real bundled ARRL config.tq6 to verify region-field and zonemap extraction. */ +class LoTWConfigTest { + + private val config = LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) + + @Test + fun `china exposes province field with all 31 options and zonemaps`() { + val meta = config.stationMeta(318) + assertEquals("CN_PROVINCE", meta.regionField?.id) + assertEquals("Province", meta.regionField?.label) + assertEquals(31, meta.regionField?.options?.size) + val gd = meta.regionField?.options?.first { it.code == "GD" } + assertEquals("Guangdong", gd?.name) + assertEquals(listOf(LoTWZonePair(44, 24)), gd?.zones) + val gx = meta.regionField?.options?.first { it.code == "GX" } + assertEquals(listOf(LoTWZonePair(43, 24), LoTWZonePair(44, 24)), gx?.zones) + // China spans 8 zone pairs — the UI must NOT prefill from the national map. + assertTrue(meta.countryZones.size > 1) + } + + @Test + fun `usa exposes state field with 49 continental options`() { + val meta = config.stationMeta(291) + assertEquals("US_STATE", meta.regionField?.id) + assertEquals("State", meta.regionField?.label) + // AK and HI are separate DXCC entities (6 and 110) with their own 1-option blocks. + assertEquals(49, meta.regionField?.options?.size) + assertTrue(meta.regionField!!.options.none { it.code == "AK" || it.code == "HI" }) + assertTrue(meta.countryZones.size > 1) + } + + @Test + fun `alaska and hawaii each expose their own single-state block`() { + val ak = config.stationMeta(6) + assertEquals(listOf("AK"), ak.regionField?.options?.map { it.code }) + val hi = config.stationMeta(110) + assertEquals(listOf("HI"), hi.regionField?.options?.map { it.code }) + } + + @Test + fun `germany has no region field and one national zone pair`() { + val meta = config.stationMeta(230) + assertNull(meta.regionField) + assertEquals(listOf(LoTWZonePair(28, 14)), meta.countryZones) + } + + @Test + fun `india has no region field and one national zone pair`() { + val meta = config.stationMeta(324) + assertNull(meta.regionField) + assertEquals(listOf(LoTWZonePair(41, 22)), meta.countryZones) + } + + @Test + fun `unknown entity yields empty meta`() { + val meta = config.stationMeta(999999) + assertNull(meta.regionField) + assertTrue(meta.countryZones.isEmpty()) + } +} diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt new file mode 100644 index 00000000..ebaa9711 --- /dev/null +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/lotw/Pkcs12ReaderTest.kt @@ -0,0 +1,73 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.rtbishop.look4sat.core.data.lotw + +import java.security.Signature +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Verifies the PBES2/AES-256-CBC PKCS12 reader against a fixture generated with + * `openssl pkcs12 -export -certpbe NONE` — same layout as modern TQSL exports + * (plain certificate bags + PBES2-shrouded private key), which Android's legacy + * Bouncy Castle parser cannot read. + */ +class Pkcs12ReaderTest { + + private fun fixture(): ByteArray = + javaClass.classLoader!!.getResourceAsStream("test_pbes2.p12")!!.use { it.readBytes() } + + @Test + fun readsPbes2KeyAndCertificate() { + val (key, cert) = Pkcs12Reader.read(fixture(), "testpass123".toCharArray()) + assertEquals("RSA", key.algorithm) + assertEquals("RSA", cert.publicKey.algorithm) + assertNotNull(cert.subjectX500Principal) + } + + @Test + fun keyPairsWithCertificate() { + val (key, cert) = Pkcs12Reader.read(fixture(), "testpass123".toCharArray()) + val challenge = "Look4Sat LoTW certificate key check".toByteArray(Charsets.US_ASCII) + val signed = Signature.getInstance("SHA1withRSA").run { + initSign(key); update(challenge); sign() + } + val valid = Signature.getInstance("SHA1withRSA").run { + initVerify(cert); update(challenge); verify(signed) + } + assertTrue("private key must match certificate", valid) + } + + @Test + fun wrongPasswordThrows() { + assertThrows(Exception::class.java) { + Pkcs12Reader.read(fixture(), "wrongpass".toCharArray()) + } + } + + @Test + fun garbageBytesThrows() { + assertThrows(Exception::class.java) { + Pkcs12Reader.read("not a p12 file at all".toByteArray(), "x".toCharArray()) + } + } +} diff --git a/core/data/src/test/resources/test_pbes2.p12 b/core/data/src/test/resources/test_pbes2.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c9c160ab3c3ca237b936dccb457c44e7ce63d436 GIT binary patch literal 2457 zcmb7GdpMNa8lP{z!3;A@E;G3s-6kgUjk|FtnG>nWpb`#ksRoU^l4RU-OBbVU(v&V} zlO_?`B70mTB}Pi+)($)5n$Ya=oSshSdCni_{P8~Pd4Iq6U28q>TEBNKh>f}g2SA98 za*{;pB-4@?F@O}nW1}n(Y?P@OHiXz<(SK0jWds|%B!+XumMJOoJqs-jfIK$nCI;z} z(%(T8PCOTAD^9Q!<7N;WPT+xX01j8&#rep>^A!?kjIu!~gF~I8NOdO&^J5`EMgj(r zD5{XMBtL$w*yGPKsF!;gV=yHoDBf5avX%q^;5sV8=BMA{BV~xGn_I) zdC1uGhf-3d?W!fL#!$j_W0`M$>uD-=%vB=#9IMb1C29B z(tv_XfQyzg`RqZRZqiAWi`jUWS+dlJ`n=;N0X+{ort^a)BYYF2yt&vVuv}xzdMU#^ z-Mhk61>>R1`!w?E`T^yHy&4ys2?2*+47;Tz_&WwufETlkURW*D)wGs$<=xz7KB6m7 zdQv{{X2%HA8#bbXBe*+<@f){0iv%s7or|mCAOHgjT_HN8Es_BUo;k zkupmaLf{^h|3PZh^JT4{hV6Z z!3%Tk*r0hFVz)|$rTtgATL8-Cyy3^h-jMd0&B8`xfgic_?WGEMvoL+DZ?6CC*Aik*KZ-!+M-{*gLS5bO|7kNX|{B-71C{yVfgqf`Hy#W+V(Z+ROY0#(n1)rLeK6#{S{$L zS-Im>kkT7D;jk@AEkm?>o5yo>N`!gO?DFPv<`E8Y$%P|o!*snqByey1{UzNt)1f63 zW4-MHNB&YCr*Wtg3)RDA=Eo_UpbbB?fl{J~iwzR- z4=wl-u5$~<+~{gn>-HigvA!e{e}t{cSRw+!mIx8UL1H`lvsU3^k|GATK^{LcBrwoU zXzRaSG^B<=7Dq)zW*dwc!r@35gqB49OWAOgSfA>WsDCEY0Dlkx=peA+d$GP9-qiap zhT4W|@SGC}Hlj;^J2!M_I+X5zHrkI_+8NhJ&g&7BA3Zdz8Au4-M@Y`tP40T2wnw(p zi6A{4y)e;N8`-Hf4@7*vT$=kX6o`<=Z)2%c>eiVI*L1A|@8_se-`z;UU^4Y!8}skB zDY3Xk5jou6){1jd`*_`wT=U7LB$PJP1PEp#Sk}rlST{F^m@_$fcit#X(G)KY$Ecw= zWwg5b#fC_i*u_2mRbNKZ4qiWY)eHRh_O7xF=e>};{KOMcjRg%I-n4AL_0PMY;FQf7`YP|) za=SW)hrgVdav|rI48%86;vVXmXnMCFE6Ybc_Aq({F}>)7k~{n>O+9E*AbNUmbb9?g zPA)IVrV?MQ%|^-=mP;!(Qf;pNt}{V&Gh1xg89mmpE)vVMPe zoW)8Q^9J@|L$@5_^0wNLjDW(yige2?u-`Z2!f5Y}q4?SG0s4B>M-hiW+G6Cg(49fq z8GI63I+{qzH2X4Q{(RGp!AUK5``)o-hlOLa4*sT(uScmnK6WQ^aux@arqyT5n#+Ur zn2$Sm4I@i4;68yl^YhmCcX*nlpFMSRh1NMcF)#Ov@O*tkjV>23*tDggBDdAX3yAEm?myK$hMU=PEb<>#eG5TOVN@SRO+IWbbz*dObqgn9XFF zpwisUyr=UhCeK5theeqt)gMcKRW%zhequctV)3j4GY(VUL~;(j=gy##bX}dUnh6!j zgpJQ_4IA!8wqEfJ{)cuKUoMZA++=^d$u2Q}K~0t=}sBi z6+Ly-JCTm|&MVl6=Ef6h>Rwyw&ev{9V7;O=wk0K?*n!JF85c+1X6G67%0I17v>#g~ z>(1W%Y&0TQC=)xgg`V6rKP916oOe-Ix-35K_O%Ds9|jsT`lA^4Ny4h0lFSIbI@*(U z5ML-LJQfo)zV6Pea1-jC-48yw>s=ugczvSlcV|Bv9oO(nX9gZ$wdHLUrR-TY7@bYh z_^mrVH!7-W52}lNw+{U6wKAE?Jc|=_}jsws2^~6Z*5f z8lED