diff --git a/README.md b/README.md index acc56d56..369455d2 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ To test this flow locally: Communication between the COAP HTTP Proxy and the Crest device service should be encrypted using mutual TLS. The repositories contain test certificates that can be used for local testing. (they are not included in the jar or docker image) -They can be also be (re)generated using the [generate_certificates.sh](generate_certificates.sh) script. +They can be also be (re)generated using the [generate_certificates.sh](scripts/generate_certificates.sh) script. diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 09a3a607..f2ab359a 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -13,9 +13,11 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.security:spring-security-core") implementation("org.springframework.kafka:spring-kafka") + implementation(project(":components:avro")) implementation(project(":components:device")) implementation(project(":components:firmware")) implementation(project(":components:psk")) @@ -27,8 +29,6 @@ dependencies { implementation(libs.commonsCodec) - implementation(libs.avro) - runtimeOnly("io.micrometer:micrometer-registry-prometheus") runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.flywaydb:flyway-database-postgresql") @@ -73,12 +73,12 @@ testing { useJUnitJupiter() dependencies { implementation(project()) + implementation(project(":components:avro")) + implementation(project(":components:psk")) implementation(project(":components:device")) implementation(project(":components:firmware")) - implementation(project(":components:psk")) implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation(libs.kafkaAvro) - implementation(libs.avro) implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.boot:spring-boot-starter-test") implementation("org.springframework.kafka:spring-kafka-test") diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt index 35819ce4..d5228b72 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/CoapMessageHandlingTest.kt @@ -34,7 +34,7 @@ import org.springframework.kafka.test.context.EmbeddedKafka import org.springframework.kafka.test.utils.KafkaTestUtils import org.springframework.test.annotation.DirtiesContext -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @EmbeddedKafka(topics = ["\${kafka.producers.command-feedback.topic}"]) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class CoapMessageHandlingTest { diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/DeviceCredentialsRetrievalTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/DeviceCredentialsRetrievalTest.kt index 8c8d4f97..3850838b 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/DeviceCredentialsRetrievalTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/DeviceCredentialsRetrievalTest.kt @@ -21,11 +21,13 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.kafka.test.context.EmbeddedKafka +import org.springframework.test.annotation.DirtiesContext -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @EmbeddedKafka( topics = ["\${kafka.producers.device-message.topic}"], ) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class DeviceCredentialsRetrievalTest { companion object { private const val IDENTITY = "1234" diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/IntegrationTestHelper.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/IntegrationTestHelper.kt index 366bae44..55d67a36 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/IntegrationTestHelper.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/IntegrationTestHelper.kt @@ -4,6 +4,7 @@ package org.gxf.crestdeviceservice import com.alliander.sng.CommandFeedback +import com.alliander.sng.Firmwares import com.gxf.utilities.kafka.avro.AvroDeserializer import com.gxf.utilities.kafka.avro.AvroSerializer import org.apache.avro.specific.SpecificRecordBase @@ -30,7 +31,9 @@ object IntegrationTestHelper { DefaultKafkaConsumerFactory( testProperties, StringDeserializer(), - AvroDeserializer(listOf(DeviceMessage.getClassSchema(), CommandFeedback.getClassSchema()))) + AvroDeserializer( + listOf( + DeviceMessage.getClassSchema(), CommandFeedback.getClassSchema(), Firmwares.getClassSchema()))) val consumer = consumerFactory.createConsumer() embeddedKafkaBroker.consumeFromAnEmbeddedTopic(consumer, topic) return consumer diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MakiCommandHandlingTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MakiCommandHandlingTest.kt index 6db1f826..c13d2ffb 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MakiCommandHandlingTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MakiCommandHandlingTest.kt @@ -23,7 +23,7 @@ import org.springframework.kafka.test.context.EmbeddedKafka import org.springframework.kafka.test.utils.KafkaTestUtils import org.springframework.test.annotation.DirtiesContext -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @EmbeddedKafka(topics = ["\${kafka.consumers.command.topic}", "\${kafka.producers.command-feedback.topic}"]) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class MakiCommandHandlingTest { diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MessageHandlingTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MessageHandlingTest.kt index e29e7509..6a7e93f9 100644 --- a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MessageHandlingTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/MessageHandlingTest.kt @@ -23,7 +23,7 @@ import org.springframework.kafka.test.context.EmbeddedKafka import org.springframework.test.annotation.DirtiesContext import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @EmbeddedKafka( topics = ["\${kafka.producers.device-message.topic}"], ) diff --git a/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/WebServerTest.kt b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/WebServerTest.kt new file mode 100644 index 00000000..79159f49 --- /dev/null +++ b/application/src/integrationTest/kotlin/org/gxf/crestdeviceservice/WebServerTest.kt @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import java.io.File +import java.time.Duration +import java.time.Instant +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.IntegrationTestHelper.createKafkaConsumer +import org.gxf.crestdeviceservice.config.KafkaProducerProperties +import org.gxf.crestdeviceservice.firmware.repository.FirmwarePacketRepository +import org.gxf.crestdeviceservice.firmware.repository.FirmwareRepository +import org.gxf.crestdeviceservice.psk.entity.PreSharedKey +import org.gxf.crestdeviceservice.psk.entity.PreSharedKeyStatus +import org.gxf.crestdeviceservice.psk.repository.PskRepository +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.client.postForEntity +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.FileSystemResource +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.kafka.test.EmbeddedKafkaBroker +import org.springframework.kafka.test.context.EmbeddedKafka +import org.springframework.test.annotation.DirtiesContext +import org.springframework.util.LinkedMultiValueMap + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EmbeddedKafka(topics = ["\${kafka.producers.firmware.topic}"]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@EnableConfigurationProperties(KafkaProducerProperties::class) +class WebServerTest { + @Autowired private lateinit var restTemplate: TestRestTemplate + @Autowired private lateinit var firmwareRepository: FirmwareRepository + @Autowired private lateinit var firmwarePacketRepository: FirmwarePacketRepository + @Autowired private lateinit var embeddedKafkaBroker: EmbeddedKafkaBroker + @Autowired private lateinit var kafkaProducerProperties: KafkaProducerProperties + @Autowired private lateinit var pskRepository: PskRepository + + companion object { + private const val NAME = "RTU#FULL#TO#23.10.txt" + private const val NUMBER_OF_PACKETS = 13 + private const val IDENTITY = "1234" + private const val PRE_SHARED_KEY = "1234567890123456" + } + + @BeforeEach + fun setup() { + pskRepository.save(PreSharedKey(IDENTITY, 0, Instant.MIN, PRE_SHARED_KEY, PreSharedKeyStatus.ACTIVE)) + } + + @AfterEach + fun cleanup() { + pskRepository.deleteAll() + } + + @Test + fun firmwareFileUploadTest() { + // arrange + val firmwareFile = ClassPathResource(NAME).file + val consumer = createKafkaConsumer(embeddedKafkaBroker, kafkaProducerProperties.firmware.topic) + + // act + val response = uploadFile(firmwareFile) + + // assert + assertThat(response.statusCode.value()).isEqualTo(302) + assertThat(firmwareRepository.findByName(NAME)).isNotNull + assertThat(firmwarePacketRepository.findAll()).hasSize(NUMBER_OF_PACKETS) + + val records = consumer.poll(Duration.ofSeconds(1)) + assertThat(records.records(kafkaProducerProperties.firmware.topic)).hasSize(1) + } + + fun uploadFile(file: File): ResponseEntity { + val headers: HttpHeaders = HttpHeaders().apply { contentType = MediaType.MULTIPART_FORM_DATA } + + val body = LinkedMultiValueMap().apply { add("file", FileSystemResource(file)) } + val requestEntity = HttpEntity(body, headers) + + return this.restTemplate.postForEntity("/web/firmware", requestEntity) + } + + @Test + fun pskRequestOnWebPortShouldReturn404() { + // create second PSK for identity this one should be returned + pskRepository.save(PreSharedKey(IDENTITY, 1, Instant.MIN, "0000111122223333", PreSharedKeyStatus.ACTIVE)) + + val headers = HttpHeaders().apply { add("x-device-identity", IDENTITY) } + val result = restTemplate.exchange("/psk", HttpMethod.GET, HttpEntity(headers), String::class.java) + + assertThat(result.statusCode.is4xxClientError).isTrue() + } +} diff --git a/application/src/integrationTest/resources/RTU#FULL#TO#23.10.txt b/application/src/integrationTest/resources/RTU#FULL#TO#23.10.txt new file mode 100644 index 00000000..c42df97c --- /dev/null +++ b/application/src/integrationTest/resources/RTU#FULL#TO#23.10.txt @@ -0,0 +1,13 @@ +OTA00004Ny+G|NeoXu!zMI*ungbnQwylZQJ20!QA=&G(L+*5CO0Nu?N}rC*rK{yx{@B9g?g&9}oyo0Jv!VL0p$<~pa8sqK#{a3P?5DKV3D>baFNpx2(SRRCxjc73#JRV3yFi8gR6s45D5SP&9~mSYyo=#;E}o~@R7SGFc1k405F#+@MP}OAOIil_5nbFQGsHCbAf=7v?pv32`~VVk+mnFk+vtWk+&y=50xKA5Dee|54I1I5hRb3hTwpc=_ZenpAZcI01uK8B#)DylY_B=aFCJiCSMQ@5CD-81;B_S8Ss(dkdy2tfRq0a4IlvSCZCaykq?j$z7LQekQkNt5Dh>8g8PUkfccP-?a00Y32;ELw%*4WIyl5e&eD;0M4>n+U*wk?JPt5Dl;Zfe{42h2Vgb>Lw|gK#>L~ln@QT0EOU};trHkGXIh9k-w3zk$w;mPym3D_a%W52f$yL5Rw%Ff#CTN5O4s3;DD3&B`TOJm?@Z9nt+j&5D<_6fRX7Yfe{wK4wQEikl=uo7!V~807uV&;0Tp~kw+(nm0Zn;4^9vzAOL`qM<-yDNhd&)NGAxD|B_b_B|rcOFYu5^ClHZIC*s_J5fH#M5G7CmfRRTh1KoiU3&0;K5NIIc{SYO90AS#7;L8Cdz<`lQCy!s|+}sc)umB5GDWsfRRZjlHkdofDI8OVc>A!91t?l0DzJ7CXkW!CZLh_Ca{tBCfpD*-~fp$iYtpNA@v}V^d>Np^(FuiGVlOElJ+K0lJ_PXz%byD-~pHJ5Hx@Q+LRBF50KFi)6amB0w;kHK@c`@0C@volXwGwkpd@(BQfxU;P((Y5C9_YHR40*g8kq0K~FbME$z$XE!5IN8Q>RkFwAcHZ3LW5I-+yU&74g +OTA00015INugu;J^U@BtAd>RkFw6oM6k+z>hd0P0-&O+bcGhGK?uhTH+{kx>vj5CE{@>!0ue5hUtd`c0&UwT5#LIxqmh0TCoj;85U)Uztq~l!Ip75IR5rC*lS$5(g>tF%T8N;E@I=O%OUz00_W`;sh`l2XXW<5DdWJk^c}nU;qRs@Zjzb{~zX3`b~r2kl^kRI)DJ?QulS5C8y|S@aN+1tx^%;pq0_-vJ2_J0JiZl8Tk-_krL(z`&6OCdx1p5IZmcxb=(yf#~QihT`}x(BRD%st`MH0K%WZ0TCpRSD8(plaG@Qlq?WCfB->ffRP0zk6(b31tyKRp0|t;JP-gQC4u1R8u17N=o8=qzyj!Y5Iv9p{?CKrkl+rKGiZR5@+FXy_Yggx0P`gkG12fq^bs(b@M!Q9FIjCS%$-5I&#)fRP9$ACixgh2@*zQpb+vj}Soc01lK5WcDtAkq0LD76|Y|!9fr~pa6&?3G~~=8Nh&%1}FFu1;F7DL9hVsAPMx&z=Gftz<`kiC-@fU5JE5jid>paAmQmP_$}N40gw%nXb?hR0QdpwT>4Gl0UVYSmW+`Tm&Oo6@Bn&=g#iE$5g+&g=2H4iaf6BwLl6KClv!x_0p?QrO@V=rlYoJp5JYeQg5ZFY_a%evoZtn(f#86Xh7d%60QV(}?VR8~+fl?q;85UV?o1FwzyJ`E2qzGd2PY4Z50Lqp{?D!uMqmH|z>a@_ll&(6nf}jUk^Cmr5Jw;Yf#86XBqe~8CnbQBC?$ZC`VdDj03{`WlPM*DlO`pAlPe{Flll-xKmaNwfRipIfRijGfRimHE5HB{M_>SO;K%_ElnrEnkq0Jd7clfZ5JzwTf#86XBqd;zFeTi;hv=N(tq@0`0D<6ulO!d8lP@KJkq0J#lav +OTA00025J#{8FeSjigyEdvpOcT14wU#>`w&Oq0DzM(C3e6=;85UZ;eO!90c;RRAOI;PSHMN!P~d6ee&ELe4wSnPNH74GW{E2y^nu`jlP4ugz((N05J*q}f#86XB_%z;M&MB3X5)U~pb$u40LK9il$U0SDi7N^4P0xYgfRiL8au7)%0DzMvC4iGAC4iGBC4iGCB@GZsFaUs)E+v4GBqfL=3H0~DfRRQJNk9N8C5R&l^zy-j;gaAEls!+n5KEu{fRiI7h3uZ-4wQl&j_jV`f)Gq_03#)U;o{)+;@<%sl8Tk-_hb-F002~$ypj{ZgW!OZBPAb`50i!vP7na$Eb%M{;34BR;!Wva;9=mf5KeFacHoUIkuMIEc?t*MDDO7nClF4M03h!+;zR3S;9=l);EgSjFKrM{Kmd6P1>hj>HR4C?U*KWjcHqhoPrv|q3J{VR0w~}%;zH_Q;9=m^5KsUBv*_zCx8eIPfZ~WQfRP0z*AP%30F5n?FAkJsW{{r10TCpRSC9};&;a}a?pORz5Q2b_BqfL=3G}ZJQ1Ad`W`L0;C5R&l^g_U3;LZWk5K(XdjV+Nc4wPhO5RxS&0pJ0^yAV-;007{CktQXGBPsC!BSG*%;HeN%&;TYS0pNg?O55CR60pNg|ti5552pRgeJ9zX9NYkuN2PBMJ0PzyKp*5LPe%WM&YOFC_usfRQjIk3R|Ym=IP_0Ff^alw@WAkuW8Ikq0IS6WsB0Qe@5k@+T|k@_aEk^3eWgd1oOSdajVqlx8=qaLCiqJWY7CV_C85L+Ms4wPM64wNojaFg*RV3Y7A&JbHL0H~FKku@cd-~pC{aG587lkgB*Kmau*K#?&eP?0hvV39K=aFJ3FTTlQrB~M>pPj63uPe79~B~X+25L;jXG9_S>GbM17G$r|&{?C(@un=2-0DzG-C6M5gm52|SABYc`A9N5~kN^&pE@;4zF(uHDG9}=UGbN}HTc7~&kTfN16@3Mc1)l}s@Py&45L>_i{?9;>F(puuG9_SL!N}T>t=M1CWvGCYd97@rWZ~@GF`X5M96k2q%w|fRP6$kCTDrk>Q_{7Z6?G0Eq{I5d*-E<&ohCz<`klC*lxY@BomJ2q%dvS@etMk>Q_{kCPh^UJwB0CV>$Pz=h|L;h&R_lMa+c5MF=))+Nx8*Cl|F4JVM14kySKLJ(fC0G2L*kqsx1kq##a7a0dI^&b +OTA00045MLkwuPvS}jV+Lq*Cl|H)+Nqgwh&*S0Qd@kkqIY|kqRfEkqalVk+=|FumB7v$QIcK>IVA;81TU?(JT%SU%&u%0f3PWCy5RwQd5RwNcXAod;0M;dtk=G@N4VfJvkqsv>krxnPfB+6BC>J>gSoDbDlHs3|kCWgKV4wgoB@mG^B_NSAB`}dRCE+Y`5MaOnR~rD4@+Am^gqCWksAQlL#k(lLsfI5Ml5DkFzK*B5=|!f&Kc=V37wW4iI7h0C15ACxzgU;0}~|XpJq*Um*};PymuI!Y|KpP~a#31;F|N=1>3-VqgIJO~8Wlz`+siz`6mn;ARi65MpoucOSZy;rIIi>RkFw0D%#K#SmiP0RJE6Qu<8+knn-vEds#c;D8Wf@Br}OhyNPD+X1Rvx=kFGjgYnwV=w@!T)IsdmW_~|kgbrskQr-2J;5g@P_{(z}o?;T)It|5M(d_`T^!p`b~g?3BdXR=1}@gpb%t001$!%!1@8^Q2I^4gZcsHPzn%aPyqT(fPwk}=wA9wAc2Axx$ub)WN-kGk?JOq5eLAKkpw1~G#K<>5M+P=k>HS%>n4DM0>FUaC(qB3>=0zI0PYX}ALdf}O(20Wfq(CE4*(Ek@BsJ$z=O|`ACV7{2$g`5>?ZIKWdHzy5g)*SkrV=f5gWjOk?kg85M>Ymfe{zLfRQ65fe{nHfRXAZGZ1AU0D%z?z<`kkCV>$Pz<`nKCQb +OTA00055M?j`fe{D5kdf&of#8u51HgdbtPo|80QN2yFUBv|FOMyeFOZY*CE*Zdpa6iA@FgFU|B{5~lHs3|kCRRiW#9ly>*Ov%;q)$5;8iYwk?D0PrPnlkp{glW&vXlFyP3l#~!=Pypy!50DR#50DR#fRXSekdZYIW?%sECB!@_;L-=U^o2Yr;F1R;5NH4Z4wPM64wNojV3Y7AaFg*R;Sgz100<@yl(QKRkPncG;E>=9l;aR-Z~!nRh~ksspOcT14wUFxFYk^JYCr&hkq#$@l^>FilY!)u;h&QS5Ncom50HS7R;fp0mA_flz9rMm0J*SAOMNr1Hg*l0l%kz)YKSz(o*qumBH`50DR#W#IM_Kfpkew@`5Ok0L4wUFx50DR#`I-LDV37zWOb~SN00$>=Blv!x`nPd=k5CHzqV3GVLfZ~wg4wOe|`I*)bbwB_R;6RZFCKMOQ^!b_o&w(#j5Or_>id>pa4wN`(A7A6o`I-LDZ4h;U08rpW4^FTi2qsDpcJKfQCxDX&Ck((4k_aadk_RWP5O<&e4wNQngfJNNfRp?t2*CN7S`c@z0RGQlk^Clr;*j7DlxAq20SE +OTA00065O>f3(JX+G2PW7S2=J`HfRP3#-Vk@-0FaRcCz&LZl-U*$@YoWOEYc8oumG7KDwW~)id>pa4wPMJ4#0O1c)$SI0fgYH54#_VmErfQT)Iu<5P1Lq3&4ahkp)2X01pu#_yOipz7ToP0O(qPkq9Oaw)m9ZzYo66zuypg000k=fRP9$ACixgjOCQ!pOdW+dLRH9@Qmk};0C~ekq9P=FhKNg5PDz$pOcT14wUFxfRP9%kCT9rYY=*H00$?JlYo(l2OpA;lZWS&;a?DbZ~!$W_yJ_#_MYGk@f{H)fPtnEet-axfg*u3fq;`SC6JRcB_NaY5PpyVGbJ#SG$kLB|B^qGFO#2>W)OdH043q_E`X5-CKwk8@Ls?nF9;BSfB-Py`2p?^{~zX3`b~j>kCQ17f1m*BT>4Gl0UVYSmW+`TmyeSm5P%Q>zySlFkPQ(ehhLdZpOcT13lM4z$6fXFaYxjV3YVJaFh8afRp+rkdwL)fj|KJCinp$fgh4DlOcg0k}s3&5P@(2zml(#4wU#>fRXqnkdgT&3J`&S0HBfjCa{tFCJ(mwl-|D&zJU;d@BpD5u^oVu_$H8(`6i%~`X((9f&c)pllvyWldqGXlaG^u=9S?^5P~oO0w;kHD!_n|1SXGPfRh9!GZ2D60Ejd=@PLs4Cx{~%@PXiflj#tGPyhlafRP9$4wSPQK#>F{VBif9f?xn};DF$e;0~1ZHGq)>CWtgF5Q1<35bzL^1SWuy1SWwIF~ES4V-SLX00Jk7BO&mB-~_`Ek^(1ykp>WgkN^QEh$A8JfRP9$4wSPQZRGqAg5UtCm7V1I5(L12kpw1z5zEh-5QAU<5R&F550DR#50DR#K`b*Q9}t6(015QXz<`kz0)UhDB!H3TCgBi+&;Woy;r9T*001hOfRXeikdZhLgWv%5C4e%KKrrwSlJzAJlJq565QGo_j4XhY<|e3>8o+?y4wQH-$Pk1;09{)SlrCIwlkp{Blkg=#kyZ +OTA00075QI@6_5QLBbFp=mcfRTCwkdb=>C>J>gVGx9%04Vf;k>)0W5!k^;z(EoD&qol1zyJ`E=q3=7=O%!W_9TcSN$@@pg)jgCCxI{t^e)e|&w!DM2Z$q?5QSg>+CU(Y2PZI*2q%D%4JVM1yby(O01hW87dZzQ@Q&$~;c38u;K&e#pa2agkdY222p1U#2=Jo7fRUFFh7bTikqIYIkqReZkqakqkqjsB5QZ=S@+1(F^CS?G^dx|h4JVM1OAv-Y01hVsmk1Xb2MF}1&x`7n;lU7wZ~(C#K#>V2P>~8JV37+aaFO*8hJXMJCrB4r2Wkg<2MF}0&#^4G5QiWDT>*j)lOCZQu^d2=2`5mIJ`jg60178ykqakqkqjq<7L^932G9_PzyM&82Pbfm2q$k}e@}3e2q%mXhtL3ElLsgHnf}j(;E>=BlzC{u5Qs1U0Vg356~K=_@9&-miSUq<91w^=001X|lm8}wk^Lr+k^UwDmbnm!PympV{w9Ev{U!sz{p9}=4!{@?h+qJKkpU-tvwC-M-8@BsNGK@kPOfRO?Z2MfRXJcgD`IpiLd|w07IC7k>@6mk?1CfKK<|A5Q)G5p9d)P0hW-H=q7-Z=O%!W>=2320Q4o0k@Y2jG9loS2PpIbmP8PV-~f=5^(BCl^d*3i0w;(gG4L1=iVy()Bp{LbB_bo4@PLv2B#@Cq5Q;DWkLC`P>ko({8SsMOfRp(p-VlmF0DzJ1C6JNtB>|R@lkX*flk5 +OTA00085QrVxs701uE4kPna#kPnc6k@qEtBLfhMfB?zxfRXkkk6(b3_9Y*ZkCPG*il6`wkPnc6k@h8xFd*;{lJ+Ii5Q?w>5R&&LAd&7RFp=*ifRX+rED(yo0FaUYBq$d-2l((1lJ6xDlA{oc&;afwfRO?PL5CG~Xh$BJqhT@jtpOcT14wRD+iy#2#T7Z%2CJ&Ggkc2Qf^n&AF5Q{JXmf@e1kCP6R_*xFYfRXAZhY*Wk01%SuCIrHOk?SUiBZ=_LXi4wU#>f#hBgi+})r+JKSkCJ&Ggkbx2M&+ox15Q~rifRX7YhA;{67r_vc=_ZNj3=oW<0FRRnl=xaekpw3XkPna#kkt^4008?WaNvO8kl-NbK#>F|4wQZnjSv7EF%OUrkbsftCWs@c@POb25REVZkdgZ&i4z0B5R&O80>FTi4iJq{0EXfCE(_}t;ee6pCV>$L!6OijzyJ@B50KAa$j`u$BPGlu3G^EfjnDvZzz~unCB!fZ@Ik@Bo03|0a-;04I=>*Cl|H)+NUfj}QO`CV-I@0*E6C^uxeP;85Ur5RX6r&p%)9)dxxN(396Cz?0S`0}zi;0DzJCB#&Q!lldfokp?D#5%v&|U;qZd5R&;M5R(2T5R&~SfZ&l3kDvfxllLTmk?U0V*6E?j_-@FkFu@g)zCfDn+d01lLXGKeF&@PLu+CWs?B@Dc +OTA00095RkwChU=5ykdxUZfRorI3&4PpuMm*X02Bg&5eLA5;DD2U0}ztgC5#Y|-~bSk*d_4GfRTO!fiTJRh2*^ukq`hV2oRDYB@mL?B@mL>B?y&T5Ro7NfRWfGkdfIX0hW-H*(HFJM-Y)P0N5pnK9fLL@PXiflYRq)yCp!4V61ok`N~mz=kkM@LLd*KmY;2fZ>3X5GMk_5RwoliGd6dlTZMUF+uf^evmXN^?;EOCx|075R-5KkdqE4fRha;2`&H(stdadkPwrA0J<-!FPJZgCxDX)Cy6AoWKB=;j!I_Fgf&2;ZWdei7N^4mJpoq0KCA8np0EH=;A-S|;?4mMl!Imhz!6YR5TCFBfPsM#B)~-GP~d9hcjAxawh*7d0GQ!1+ya-8^l0sT;ELpz;#Cl!5C8zc000)4fRRQgi#HMU43zB>r+4up3fj|(YKmf)84wPMHMCMT7YUFp~i05w*rEma&<(J|Pl$U0SE0OR;;85Vx5T(EX_yLB2k6(s^MCMT7YUFp~lMtoQ0D|b5;V9Wi;85Ug=y~CaY3q$5T;N7>evUDQ1eFMP~c_jdEtuW=@6!H0C3=d<(J|PlnrF~0b_^%@5m6QzyNCGcjAKTnc=S3PnQVvNWhH{rqBSc0YP40Pi*gb;fmy!;trHR5T}3uMCMT7YUFp~i0ql+OxQ)2+7PFZ00{JGz(?Rv;9>1~;fmy!;;#^=umEiDdEtuWm*NhT%4V(sL0;VusBi$`_W-~E07T|c;A-S|;)(6a5U7vpCisYB#4wRQ>i7V;#>=3A+0ImT+USCga?|I>h7_Dl$U0SD+%%6l9Ldw5CB8qP~dFmd*O!Vm*NhTmu9UHt}p=LK;icQzyJV%kr5{!l8=)<5Ux-F4wQapMCMT7YUFp~%K;9Q{1C2S0253{;85UV>3iXd88`fB;6oMCMT7YUFp~g5j6q4wO9*uCM?AzyJVB;85Ud>3iXd&5U$Vwgyomw4wMaKAdwL#FyKJoWDu_q0MC-2l8=&#AOL=6Mc`22VBm1zisYB#4wTLiuYdrLlZ)z`;h&R_lZxb*;-8a25U-E`kCP6RerJm0m*Q&E|L-8@?+~%z05Cw|_W-~E07T|c;A-S|;#Lr{003*^eBp}Zm*NhTmu878QS=oMvJe13;ZWdW<#*!90fStgPY#p@5VCLp;r9T*002bhP~d9hcjAuY#1OK80G#0tloL!3mr(FQ;ZWde%FhJq=0KfnMMBz~2YUEQ8vj6~4;A-S|;>ZDwQJ+r^l!InQ5VK$aP~d9hcjAQRoZ$|X6HG;yLJ+fX0HE+l;85Uh=6vCbfrv+w`_zyJV5=1|~j0C4d{=1|~jJ$xhH^=gb>N#0JtYV?=SD4?+w5};4t7I;E@ +OTA000C5YBJ_fRV5#kdd(`4wOq>-;vLew-C=@0I(;Jk+CNXluKRu0jeh`Foh7)q5uE@0000000062AORN&0R=G(?IHkXZ*FBe083C#IsgFRor^kx?6 { + override fun customize(factory: TomcatServletWebServerFactory) { + val connector = Connector(Http11NioProtocol::class.java.name) + connector.scheme = "http" + connector.port = webServerProperties.port + factory.addAdditionalTomcatConnectors(connector) + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/config/WebServerProperties.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/config/WebServerProperties.kt new file mode 100644 index 00000000..37bf9e2b --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/config/WebServerProperties.kt @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "config.web-server") data class WebServerProperties(val port: Int) diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/FirmwareController.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/FirmwareController.kt new file mode 100644 index 00000000..cf59199d --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/FirmwareController.kt @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.controller + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gxf.crestdeviceservice.firmware.exception.FirmwareException +import org.gxf.crestdeviceservice.firmware.service.FirmwareService +import org.gxf.crestdeviceservice.service.FirmwareProducerService +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.servlet.mvc.support.RedirectAttributes + +@Controller +@RequestMapping("/web/firmware") +class FirmwareController( + val firmwareService: FirmwareService, + val firmwareProducerService: FirmwareProducerService, +) { + private val logger = KotlinLogging.logger {} + private val redirectUrl = "redirect:/web/firmware" + + @GetMapping + fun showUploadForm(model: Model): String { + return "uploadForm" + } + + @PostMapping + fun handleFileUpload(@RequestPart("file") file: MultipartFile, redirectAttributes: RedirectAttributes): String { + if (file.originalFilename.isNullOrEmpty()) { + redirectAttributes.setMessage("No file provided") + return redirectUrl + } + + redirectAttributes.addFlashAttribute("filename", file.originalFilename) + + if (file.isEmpty) { + redirectAttributes.setMessage("An empty file was provided") + return redirectUrl + } + try { + logger.info { "Processing firmware file with name: ${file.originalFilename}" } + + val firmwares = firmwareService.processFirmware(file) + firmwareProducerService.send(firmwares) + + logger.info { "Firmware file successfully processed" } + redirectAttributes.setMessage("Successfully processed ${firmwares.firmwares.size} firmware packets") + } catch (exception: FirmwareException) { + logger.error(exception) { "Failed to process firmware file" } + redirectAttributes.setMessage("Failed to process file: ${exception.message}") + } + return redirectUrl + } + + private fun RedirectAttributes.setMessage(message: String) { + this.addFlashAttribute("message", message) + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/MessageController.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/MessageController.kt index 9bbfc1da..106b09be 100644 --- a/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/MessageController.kt +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/controller/MessageController.kt @@ -39,8 +39,8 @@ class MessageController( urcService.interpretURCsInMessage(identity, body) val downlink = downlinkService.getDownlinkForDevice(identity, body) return ResponseEntity.ok(downlink) - } catch (e: Exception) { - logger.error(e) { + } catch (exception: Exception) { + logger.error(exception) { "Exception occurred while interpreting message from or creating downlink for device $identity" } return ResponseEntity.internalServerError().build() diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerService.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerService.kt new file mode 100644 index 00000000..0a3dd00b --- /dev/null +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerService.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.service + +import com.alliander.sng.Firmwares +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.avro.specific.SpecificRecordBase +import org.gxf.crestdeviceservice.config.KafkaProducerProperties +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Service + +@Service +class FirmwareProducerService( + private val kafkaTemplate: KafkaTemplate, + private val kafkaProducerProperties: KafkaProducerProperties +) { + private val logger = KotlinLogging.logger {} + + fun send(firmwares: Firmwares) { + logger.info { "Sending firmwares to Maki" } + kafkaTemplate.send(kafkaProducerProperties.firmware.topic, kafkaProducerProperties.firmware.key, firmwares) + } +} diff --git a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt index 515bc63b..0afaf041 100644 --- a/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt +++ b/application/src/main/kotlin/org/gxf/crestdeviceservice/service/UrcService.kt @@ -62,7 +62,7 @@ class UrcService( val commandsInProgress = commandService.getAllCommandsInProgressForDevice(deviceId) return try { commandsInProgress.first { command -> downlinkConcernsCommandType(downlink, command.type) } - } catch (e: NoSuchElementException) { + } catch (exception: NoSuchElementException) { null } } diff --git a/application/src/main/resources/application-dev.yaml b/application/src/main/resources/application-dev.yaml index 329745b2..8fc44f47 100644 --- a/application/src/main/resources/application-dev.yaml +++ b/application/src/main/resources/application-dev.yaml @@ -19,9 +19,18 @@ mutual-tls: truststore: certificate: "classpath:ssl/dev-proxy-cert.pem" +# port for proxy, using mutual TLS server: port: 9000 +config: + http: +# url: "http://localhost:9001" +# connection-timeout: "5000ms" + # port for web client + web-server: + port: 9001 + kafka: consumers: pre-shared-key: @@ -33,6 +42,9 @@ kafka: topic: "crest-message" command-feedback: topic: "command-feedback" + firmware: + topic: "firmware" + key: "firmware" database: # This key was used to encrypt the data in db/migration/test-data/V3__test_data.sql @@ -50,3 +62,7 @@ psk: -----END PRIVATE KEY----- message: max-bytes: 1024 + +logging: + level: + org.gxf.crestdeviceservice: debug diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index c9cab6e0..ec733ee9 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -2,7 +2,7 @@ # #SPDX-License-Identifier: Apache-2.0 -# The crest device service should always require mutual tls +# Port for proxy should always require mutual tls server: ssl: enabled: true diff --git a/application/src/main/resources/db/migration/V10__firmware_version.sql b/application/src/main/resources/db/migration/V10__firmware_version.sql new file mode 100644 index 00000000..736b659e --- /dev/null +++ b/application/src/main/resources/db/migration/V10__firmware_version.sql @@ -0,0 +1,5 @@ +alter table firmware + drop column hash; + +alter table firmware + add column version varchar(255) not null; \ No newline at end of file diff --git a/application/src/main/resources/static/web.css b/application/src/main/resources/static/web.css new file mode 100644 index 00000000..5e4c3a93 --- /dev/null +++ b/application/src/main/resources/static/web.css @@ -0,0 +1,131 @@ +:root { + --li-primary: #821e7d; + --li-secondary: #008cbe; + --li-blue: #008cbe; + --li-purple: #322882; + --li-magenta: #821e7d; + --li-orange: #e56c00; + --li-red: #af1a1d; + --li-green: #7db43c; + --li-green-lighter: #95dc45; + --li-lime: #88b986; + --li-brown: #642d32; + --li-black: #2d2d2d; + --li-white: #fff; + --font-family-sans-serif: Source Sans Pro, apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji, Helvetica Neue, sans-serif; + --border-radius: 0.3rem; +} + +/* default */ +* { + padding: 0; + margin: 0; +} + +*, +*:before { + box-sizing: border-box; +} + +html, +body { + position: relative; + margin: 0; + padding: 0; + min-height: 100vh; + max-width: 100vw; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +/* typografy */ +body { + font-family: var(--font-family-sans-serif); + color: var(--li-black); + font-size: 16px; + letter-spacing: 0; + line-height: 21px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: inherit; + font-weight: 500; + line-height: 1.2; + margin-bottom: 0.5rem; + margin-top: 0; +} + +body { + background-color: var(--li-green); + background-image: linear-gradient(45deg, var(--li-green) 0%, var(--li-green-lighter) 100%); +} + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + color: var(--li-black); + word-wrap: break-word; + background-color: var(--li-white); + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.175); + border-radius: var(--border-radius); + padding: 1rem; + margin: 1rem; + width: 500px; +} + +.container { + padding-right: 1rem; + padding-left: 1rem; + margin-right: auto; + margin-left: auto; +} + +/* file upload button */ +input[type="file"]::file-selector-button { + border-radius: var(--border-radius); + padding: 0 16px; + height: 40px; + cursor: pointer; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.16); + box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05); + margin-right: 16px; + transition: background-color 200ms; +} + +/* file upload button hover state */ +input[type="file"]::file-selector-button:hover { + background-color: #f3f4f6; +} + +/* file upload button active state */ +input[type="file"]::file-selector-button:active { + background-color: #e5e7eb; +} + +button[type=submit] { + width: 100%; + background-color: var(--li-green); + color: var(--li-white); + padding: 1rem 1rem; + margin: 1rem 0 0 0; + border: none; + border-radius: var(--border-radius); + cursor: pointer; +} diff --git a/application/src/main/resources/templates/uploadForm.html b/application/src/main/resources/templates/uploadForm.html new file mode 100644 index 00000000..944a0121 --- /dev/null +++ b/application/src/main/resources/templates/uploadForm.html @@ -0,0 +1,40 @@ + + + + + + + KOD + + + + + +
+
+
+

File to upload:

+
+ +
+ +
+
+ + + + + + + + + +
Message:
Filename:
+
+
+
+
+ + diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFileFactory.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFileFactory.kt new file mode 100644 index 00000000..63c7ebbc --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFileFactory.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import org.springframework.core.io.ClassPathResource +import org.springframework.mock.web.MockMultipartFile + +object FirmwareFileFactory { + fun getFirmwareFile(): MockMultipartFile { + val fileName = "RTU#FULL#TO#23.10.txt" + val firmwareFile = ClassPathResource(fileName).file + return MockMultipartFile("file", firmwareFile.name, "text/plain", firmwareFile.readBytes()) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwaresFactory.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwaresFactory.kt new file mode 100644 index 00000000..beae273f --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/FirmwaresFactory.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import com.alliander.sng.Firmware +import com.alliander.sng.FirmwareType +import com.alliander.sng.Firmwares +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_FROM_VERSION +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_NAME +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_VERSION +import org.gxf.crestdeviceservice.TestConstants.NUMBER_OF_PACKETS + +object FirmwaresFactory { + fun getFirmwares() = Firmwares.newBuilder().setFirmwares(listOf(firmware())).build() + + private fun firmware() = + Firmware.newBuilder() + .setName(FIRMWARE_NAME) + .setType(FirmwareType.device) + .setVersion(FIRMWARE_VERSION) + .setFromVersion(FIRMWARE_FROM_VERSION) + .setNumberOfPackages(NUMBER_OF_PACKETS) + .build() +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt index 91e477ba..d91a87ed 100644 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt @@ -8,7 +8,18 @@ import java.util.UUID object TestConstants { const val DEVICE_ID = "device-id" - const val MESSAGE_RECEIVED = "Command received" val CORRELATION_ID = UUID.randomUUID() val timestamp = Instant.now() + + const val DEVICE_MESSAGE_TOPIC = "device-message" + const val COMMAND_FEEDBACK_TOPIC = "command-feedback" + const val FIRMWARE_TOPIC = "firmware-topic" + const val FIRMWARE_KEY = "firmware-key" + + const val MESSAGE_RECEIVED = "Command received" + + const val FIRMWARE_VERSION = "99.99" + const val FIRMWARE_FROM_VERSION = "23.10" + const val FIRMWARE_NAME = "RTU#DELTA#FROM#$FIRMWARE_FROM_VERSION#TO#$FIRMWARE_VERSION" + const val NUMBER_OF_PACKETS = 13 } diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/config/ApiAccessFilterTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/config/ApiAccessFilterTest.kt new file mode 100644 index 00000000..1778a7c5 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/config/ApiAccessFilterTest.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.config + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.boot.autoconfigure.web.ServerProperties +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse + +class ApiAccessFilterTest { + private val serverProperties = mock() + private val filter = ApiAccessFilter(serverProperties) + private val proxyPort = 9000 + + @BeforeEach + fun setup() { + whenever(serverProperties.port).thenReturn(proxyPort) + } + + @ParameterizedTest + @CsvSource( + "9000, /psk, 200", + "9000, /sng, 200", + "9000, /error, 200", + "9000, /web/firmware, 404", + "9000, /web/firmware/api, 404", + "9000, /wbe/firmware/api, 404", + "9001, /psk, 404", + "9001, /sng, 404", + "9001, /error, 200", + "9001, /web/firmware, 200", + "9001, /web/firmware/api, 200", + "9001, /wbe/firmware/api, 404", + "8080, /psk, 404", + ) + fun shouldReturnTheRightStatusCodeForRequestOnPort(port: Int, uri: String, expectedHttpCode: Int) { + val chain = MockFilterChain() + val request = MockHttpServletRequest() + request.serverPort = port + request.requestURI = uri + val response = MockHttpServletResponse() + + filter.doFilter(request, response, chain) + assertThat(response.status).isEqualTo(expectedHttpCode) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/FirmwareControllerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/FirmwareControllerTest.kt new file mode 100644 index 00000000..0b77711f --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/FirmwareControllerTest.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.controller + +import org.gxf.crestdeviceservice.FirmwareFileFactory +import org.gxf.crestdeviceservice.FirmwaresFactory.getFirmwares +import org.gxf.crestdeviceservice.firmware.service.FirmwareService +import org.gxf.crestdeviceservice.service.FirmwareProducerService +import org.junit.jupiter.api.Test +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(FirmwareController::class) +class FirmwareControllerTest { + @Autowired private lateinit var mockMvc: MockMvc + + @MockBean private lateinit var firmwareService: FirmwareService + + @MockBean private lateinit var firmwareProducerService: FirmwareProducerService + + @Test + fun shouldProcessFirmwareAndSendAllFirmwares() { + val firmwareFile = FirmwareFileFactory.getFirmwareFile() + val firmwares = getFirmwares() + + whenever(firmwareService.processFirmware(firmwareFile)).thenReturn(firmwares) + + mockMvc.perform(multipart("https://localhost:9001/web/firmware").file(firmwareFile)).andExpect(status().isFound) + + verify(firmwareService).processFirmware(firmwareFile) + verify(firmwareProducerService).send(firmwares) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/DeviceCredentialsControllerTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/PskControllerTest.kt similarity index 80% rename from application/src/test/kotlin/org/gxf/crestdeviceservice/controller/DeviceCredentialsControllerTest.kt rename to application/src/test/kotlin/org/gxf/crestdeviceservice/controller/PskControllerTest.kt index 2c6800a2..624d5b02 100644 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/DeviceCredentialsControllerTest.kt +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/controller/PskControllerTest.kt @@ -15,21 +15,23 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers @WebMvcTest(PskController::class) -class DeviceCredentialsControllerTest { +class PskControllerTest { - @Autowired private lateinit var mvcRequest: MockMvc + @Autowired private lateinit var mockMvc: MockMvc @MockBean private lateinit var pskService: PskService @MockBean private lateinit var metricService: MetricService + private val url = "https://localhost:9000/psk" + @Test fun shouldReturn404WhenPskForIdentityIsNotFound() { val identity = "identity" whenever(pskService.getCurrentActiveKey(identity)).thenReturn(null) - mvcRequest - .perform(MockMvcRequestBuilders.get("/psk").header("x-device-identity", identity)) + mockMvc + .perform(MockMvcRequestBuilders.get(url).header("x-device-identity", identity)) .andExpect(MockMvcResultMatchers.status().isNotFound) } @@ -38,8 +40,8 @@ class DeviceCredentialsControllerTest { val identity = "identity" whenever(pskService.getCurrentActiveKey(identity)).thenReturn("key") - mvcRequest - .perform(MockMvcRequestBuilders.get("/psk").header("x-device-identity", identity)) + mockMvc + .perform(MockMvcRequestBuilders.get(url).header("x-device-identity", identity)) .andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.content().string("key")) } diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerServiceTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerServiceTest.kt new file mode 100644 index 00000000..c4f02e11 --- /dev/null +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/FirmwareProducerServiceTest.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.service + +import org.apache.avro.specific.SpecificRecordBase +import org.gxf.crestdeviceservice.FirmwaresFactory +import org.gxf.crestdeviceservice.TestConstants.COMMAND_FEEDBACK_TOPIC +import org.gxf.crestdeviceservice.TestConstants.DEVICE_MESSAGE_TOPIC +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_KEY +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_TOPIC +import org.gxf.crestdeviceservice.config.KafkaProducerProperties +import org.gxf.crestdeviceservice.config.KafkaProducerTopicKeyProperties +import org.gxf.crestdeviceservice.config.KafkaProducerTopicProperties +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.kafka.core.KafkaTemplate + +@ExtendWith(MockitoExtension::class) +class FirmwareProducerServiceTest { + @Mock private lateinit var mockedKafkaTemplate: KafkaTemplate + + private val kafkaProducerProperties = + KafkaProducerProperties( + KafkaProducerTopicProperties(DEVICE_MESSAGE_TOPIC), + KafkaProducerTopicProperties(COMMAND_FEEDBACK_TOPIC), + KafkaProducerTopicKeyProperties(FIRMWARE_TOPIC, FIRMWARE_KEY)) + + @Test + fun shouldCallMessageProducerWithCorrectParams() { + val firmwareProducerService = FirmwareProducerService(mockedKafkaTemplate, kafkaProducerProperties) + val firmwares = FirmwaresFactory.getFirmwares() + + firmwareProducerService.send(firmwares) + verify(mockedKafkaTemplate).send(FIRMWARE_TOPIC, FIRMWARE_KEY, firmwares) + } +} diff --git a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/MessageProducerServiceTest.kt b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/MessageProducerServiceTest.kt index 85422fc6..d751332e 100644 --- a/application/src/test/kotlin/org/gxf/crestdeviceservice/service/MessageProducerServiceTest.kt +++ b/application/src/test/kotlin/org/gxf/crestdeviceservice/service/MessageProducerServiceTest.kt @@ -6,7 +6,12 @@ package org.gxf.crestdeviceservice.service import com.fasterxml.jackson.databind.ObjectMapper import org.apache.avro.specific.SpecificRecordBase import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.TestConstants.COMMAND_FEEDBACK_TOPIC +import org.gxf.crestdeviceservice.TestConstants.DEVICE_MESSAGE_TOPIC +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_KEY +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_TOPIC import org.gxf.crestdeviceservice.config.KafkaProducerProperties +import org.gxf.crestdeviceservice.config.KafkaProducerTopicKeyProperties import org.gxf.crestdeviceservice.config.KafkaProducerTopicProperties import org.gxf.sng.avro.DeviceMessage import org.junit.jupiter.api.Test @@ -22,10 +27,11 @@ class MessageProducerServiceTest { @Mock private lateinit var mockedKafkaTemplate: KafkaTemplate - private val deviceMessageTopic = "device-message" private val kafkaProducerProperties = KafkaProducerProperties( - KafkaProducerTopicProperties(deviceMessageTopic), KafkaProducerTopicProperties("command-feedback")) + KafkaProducerTopicProperties(DEVICE_MESSAGE_TOPIC), + KafkaProducerTopicProperties(COMMAND_FEEDBACK_TOPIC), + KafkaProducerTopicKeyProperties(FIRMWARE_TOPIC, FIRMWARE_KEY)) @Test fun shouldCallMessageProducerWithCorrectParams() { @@ -39,7 +45,7 @@ class MessageProducerServiceTest { messageProducerService.produceMessage(jsonNode) verify(mockedKafkaTemplate) .send( - check { assertThat(it).isEqualTo(deviceMessageTopic) }, + check { assertThat(it).isEqualTo(DEVICE_MESSAGE_TOPIC) }, check { assertThat((it as DeviceMessage).payload).isEqualTo(jsonNode.toString()) }) } } diff --git a/application/src/test/resources/RTU#FULL#TO#23.10.txt b/application/src/test/resources/RTU#FULL#TO#23.10.txt new file mode 100644 index 00000000..c42df97c --- /dev/null +++ b/application/src/test/resources/RTU#FULL#TO#23.10.txt @@ -0,0 +1,13 @@ +OTA00004Ny+G|NeoXu!zMI*ungbnQwylZQJ20!QA=&G(L+*5CO0Nu?N}rC*rK{yx{@B9g?g&9}oyo0Jv!VL0p$<~pa8sqK#{a3P?5DKV3D>baFNpx2(SRRCxjc73#JRV3yFi8gR6s45D5SP&9~mSYyo=#;E}o~@R7SGFc1k405F#+@MP}OAOIil_5nbFQGsHCbAf=7v?pv32`~VVk+mnFk+vtWk+&y=50xKA5Dee|54I1I5hRb3hTwpc=_ZenpAZcI01uK8B#)DylY_B=aFCJiCSMQ@5CD-81;B_S8Ss(dkdy2tfRq0a4IlvSCZCaykq?j$z7LQekQkNt5Dh>8g8PUkfccP-?a00Y32;ELw%*4WIyl5e&eD;0M4>n+U*wk?JPt5Dl;Zfe{42h2Vgb>Lw|gK#>L~ln@QT0EOU};trHkGXIh9k-w3zk$w;mPym3D_a%W52f$yL5Rw%Ff#CTN5O4s3;DD3&B`TOJm?@Z9nt+j&5D<_6fRX7Yfe{wK4wQEikl=uo7!V~807uV&;0Tp~kw+(nm0Zn;4^9vzAOL`qM<-yDNhd&)NGAxD|B_b_B|rcOFYu5^ClHZIC*s_J5fH#M5G7CmfRRTh1KoiU3&0;K5NIIc{SYO90AS#7;L8Cdz<`lQCy!s|+}sc)umB5GDWsfRRZjlHkdofDI8OVc>A!91t?l0DzJ7CXkW!CZLh_Ca{tBCfpD*-~fp$iYtpNA@v}V^d>Np^(FuiGVlOElJ+K0lJ_PXz%byD-~pHJ5Hx@Q+LRBF50KFi)6amB0w;kHK@c`@0C@volXwGwkpd@(BQfxU;P((Y5C9_YHR40*g8kq0K~FbME$z$XE!5IN8Q>RkFwAcHZ3LW5I-+yU&74g +OTA00015INugu;J^U@BtAd>RkFw6oM6k+z>hd0P0-&O+bcGhGK?uhTH+{kx>vj5CE{@>!0ue5hUtd`c0&UwT5#LIxqmh0TCoj;85U)Uztq~l!Ip75IR5rC*lS$5(g>tF%T8N;E@I=O%OUz00_W`;sh`l2XXW<5DdWJk^c}nU;qRs@Zjzb{~zX3`b~r2kl^kRI)DJ?QulS5C8y|S@aN+1tx^%;pq0_-vJ2_J0JiZl8Tk-_krL(z`&6OCdx1p5IZmcxb=(yf#~QihT`}x(BRD%st`MH0K%WZ0TCpRSD8(plaG@Qlq?WCfB->ffRP0zk6(b31tyKRp0|t;JP-gQC4u1R8u17N=o8=qzyj!Y5Iv9p{?CKrkl+rKGiZR5@+FXy_Yggx0P`gkG12fq^bs(b@M!Q9FIjCS%$-5I&#)fRP9$ACixgh2@*zQpb+vj}Soc01lK5WcDtAkq0LD76|Y|!9fr~pa6&?3G~~=8Nh&%1}FFu1;F7DL9hVsAPMx&z=Gftz<`kiC-@fU5JE5jid>paAmQmP_$}N40gw%nXb?hR0QdpwT>4Gl0UVYSmW+`Tm&Oo6@Bn&=g#iE$5g+&g=2H4iaf6BwLl6KClv!x_0p?QrO@V=rlYoJp5JYeQg5ZFY_a%evoZtn(f#86Xh7d%60QV(}?VR8~+fl?q;85UV?o1FwzyJ`E2qzGd2PY4Z50Lqp{?D!uMqmH|z>a@_ll&(6nf}jUk^Cmr5Jw;Yf#86XBqe~8CnbQBC?$ZC`VdDj03{`WlPM*DlO`pAlPe{Flll-xKmaNwfRipIfRijGfRimHE5HB{M_>SO;K%_ElnrEnkq0Jd7clfZ5JzwTf#86XBqd;zFeTi;hv=N(tq@0`0D<6ulO!d8lP@KJkq0J#lav +OTA00025J#{8FeSjigyEdvpOcT14wU#>`w&Oq0DzM(C3e6=;85UZ;eO!90c;RRAOI;PSHMN!P~d6ee&ELe4wSnPNH74GW{E2y^nu`jlP4ugz((N05J*q}f#86XB_%z;M&MB3X5)U~pb$u40LK9il$U0SDi7N^4P0xYgfRiL8au7)%0DzMvC4iGAC4iGBC4iGCB@GZsFaUs)E+v4GBqfL=3H0~DfRRQJNk9N8C5R&l^zy-j;gaAEls!+n5KEu{fRiI7h3uZ-4wQl&j_jV`f)Gq_03#)U;o{)+;@<%sl8Tk-_hb-F002~$ypj{ZgW!OZBPAb`50i!vP7na$Eb%M{;34BR;!Wva;9=mf5KeFacHoUIkuMIEc?t*MDDO7nClF4M03h!+;zR3S;9=l);EgSjFKrM{Kmd6P1>hj>HR4C?U*KWjcHqhoPrv|q3J{VR0w~}%;zH_Q;9=m^5KsUBv*_zCx8eIPfZ~WQfRP0z*AP%30F5n?FAkJsW{{r10TCpRSC9};&;a}a?pORz5Q2b_BqfL=3G}ZJQ1Ad`W`L0;C5R&l^g_U3;LZWk5K(XdjV+Nc4wPhO5RxS&0pJ0^yAV-;007{CktQXGBPsC!BSG*%;HeN%&;TYS0pNg?O55CR60pNg|ti5552pRgeJ9zX9NYkuN2PBMJ0PzyKp*5LPe%WM&YOFC_usfRQjIk3R|Ym=IP_0Ff^alw@WAkuW8Ikq0IS6WsB0Qe@5k@+T|k@_aEk^3eWgd1oOSdajVqlx8=qaLCiqJWY7CV_C85L+Ms4wPM64wNojaFg*RV3Y7A&JbHL0H~FKku@cd-~pC{aG587lkgB*Kmau*K#?&eP?0hvV39K=aFJ3FTTlQrB~M>pPj63uPe79~B~X+25L;jXG9_S>GbM17G$r|&{?C(@un=2-0DzG-C6M5gm52|SABYc`A9N5~kN^&pE@;4zF(uHDG9}=UGbN}HTc7~&kTfN16@3Mc1)l}s@Py&45L>_i{?9;>F(puuG9_SL!N}T>t=M1CWvGCYd97@rWZ~@GF`X5M96k2q%w|fRP6$kCTDrk>Q_{7Z6?G0Eq{I5d*-E<&ohCz<`klC*lxY@BomJ2q%dvS@etMk>Q_{kCPh^UJwB0CV>$Pz=h|L;h&R_lMa+c5MF=))+Nx8*Cl|F4JVM14kySKLJ(fC0G2L*kqsx1kq##a7a0dI^&b +OTA00045MLkwuPvS}jV+Lq*Cl|H)+Nqgwh&*S0Qd@kkqIY|kqRfEkqalVk+=|FumB7v$QIcK>IVA;81TU?(JT%SU%&u%0f3PWCy5RwQd5RwNcXAod;0M;dtk=G@N4VfJvkqsv>krxnPfB+6BC>J>gSoDbDlHs3|kCWgKV4wgoB@mG^B_NSAB`}dRCE+Y`5MaOnR~rD4@+Am^gqCWksAQlL#k(lLsfI5Ml5DkFzK*B5=|!f&Kc=V37wW4iI7h0C15ACxzgU;0}~|XpJq*Um*};PymuI!Y|KpP~a#31;F|N=1>3-VqgIJO~8Wlz`+siz`6mn;ARi65MpoucOSZy;rIIi>RkFw0D%#K#SmiP0RJE6Qu<8+knn-vEds#c;D8Wf@Br}OhyNPD+X1Rvx=kFGjgYnwV=w@!T)IsdmW_~|kgbrskQr-2J;5g@P_{(z}o?;T)It|5M(d_`T^!p`b~g?3BdXR=1}@gpb%t001$!%!1@8^Q2I^4gZcsHPzn%aPyqT(fPwk}=wA9wAc2Axx$ub)WN-kGk?JOq5eLAKkpw1~G#K<>5M+P=k>HS%>n4DM0>FUaC(qB3>=0zI0PYX}ALdf}O(20Wfq(CE4*(Ek@BsJ$z=O|`ACV7{2$g`5>?ZIKWdHzy5g)*SkrV=f5gWjOk?kg85M>Ymfe{zLfRQ65fe{nHfRXAZGZ1AU0D%z?z<`kkCV>$Pz<`nKCQb +OTA00055M?j`fe{D5kdf&of#8u51HgdbtPo|80QN2yFUBv|FOMyeFOZY*CE*Zdpa6iA@FgFU|B{5~lHs3|kCRRiW#9ly>*Ov%;q)$5;8iYwk?D0PrPnlkp{glW&vXlFyP3l#~!=Pypy!50DR#50DR#fRXSekdZYIW?%sECB!@_;L-=U^o2Yr;F1R;5NH4Z4wPM64wNojV3Y7AaFg*R;Sgz100<@yl(QKRkPncG;E>=9l;aR-Z~!nRh~ksspOcT14wUFxFYk^JYCr&hkq#$@l^>FilY!)u;h&QS5Ncom50HS7R;fp0mA_flz9rMm0J*SAOMNr1Hg*l0l%kz)YKSz(o*qumBH`50DR#W#IM_Kfpkew@`5Ok0L4wUFx50DR#`I-LDV37zWOb~SN00$>=Blv!x`nPd=k5CHzqV3GVLfZ~wg4wOe|`I*)bbwB_R;6RZFCKMOQ^!b_o&w(#j5Or_>id>pa4wN`(A7A6o`I-LDZ4h;U08rpW4^FTi2qsDpcJKfQCxDX&Ck((4k_aadk_RWP5O<&e4wNQngfJNNfRp?t2*CN7S`c@z0RGQlk^Clr;*j7DlxAq20SE +OTA00065O>f3(JX+G2PW7S2=J`HfRP3#-Vk@-0FaRcCz&LZl-U*$@YoWOEYc8oumG7KDwW~)id>pa4wPMJ4#0O1c)$SI0fgYH54#_VmErfQT)Iu<5P1Lq3&4ahkp)2X01pu#_yOipz7ToP0O(qPkq9Oaw)m9ZzYo66zuypg000k=fRP9$ACixgjOCQ!pOdW+dLRH9@Qmk};0C~ekq9P=FhKNg5PDz$pOcT14wUFxfRP9%kCT9rYY=*H00$?JlYo(l2OpA;lZWS&;a?DbZ~!$W_yJ_#_MYGk@f{H)fPtnEet-axfg*u3fq;`SC6JRcB_NaY5PpyVGbJ#SG$kLB|B^qGFO#2>W)OdH043q_E`X5-CKwk8@Ls?nF9;BSfB-Py`2p?^{~zX3`b~j>kCQ17f1m*BT>4Gl0UVYSmW+`TmyeSm5P%Q>zySlFkPQ(ehhLdZpOcT13lM4z$6fXFaYxjV3YVJaFh8afRp+rkdwL)fj|KJCinp$fgh4DlOcg0k}s3&5P@(2zml(#4wU#>fRXqnkdgT&3J`&S0HBfjCa{tFCJ(mwl-|D&zJU;d@BpD5u^oVu_$H8(`6i%~`X((9f&c)pllvyWldqGXlaG^u=9S?^5P~oO0w;kHD!_n|1SXGPfRh9!GZ2D60Ejd=@PLs4Cx{~%@PXiflj#tGPyhlafRP9$4wSPQK#>F{VBif9f?xn};DF$e;0~1ZHGq)>CWtgF5Q1<35bzL^1SWuy1SWwIF~ES4V-SLX00Jk7BO&mB-~_`Ek^(1ykp>WgkN^QEh$A8JfRP9$4wSPQZRGqAg5UtCm7V1I5(L12kpw1z5zEh-5QAU<5R&F550DR#50DR#K`b*Q9}t6(015QXz<`kz0)UhDB!H3TCgBi+&;Woy;r9T*001hOfRXeikdZhLgWv%5C4e%KKrrwSlJzAJlJq565QGo_j4XhY<|e3>8o+?y4wQH-$Pk1;09{)SlrCIwlkp{Blkg=#kyZ +OTA00075QI@6_5QLBbFp=mcfRTCwkdb=>C>J>gVGx9%04Vf;k>)0W5!k^;z(EoD&qol1zyJ`E=q3=7=O%!W_9TcSN$@@pg)jgCCxI{t^e)e|&w!DM2Z$q?5QSg>+CU(Y2PZI*2q%D%4JVM1yby(O01hW87dZzQ@Q&$~;c38u;K&e#pa2agkdY222p1U#2=Jo7fRUFFh7bTikqIYIkqReZkqakqkqjsB5QZ=S@+1(F^CS?G^dx|h4JVM1OAv-Y01hVsmk1Xb2MF}1&x`7n;lU7wZ~(C#K#>V2P>~8JV37+aaFO*8hJXMJCrB4r2Wkg<2MF}0&#^4G5QiWDT>*j)lOCZQu^d2=2`5mIJ`jg60178ykqakqkqjq<7L^932G9_PzyM&82Pbfm2q$k}e@}3e2q%mXhtL3ElLsgHnf}j(;E>=BlzC{u5Qs1U0Vg356~K=_@9&-miSUq<91w^=001X|lm8}wk^Lr+k^UwDmbnm!PympV{w9Ev{U!sz{p9}=4!{@?h+qJKkpU-tvwC-M-8@BsNGK@kPOfRO?Z2MfRXJcgD`IpiLd|w07IC7k>@6mk?1CfKK<|A5Q)G5p9d)P0hW-H=q7-Z=O%!W>=2320Q4o0k@Y2jG9loS2PpIbmP8PV-~f=5^(BCl^d*3i0w;(gG4L1=iVy()Bp{LbB_bo4@PLv2B#@Cq5Q;DWkLC`P>ko({8SsMOfRp(p-VlmF0DzJ1C6JNtB>|R@lkX*flk5 +OTA00085QrVxs701uE4kPna#kPnc6k@qEtBLfhMfB?zxfRXkkk6(b3_9Y*ZkCPG*il6`wkPnc6k@h8xFd*;{lJ+Ii5Q?w>5R&&LAd&7RFp=*ifRX+rED(yo0FaUYBq$d-2l((1lJ6xDlA{oc&;afwfRO?PL5CG~Xh$BJqhT@jtpOcT14wRD+iy#2#T7Z%2CJ&Ggkc2Qf^n&AF5Q{JXmf@e1kCP6R_*xFYfRXAZhY*Wk01%SuCIrHOk?SUiBZ=_LXi4wU#>f#hBgi+})r+JKSkCJ&Ggkbx2M&+ox15Q~rifRX7YhA;{67r_vc=_ZNj3=oW<0FRRnl=xaekpw3XkPna#kkt^4008?WaNvO8kl-NbK#>F|4wQZnjSv7EF%OUrkbsftCWs@c@POb25REVZkdgZ&i4z0B5R&O80>FTi4iJq{0EXfCE(_}t;ee6pCV>$L!6OijzyJ@B50KAa$j`u$BPGlu3G^EfjnDvZzz~unCB!fZ@Ik@Bo03|0a-;04I=>*Cl|H)+NUfj}QO`CV-I@0*E6C^uxeP;85Ur5RX6r&p%)9)dxxN(396Cz?0S`0}zi;0DzJCB#&Q!lldfokp?D#5%v&|U;qZd5R&;M5R(2T5R&~SfZ&l3kDvfxllLTmk?U0V*6E?j_-@FkFu@g)zCfDn+d01lLXGKeF&@PLu+CWs?B@Dc +OTA00095RkwChU=5ykdxUZfRorI3&4PpuMm*X02Bg&5eLA5;DD2U0}ztgC5#Y|-~bSk*d_4GfRTO!fiTJRh2*^ukq`hV2oRDYB@mL?B@mL>B?y&T5Ro7NfRWfGkdfIX0hW-H*(HFJM-Y)P0N5pnK9fLL@PXiflYRq)yCp!4V61ok`N~mz=kkM@LLd*KmY;2fZ>3X5GMk_5RwoliGd6dlTZMUF+uf^evmXN^?;EOCx|075R-5KkdqE4fRha;2`&H(stdadkPwrA0J<-!FPJZgCxDX)Cy6AoWKB=;j!I_Fgf&2;ZWdei7N^4mJpoq0KCA8np0EH=;A-S|;?4mMl!Imhz!6YR5TCFBfPsM#B)~-GP~d9hcjAxawh*7d0GQ!1+ya-8^l0sT;ELpz;#Cl!5C8zc000)4fRRQgi#HMU43zB>r+4up3fj|(YKmf)84wPMHMCMT7YUFp~i05w*rEma&<(J|Pl$U0SE0OR;;85Vx5T(EX_yLB2k6(s^MCMT7YUFp~lMtoQ0D|b5;V9Wi;85Ug=y~CaY3q$5T;N7>evUDQ1eFMP~c_jdEtuW=@6!H0C3=d<(J|PlnrF~0b_^%@5m6QzyNCGcjAKTnc=S3PnQVvNWhH{rqBSc0YP40Pi*gb;fmy!;trHR5T}3uMCMT7YUFp~i0ql+OxQ)2+7PFZ00{JGz(?Rv;9>1~;fmy!;;#^=umEiDdEtuWm*NhT%4V(sL0;VusBi$`_W-~E07T|c;A-S|;)(6a5U7vpCisYB#4wRQ>i7V;#>=3A+0ImT+USCga?|I>h7_Dl$U0SD+%%6l9Ldw5CB8qP~dFmd*O!Vm*NhTmu9UHt}p=LK;icQzyJV%kr5{!l8=)<5Ux-F4wQapMCMT7YUFp~%K;9Q{1C2S0253{;85UV>3iXd88`fB;6oMCMT7YUFp~g5j6q4wO9*uCM?AzyJVB;85Ud>3iXd&5U$Vwgyomw4wMaKAdwL#FyKJoWDu_q0MC-2l8=&#AOL=6Mc`22VBm1zisYB#4wTLiuYdrLlZ)z`;h&R_lZxb*;-8a25U-E`kCP6RerJm0m*Q&E|L-8@?+~%z05Cw|_W-~E07T|c;A-S|;#Lr{003*^eBp}Zm*NhTmu878QS=oMvJe13;ZWdW<#*!90fStgPY#p@5VCLp;r9T*002bhP~d9hcjAuY#1OK80G#0tloL!3mr(FQ;ZWde%FhJq=0KfnMMBz~2YUEQ8vj6~4;A-S|;>ZDwQJ+r^l!InQ5VK$aP~d9hcjAQRoZ$|X6HG;yLJ+fX0HE+l;85Uh=6vCbfrv+w`_zyJV5=1|~j0C4d{=1|~jJ$xhH^=gb>N#0JtYV?=SD4?+w5};4t7I;E@ +OTA000C5YBJ_fRV5#kdd(`4wOq>-;vLew-C=@0I(;Jk+CNXluKRu0jeh`Foh7)q5uE@0000000062AORN&0R=G(?IHkXZ*FBe083C#IsgFRor^kx?6 + @OneToMany(mappedBy = "firmware") @Cascade(CascadeType.ALL) val packets: MutableList ) diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacket.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacket.kt index 6a1edca1..9070c477 100644 --- a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacket.kt +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacket.kt @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.crestdeviceservice.firmware.entity +import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.IdClass @@ -10,4 +11,9 @@ import jakarta.persistence.ManyToOne @Entity @IdClass(FirmwarePacketCompositeKey::class) -class FirmwarePacket(@ManyToOne @Id val firmware: Firmware, @Id val packetNumber: Int, val packet: String) +class FirmwarePacket( + @ManyToOne @Id val firmware: Firmware, + @Id val packetNumber: Int, + @Column(length = 1024) // without this, the integration test assumes a length of 255 + val packet: String +) diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacketCompositeKey.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacketCompositeKey.kt index 570d2930..be749485 100644 --- a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacketCompositeKey.kt +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/entity/FirmwarePacketCompositeKey.kt @@ -5,4 +5,6 @@ package org.gxf.crestdeviceservice.firmware.entity import java.io.Serializable -data class FirmwarePacketCompositeKey(val firmware: Firmware, val packetNumber: Int) : Serializable +data class FirmwarePacketCompositeKey(var firmware: Firmware?, var packetNumber: Int?) : Serializable { + constructor() : this(null, null) +} diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/exception/FirmwareException.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/exception/FirmwareException.kt new file mode 100644 index 00000000..a1a0742d --- /dev/null +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/exception/FirmwareException.kt @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.firmware.exception + +class FirmwareException(message: String) : Exception(message) diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt new file mode 100644 index 00000000..771d469f --- /dev/null +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/mapper/FirmwareMapper.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.firmware.mapper + +import com.alliander.sng.Firmware as ExternalFirmware +import com.alliander.sng.FirmwareType +import com.alliander.sng.Firmwares +import io.github.oshai.kotlinlogging.KotlinLogging +import java.util.UUID +import org.gxf.crestdeviceservice.firmware.entity.Firmware +import org.gxf.crestdeviceservice.firmware.entity.FirmwarePacket +import org.gxf.crestdeviceservice.firmware.exception.FirmwareException +import org.gxf.crestdeviceservice.firmware.repository.FirmwareRepository +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile + +@Component +class FirmwareMapper(private val firmwareRepository: FirmwareRepository) { + private val logger = KotlinLogging.logger {} + + fun mapFirmwareFileToEntity(file: MultipartFile): Firmware { + val fileContent = String(file.inputStream.readBytes()) + + val name = checkNotNull(file.originalFilename) { "File name should not be null" } + + val firmware = + Firmware( + UUID.randomUUID(), + name, + getFirmwareVersionFromName(name), + getPreviousFirmwareIdFromName(name), + mutableListOf()) + + val packets = mapLinesToPackets(fileContent.lines(), firmware) + + firmware.packets.addAll(packets) + + return firmware + } + + private fun getFirmwareVersionFromName(name: String) = name.substringAfter("#TO#").substringBefore(".txt") + + private fun getPreviousFirmwareIdFromName(name: String): UUID? { + val previousVersionRegex = """(?<=#FROM#)(.*)(?=#TO#)""".toRegex() + return previousVersionRegex.find(name)?.let { + val previousFirmwareVersion = it.value + val previousFirmware = firmwareRepository.findByVersion(previousFirmwareVersion) + previousFirmware?.id + ?: throw FirmwareException("Previous firmware with version $previousFirmwareVersion does not exist") + } + } + + private fun mapLinesToPackets(dtoPackets: List, firmware: Firmware) = + dtoPackets.mapIndexed { index, line -> mapLineToPacket(index, line, firmware) } + + private fun mapLineToPacket(index: Int, line: String, firmware: Firmware) = FirmwarePacket(firmware, index, line) + + fun mapEntitiesToFirmwares(firmwareEntities: List): Firmwares { + val firmwares = firmwareEntities.map { firmware -> mapEntityToSchema(firmware) } + return Firmwares.newBuilder().setFirmwares(firmwares).build() + } + + private fun mapEntityToSchema(firmware: Firmware): ExternalFirmware = + ExternalFirmware.newBuilder() + .setName(firmware.name) + .setType(getFirmwareTypeFromName(firmware.name)) + .setVersion(firmware.version) + .setFromVersion(getFromVersion(firmware)) + .setNumberOfPackages(firmware.packets.size) + .build() + + private fun getFirmwareTypeFromName(name: String): FirmwareType { + val type = name.substringBefore("#") + return translateType(type) + } + + private fun translateType(type: String): FirmwareType = + when (type) { + "RTU" -> FirmwareType.device + "MODEM" -> FirmwareType.modem + else -> { + throw FirmwareException("Firmware type $type does not exist") + } + } + + private fun getFromVersion(firmware: Firmware): String? { + return firmware.previousFirmwareId?.let { + val previousFirmware = firmwareRepository.findById(firmware.previousFirmwareId) + previousFirmware.get().version + } + } +} diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/repository/FirmwareRepository.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/repository/FirmwareRepository.kt index 4f1a0874..afa69fe4 100644 --- a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/repository/FirmwareRepository.kt +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/repository/FirmwareRepository.kt @@ -3,11 +3,16 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.crestdeviceservice.firmware.repository +import java.util.UUID import org.gxf.crestdeviceservice.firmware.entity.Firmware import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository @Repository -interface FirmwareRepository : CrudRepository { - fun findByName(name: String): Firmware +interface FirmwareRepository : CrudRepository { + fun findByName(name: String): Firmware? + + fun findByVersion(version: String): Firmware? + + override fun findAll(): List } diff --git a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareService.kt b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareService.kt index 06e3ad7a..2f13da5e 100644 --- a/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareService.kt +++ b/components/firmware/src/main/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareService.kt @@ -3,7 +3,34 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.crestdeviceservice.firmware.service +import com.alliander.sng.Firmwares +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gxf.crestdeviceservice.firmware.entity.Firmware +import org.gxf.crestdeviceservice.firmware.exception.FirmwareException +import org.gxf.crestdeviceservice.firmware.mapper.FirmwareMapper import org.gxf.crestdeviceservice.firmware.repository.FirmwareRepository import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile -@Service class FirmwareService(private val firmwareRepository: FirmwareRepository) +@Service +class FirmwareService( + private val firmwareRepository: FirmwareRepository, + private val firmwareMapper: FirmwareMapper, +) { + private val logger = KotlinLogging.logger {} + + fun processFirmware(file: MultipartFile): Firmwares { + val firmware = firmwareMapper.mapFirmwareFileToEntity(file) + if (firmwareRepository.findByName(firmware.name) != null) { + throw FirmwareException("Firmware with name ${firmware.name} already exists") + } + save(firmware) + val firmwareEntities: List = firmwareRepository.findAll() + return firmwareMapper.mapEntitiesToFirmwares(firmwareEntities) + } + + private fun save(firmware: Firmware) { + logger.info { "Saving firmware with name ${firmware.name} to database" } + firmwareRepository.save(firmware) + } +} diff --git a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt new file mode 100644 index 00000000..6b207cc4 --- /dev/null +++ b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/FirmwareFactory.kt @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import com.alliander.sng.Firmware as ExternalFirmware +import com.alliander.sng.FirmwareType +import com.alliander.sng.Firmwares +import java.util.UUID +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_FROM_VERSION +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_NAME +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_PACKET_0 +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_UUID +import org.gxf.crestdeviceservice.TestConstants.FIRMWARE_VERSION +import org.gxf.crestdeviceservice.TestConstants.PREVIOUS_FIRMWARE_UUID +import org.gxf.crestdeviceservice.firmware.entity.Firmware +import org.gxf.crestdeviceservice.firmware.entity.FirmwarePacket +import org.springframework.core.io.ClassPathResource +import org.springframework.mock.web.MockMultipartFile + +object FirmwareFactory { + fun getFirmwareFile(): MockMultipartFile { + val fileName = "RTU#FULL#TO#23.10.txt" + val firmwareFile = ClassPathResource(fileName).file + return MockMultipartFile("file", firmwareFile.name, "text/plain", firmwareFile.readBytes()) + } + + fun getFirmwareEntity(name: String): Firmware { + val firmware = Firmware(FIRMWARE_UUID, name, FIRMWARE_VERSION, PREVIOUS_FIRMWARE_UUID, mutableListOf()) + val packet = getFirmwarePacket(firmware) + firmware.packets.add(packet) + return firmware + } + + fun getPreviousFirmwareEntity(): Firmware = + Firmware(PREVIOUS_FIRMWARE_UUID, FIRMWARE_NAME, FIRMWARE_FROM_VERSION, UUID.randomUUID(), mutableListOf()) + + private fun getFirmwarePacket(firmware: Firmware) = FirmwarePacket(firmware, 0, FIRMWARE_PACKET_0) + + fun getFirmwares() = Firmwares.newBuilder().setFirmwares(listOf(firmware(), previousFirmware())).build() + + private fun firmware() = + ExternalFirmware.newBuilder() + .setName(FIRMWARE_NAME) + .setType(FirmwareType.device) + .setVersion(FIRMWARE_VERSION) + .setFromVersion(FIRMWARE_FROM_VERSION) + .setNumberOfPackages(2) + .build() + + private fun previousFirmware() = + ExternalFirmware.newBuilder() + .setName(FIRMWARE_NAME) + .setType(FirmwareType.device) + .setVersion(FIRMWARE_FROM_VERSION) + .setFromVersion(FIRMWARE_FROM_VERSION) + .setNumberOfPackages(2) + .build() +} diff --git a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt new file mode 100644 index 00000000..4c25c8a9 --- /dev/null +++ b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/TestConstants.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice + +import java.util.UUID + +object TestConstants { + const val FIRMWARE_VERSION = "01.10" + const val FIRMWARE_FROM_VERSION = "01.20" + const val FIRMWARE_NAME = "RTU#DELTA#FROM#$FIRMWARE_FROM_VERSION#TO#$FIRMWARE_VERSION" + const val FIRMWARE_WRONG_NAME = "FOTA.txt" + const val FIRMWARE_PACKET_0 = + "OTA0000or^kx?6`+01uK8B#)DylY_B=aFCG>Cq57j5CD-81;B_S8Ss(dkdq82fRo-34IlsxC!djzkq?j\$z7LQekQkNM5Dh>8g8PUkfccP-4=0%;De!?2KoAX300Y32;En+U*wkqRfs5Dl;Zfe{42h2Vgb3MVO=K#?XVQxFZn0EOb0;trHkGXIh9k-w3zk\$eykPym3D3MPRO2f\$yL5Rw%Ff#Bs35O4s3;DD0~CMuXKm?@Z9nt+jk5D<_6fRPC&fe{wK4wQEikl=uo_Yfrz07uV&;0Tp~k!L4{m0Zn;4=xZTAOL`qXD48jX(vFFXeS7j|C09*B|rcOFYu6PClHZoC*s_J5fH%i5G7CmfRSe>1KoiU3&0;K5NIIc-4G>!0AS#7;L8Cdz<`lwCy!s|+`JGaumBA!{17tG0DzGcCyFEBQfxU;MWj25CA3MHsV9=THs;ecHj<_c?x6@Ij{iUw>*g8ksBuKFbME\$z\$XEf5IN8Q>0J6vAcHZ3LW5I-+yU&74*" + const val FIRMWARE_PACKET_1 = + "OTA00015INugu;J^U@BtAd>0J6v6oM6k-4Hqe0O?%%O+bcGhGK?uhTH+{kx~#k5CE{@>!0ue5hUqc`c0&UwT5&MIxqmh0TCoj;85U\$UYSh}l!IpC5IR5rC*lS&5(g>tF%T8N;E^ULD-b\$R00_W`;si1n2XXW<5DdWJk^K-lU;rd1@Zjzb{~zU1`b~r2kl@e|I)DJ>QTk1hf`Ed6ksBtDUx1TQ5IT?m8zz9@0lffRPy{k6(b3877Ulp0{KWJP-gRC4u1R8u17N=o8=qzyj!X5Iv9p{?CKrkl+rKGiZR51tyS_;}AWd00t%%G12fq^bs(b@M!Q9F+~tQZ~%xS5%6H+aNuB*947gh{?Bg^K9B\$\$CV=3O;0}~#Xn>I%CS%\$V5I&#)fRP*~ACixgh2@*zQpb+vdk{eI01lK5WcDtAksBuX76|Y|!7UI\$pa6&?3G~~=8Nh&%CMWn31;D`&L9hVsAPMx&z=Gftz<`k?C-@e}5JE5jiCmgZAmQmP_\$}N40gw%nX%IqS0QdpvT>4Gl0UVYSmW+`Tm&Xu7@Bn&=g#iE\$5g+&ga@_lN=}cnf}jUksK\$J5Jw;Yf#86XB_)89C?\$ZCDJ6iD_Yg-g0461XlPV>ElP4vBlPo2GllBltKmaQxfRisJfRimHfRipIE5Q5^M_>SO;K%_ElnrEnksBsx7clfF5JzwTf#86XB_&{!F(ur() + private val firmwareMapper = FirmwareMapper(firmwareRepository) + + @Test + fun mapEntitiesToFirmwares() { + val firmwareEntities = listOf(getFirmwareEntity(FIRMWARE_NAME)) + val previousFirmware = getPreviousFirmwareEntity() + whenever(firmwareRepository.findById(PREVIOUS_FIRMWARE_UUID)).thenReturn(Optional.of(previousFirmware)) + + val result = firmwareMapper.mapEntitiesToFirmwares(firmwareEntities) + + val firmware = result.firmwares.first() + assertThat(firmware.name).isEqualTo(FIRMWARE_NAME) + assertThat(firmware.version).isEqualTo(FIRMWARE_VERSION) + assertThat(firmware.type).isEqualTo(FirmwareType.device) + assertThat(firmware.fromVersion).isEqualTo(FIRMWARE_FROM_VERSION) + assertThat(firmware.numberOfPackages).isEqualTo(1) + } + + @Test + fun mapEntitiesToFirmwaresThrowsException() { + val firmwareEntities = listOf(getFirmwareEntity(FIRMWARE_WRONG_NAME)) + + assertThatThrownBy { firmwareMapper.mapEntitiesToFirmwares(firmwareEntities) } + .isInstanceOf(FirmwareException::class.java) + } +} diff --git a/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareServiceTest.kt b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareServiceTest.kt new file mode 100644 index 00000000..404e6cef --- /dev/null +++ b/components/firmware/src/test/kotlin/org/gxf/crestdeviceservice/firmware/service/FirmwareServiceTest.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.crestdeviceservice.firmware.service + +import org.assertj.core.api.Assertions.assertThat +import org.gxf.crestdeviceservice.FirmwareFactory +import org.gxf.crestdeviceservice.FirmwareFactory.getFirmwareEntity +import org.gxf.crestdeviceservice.FirmwareFactory.getFirmwares +import org.gxf.crestdeviceservice.FirmwareFactory.getPreviousFirmwareEntity +import org.gxf.crestdeviceservice.firmware.mapper.FirmwareMapper +import org.gxf.crestdeviceservice.firmware.repository.FirmwareRepository +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@ExtendWith(MockitoExtension::class) +class FirmwareServiceTest { + private val firmwareRepository = mock() + private val firmwareMapper = mock() + private val firmwareService = FirmwareService(firmwareRepository, firmwareMapper) + + @Test + fun processFirmware() { + val firmwareFile = FirmwareFactory.getFirmwareFile() + val firmwareEntity = getFirmwareEntity(firmwareFile.name) + val previousFirmware = getPreviousFirmwareEntity() + val allFirmwareEntities = listOf(previousFirmware, firmwareEntity) + val firmwares = getFirmwares() + whenever(firmwareMapper.mapFirmwareFileToEntity(firmwareFile)).thenReturn(firmwareEntity) + whenever(firmwareRepository.findAll()).thenReturn(allFirmwareEntities) + whenever(firmwareMapper.mapEntitiesToFirmwares(listOf(previousFirmware, firmwareEntity))).thenReturn(firmwares) + + val result = firmwareService.processFirmware(firmwareFile) + + verify(firmwareRepository).save(firmwareEntity) + assertThat(result).isEqualTo(firmwares) + } +} diff --git a/components/firmware/src/test/resources/RTU#FULL#TO#23.10.txt b/components/firmware/src/test/resources/RTU#FULL#TO#23.10.txt new file mode 100644 index 00000000..c42df97c --- /dev/null +++ b/components/firmware/src/test/resources/RTU#FULL#TO#23.10.txt @@ -0,0 +1,13 @@ +OTA00004Ny+G|NeoXu!zMI*ungbnQwylZQJ20!QA=&G(L+*5CO0Nu?N}rC*rK{yx{@B9g?g&9}oyo0Jv!VL0p$<~pa8sqK#{a3P?5DKV3D>baFNpx2(SRRCxjc73#JRV3yFi8gR6s45D5SP&9~mSYyo=#;E}o~@R7SGFc1k405F#+@MP}OAOIil_5nbFQGsHCbAf=7v?pv32`~VVk+mnFk+vtWk+&y=50xKA5Dee|54I1I5hRb3hTwpc=_ZenpAZcI01uK8B#)DylY_B=aFCJiCSMQ@5CD-81;B_S8Ss(dkdy2tfRq0a4IlvSCZCaykq?j$z7LQekQkNt5Dh>8g8PUkfccP-?a00Y32;ELw%*4WIyl5e&eD;0M4>n+U*wk?JPt5Dl;Zfe{42h2Vgb>Lw|gK#>L~ln@QT0EOU};trHkGXIh9k-w3zk$w;mPym3D_a%W52f$yL5Rw%Ff#CTN5O4s3;DD3&B`TOJm?@Z9nt+j&5D<_6fRX7Yfe{wK4wQEikl=uo7!V~807uV&;0Tp~kw+(nm0Zn;4^9vzAOL`qM<-yDNhd&)NGAxD|B_b_B|rcOFYu5^ClHZIC*s_J5fH#M5G7CmfRRTh1KoiU3&0;K5NIIc{SYO90AS#7;L8Cdz<`lQCy!s|+}sc)umB5GDWsfRRZjlHkdofDI8OVc>A!91t?l0DzJ7CXkW!CZLh_Ca{tBCfpD*-~fp$iYtpNA@v}V^d>Np^(FuiGVlOElJ+K0lJ_PXz%byD-~pHJ5Hx@Q+LRBF50KFi)6amB0w;kHK@c`@0C@volXwGwkpd@(BQfxU;P((Y5C9_YHR40*g8kq0K~FbME$z$XE!5IN8Q>RkFwAcHZ3LW5I-+yU&74g +OTA00015INugu;J^U@BtAd>RkFw6oM6k+z>hd0P0-&O+bcGhGK?uhTH+{kx>vj5CE{@>!0ue5hUtd`c0&UwT5#LIxqmh0TCoj;85U)Uztq~l!Ip75IR5rC*lS$5(g>tF%T8N;E@I=O%OUz00_W`;sh`l2XXW<5DdWJk^c}nU;qRs@Zjzb{~zX3`b~r2kl^kRI)DJ?QulS5C8y|S@aN+1tx^%;pq0_-vJ2_J0JiZl8Tk-_krL(z`&6OCdx1p5IZmcxb=(yf#~QihT`}x(BRD%st`MH0K%WZ0TCpRSD8(plaG@Qlq?WCfB->ffRP0zk6(b31tyKRp0|t;JP-gQC4u1R8u17N=o8=qzyj!Y5Iv9p{?CKrkl+rKGiZR5@+FXy_Yggx0P`gkG12fq^bs(b@M!Q9FIjCS%$-5I&#)fRP9$ACixgh2@*zQpb+vj}Soc01lK5WcDtAkq0LD76|Y|!9fr~pa6&?3G~~=8Nh&%1}FFu1;F7DL9hVsAPMx&z=Gftz<`kiC-@fU5JE5jid>paAmQmP_$}N40gw%nXb?hR0QdpwT>4Gl0UVYSmW+`Tm&Oo6@Bn&=g#iE$5g+&g=2H4iaf6BwLl6KClv!x_0p?QrO@V=rlYoJp5JYeQg5ZFY_a%evoZtn(f#86Xh7d%60QV(}?VR8~+fl?q;85UV?o1FwzyJ`E2qzGd2PY4Z50Lqp{?D!uMqmH|z>a@_ll&(6nf}jUk^Cmr5Jw;Yf#86XBqe~8CnbQBC?$ZC`VdDj03{`WlPM*DlO`pAlPe{Flll-xKmaNwfRipIfRijGfRimHE5HB{M_>SO;K%_ElnrEnkq0Jd7clfZ5JzwTf#86XBqd;zFeTi;hv=N(tq@0`0D<6ulO!d8lP@KJkq0J#lav +OTA00025J#{8FeSjigyEdvpOcT14wU#>`w&Oq0DzM(C3e6=;85UZ;eO!90c;RRAOI;PSHMN!P~d6ee&ELe4wSnPNH74GW{E2y^nu`jlP4ugz((N05J*q}f#86XB_%z;M&MB3X5)U~pb$u40LK9il$U0SDi7N^4P0xYgfRiL8au7)%0DzMvC4iGAC4iGBC4iGCB@GZsFaUs)E+v4GBqfL=3H0~DfRRQJNk9N8C5R&l^zy-j;gaAEls!+n5KEu{fRiI7h3uZ-4wQl&j_jV`f)Gq_03#)U;o{)+;@<%sl8Tk-_hb-F002~$ypj{ZgW!OZBPAb`50i!vP7na$Eb%M{;34BR;!Wva;9=mf5KeFacHoUIkuMIEc?t*MDDO7nClF4M03h!+;zR3S;9=l);EgSjFKrM{Kmd6P1>hj>HR4C?U*KWjcHqhoPrv|q3J{VR0w~}%;zH_Q;9=m^5KsUBv*_zCx8eIPfZ~WQfRP0z*AP%30F5n?FAkJsW{{r10TCpRSC9};&;a}a?pORz5Q2b_BqfL=3G}ZJQ1Ad`W`L0;C5R&l^g_U3;LZWk5K(XdjV+Nc4wPhO5RxS&0pJ0^yAV-;007{CktQXGBPsC!BSG*%;HeN%&;TYS0pNg?O55CR60pNg|ti5552pRgeJ9zX9NYkuN2PBMJ0PzyKp*5LPe%WM&YOFC_usfRQjIk3R|Ym=IP_0Ff^alw@WAkuW8Ikq0IS6WsB0Qe@5k@+T|k@_aEk^3eWgd1oOSdajVqlx8=qaLCiqJWY7CV_C85L+Ms4wPM64wNojaFg*RV3Y7A&JbHL0H~FKku@cd-~pC{aG587lkgB*Kmau*K#?&eP?0hvV39K=aFJ3FTTlQrB~M>pPj63uPe79~B~X+25L;jXG9_S>GbM17G$r|&{?C(@un=2-0DzG-C6M5gm52|SABYc`A9N5~kN^&pE@;4zF(uHDG9}=UGbN}HTc7~&kTfN16@3Mc1)l}s@Py&45L>_i{?9;>F(puuG9_SL!N}T>t=M1CWvGCYd97@rWZ~@GF`X5M96k2q%w|fRP6$kCTDrk>Q_{7Z6?G0Eq{I5d*-E<&ohCz<`klC*lxY@BomJ2q%dvS@etMk>Q_{kCPh^UJwB0CV>$Pz=h|L;h&R_lMa+c5MF=))+Nx8*Cl|F4JVM14kySKLJ(fC0G2L*kqsx1kq##a7a0dI^&b +OTA00045MLkwuPvS}jV+Lq*Cl|H)+Nqgwh&*S0Qd@kkqIY|kqRfEkqalVk+=|FumB7v$QIcK>IVA;81TU?(JT%SU%&u%0f3PWCy5RwQd5RwNcXAod;0M;dtk=G@N4VfJvkqsv>krxnPfB+6BC>J>gSoDbDlHs3|kCWgKV4wgoB@mG^B_NSAB`}dRCE+Y`5MaOnR~rD4@+Am^gqCWksAQlL#k(lLsfI5Ml5DkFzK*B5=|!f&Kc=V37wW4iI7h0C15ACxzgU;0}~|XpJq*Um*};PymuI!Y|KpP~a#31;F|N=1>3-VqgIJO~8Wlz`+siz`6mn;ARi65MpoucOSZy;rIIi>RkFw0D%#K#SmiP0RJE6Qu<8+knn-vEds#c;D8Wf@Br}OhyNPD+X1Rvx=kFGjgYnwV=w@!T)IsdmW_~|kgbrskQr-2J;5g@P_{(z}o?;T)It|5M(d_`T^!p`b~g?3BdXR=1}@gpb%t001$!%!1@8^Q2I^4gZcsHPzn%aPyqT(fPwk}=wA9wAc2Axx$ub)WN-kGk?JOq5eLAKkpw1~G#K<>5M+P=k>HS%>n4DM0>FUaC(qB3>=0zI0PYX}ALdf}O(20Wfq(CE4*(Ek@BsJ$z=O|`ACV7{2$g`5>?ZIKWdHzy5g)*SkrV=f5gWjOk?kg85M>Ymfe{zLfRQ65fe{nHfRXAZGZ1AU0D%z?z<`kkCV>$Pz<`nKCQb +OTA00055M?j`fe{D5kdf&of#8u51HgdbtPo|80QN2yFUBv|FOMyeFOZY*CE*Zdpa6iA@FgFU|B{5~lHs3|kCRRiW#9ly>*Ov%;q)$5;8iYwk?D0PrPnlkp{glW&vXlFyP3l#~!=Pypy!50DR#50DR#fRXSekdZYIW?%sECB!@_;L-=U^o2Yr;F1R;5NH4Z4wPM64wNojV3Y7AaFg*R;Sgz100<@yl(QKRkPncG;E>=9l;aR-Z~!nRh~ksspOcT14wUFxFYk^JYCr&hkq#$@l^>FilY!)u;h&QS5Ncom50HS7R;fp0mA_flz9rMm0J*SAOMNr1Hg*l0l%kz)YKSz(o*qumBH`50DR#W#IM_Kfpkew@`5Ok0L4wUFx50DR#`I-LDV37zWOb~SN00$>=Blv!x`nPd=k5CHzqV3GVLfZ~wg4wOe|`I*)bbwB_R;6RZFCKMOQ^!b_o&w(#j5Or_>id>pa4wN`(A7A6o`I-LDZ4h;U08rpW4^FTi2qsDpcJKfQCxDX&Ck((4k_aadk_RWP5O<&e4wNQngfJNNfRp?t2*CN7S`c@z0RGQlk^Clr;*j7DlxAq20SE +OTA00065O>f3(JX+G2PW7S2=J`HfRP3#-Vk@-0FaRcCz&LZl-U*$@YoWOEYc8oumG7KDwW~)id>pa4wPMJ4#0O1c)$SI0fgYH54#_VmErfQT)Iu<5P1Lq3&4ahkp)2X01pu#_yOipz7ToP0O(qPkq9Oaw)m9ZzYo66zuypg000k=fRP9$ACixgjOCQ!pOdW+dLRH9@Qmk};0C~ekq9P=FhKNg5PDz$pOcT14wUFxfRP9%kCT9rYY=*H00$?JlYo(l2OpA;lZWS&;a?DbZ~!$W_yJ_#_MYGk@f{H)fPtnEet-axfg*u3fq;`SC6JRcB_NaY5PpyVGbJ#SG$kLB|B^qGFO#2>W)OdH043q_E`X5-CKwk8@Ls?nF9;BSfB-Py`2p?^{~zX3`b~j>kCQ17f1m*BT>4Gl0UVYSmW+`TmyeSm5P%Q>zySlFkPQ(ehhLdZpOcT13lM4z$6fXFaYxjV3YVJaFh8afRp+rkdwL)fj|KJCinp$fgh4DlOcg0k}s3&5P@(2zml(#4wU#>fRXqnkdgT&3J`&S0HBfjCa{tFCJ(mwl-|D&zJU;d@BpD5u^oVu_$H8(`6i%~`X((9f&c)pllvyWldqGXlaG^u=9S?^5P~oO0w;kHD!_n|1SXGPfRh9!GZ2D60Ejd=@PLs4Cx{~%@PXiflj#tGPyhlafRP9$4wSPQK#>F{VBif9f?xn};DF$e;0~1ZHGq)>CWtgF5Q1<35bzL^1SWuy1SWwIF~ES4V-SLX00Jk7BO&mB-~_`Ek^(1ykp>WgkN^QEh$A8JfRP9$4wSPQZRGqAg5UtCm7V1I5(L12kpw1z5zEh-5QAU<5R&F550DR#50DR#K`b*Q9}t6(015QXz<`kz0)UhDB!H3TCgBi+&;Woy;r9T*001hOfRXeikdZhLgWv%5C4e%KKrrwSlJzAJlJq565QGo_j4XhY<|e3>8o+?y4wQH-$Pk1;09{)SlrCIwlkp{Blkg=#kyZ +OTA00075QI@6_5QLBbFp=mcfRTCwkdb=>C>J>gVGx9%04Vf;k>)0W5!k^;z(EoD&qol1zyJ`E=q3=7=O%!W_9TcSN$@@pg)jgCCxI{t^e)e|&w!DM2Z$q?5QSg>+CU(Y2PZI*2q%D%4JVM1yby(O01hW87dZzQ@Q&$~;c38u;K&e#pa2agkdY222p1U#2=Jo7fRUFFh7bTikqIYIkqReZkqakqkqjsB5QZ=S@+1(F^CS?G^dx|h4JVM1OAv-Y01hVsmk1Xb2MF}1&x`7n;lU7wZ~(C#K#>V2P>~8JV37+aaFO*8hJXMJCrB4r2Wkg<2MF}0&#^4G5QiWDT>*j)lOCZQu^d2=2`5mIJ`jg60178ykqakqkqjq<7L^932G9_PzyM&82Pbfm2q$k}e@}3e2q%mXhtL3ElLsgHnf}j(;E>=BlzC{u5Qs1U0Vg356~K=_@9&-miSUq<91w^=001X|lm8}wk^Lr+k^UwDmbnm!PympV{w9Ev{U!sz{p9}=4!{@?h+qJKkpU-tvwC-M-8@BsNGK@kPOfRO?Z2MfRXJcgD`IpiLd|w07IC7k>@6mk?1CfKK<|A5Q)G5p9d)P0hW-H=q7-Z=O%!W>=2320Q4o0k@Y2jG9loS2PpIbmP8PV-~f=5^(BCl^d*3i0w;(gG4L1=iVy()Bp{LbB_bo4@PLv2B#@Cq5Q;DWkLC`P>ko({8SsMOfRp(p-VlmF0DzJ1C6JNtB>|R@lkX*flk5 +OTA00085QrVxs701uE4kPna#kPnc6k@qEtBLfhMfB?zxfRXkkk6(b3_9Y*ZkCPG*il6`wkPnc6k@h8xFd*;{lJ+Ii5Q?w>5R&&LAd&7RFp=*ifRX+rED(yo0FaUYBq$d-2l((1lJ6xDlA{oc&;afwfRO?PL5CG~Xh$BJqhT@jtpOcT14wRD+iy#2#T7Z%2CJ&Ggkc2Qf^n&AF5Q{JXmf@e1kCP6R_*xFYfRXAZhY*Wk01%SuCIrHOk?SUiBZ=_LXi4wU#>f#hBgi+})r+JKSkCJ&Ggkbx2M&+ox15Q~rifRX7YhA;{67r_vc=_ZNj3=oW<0FRRnl=xaekpw3XkPna#kkt^4008?WaNvO8kl-NbK#>F|4wQZnjSv7EF%OUrkbsftCWs@c@POb25REVZkdgZ&i4z0B5R&O80>FTi4iJq{0EXfCE(_}t;ee6pCV>$L!6OijzyJ@B50KAa$j`u$BPGlu3G^EfjnDvZzz~unCB!fZ@Ik@Bo03|0a-;04I=>*Cl|H)+NUfj}QO`CV-I@0*E6C^uxeP;85Ur5RX6r&p%)9)dxxN(396Cz?0S`0}zi;0DzJCB#&Q!lldfokp?D#5%v&|U;qZd5R&;M5R(2T5R&~SfZ&l3kDvfxllLTmk?U0V*6E?j_-@FkFu@g)zCfDn+d01lLXGKeF&@PLu+CWs?B@Dc +OTA00095RkwChU=5ykdxUZfRorI3&4PpuMm*X02Bg&5eLA5;DD2U0}ztgC5#Y|-~bSk*d_4GfRTO!fiTJRh2*^ukq`hV2oRDYB@mL?B@mL>B?y&T5Ro7NfRWfGkdfIX0hW-H*(HFJM-Y)P0N5pnK9fLL@PXiflYRq)yCp!4V61ok`N~mz=kkM@LLd*KmY;2fZ>3X5GMk_5RwoliGd6dlTZMUF+uf^evmXN^?;EOCx|075R-5KkdqE4fRha;2`&H(stdadkPwrA0J<-!FPJZgCxDX)Cy6AoWKB=;j!I_Fgf&2;ZWdei7N^4mJpoq0KCA8np0EH=;A-S|;?4mMl!Imhz!6YR5TCFBfPsM#B)~-GP~d9hcjAxawh*7d0GQ!1+ya-8^l0sT;ELpz;#Cl!5C8zc000)4fRRQgi#HMU43zB>r+4up3fj|(YKmf)84wPMHMCMT7YUFp~i05w*rEma&<(J|Pl$U0SE0OR;;85Vx5T(EX_yLB2k6(s^MCMT7YUFp~lMtoQ0D|b5;V9Wi;85Ug=y~CaY3q$5T;N7>evUDQ1eFMP~c_jdEtuW=@6!H0C3=d<(J|PlnrF~0b_^%@5m6QzyNCGcjAKTnc=S3PnQVvNWhH{rqBSc0YP40Pi*gb;fmy!;trHR5T}3uMCMT7YUFp~i0ql+OxQ)2+7PFZ00{JGz(?Rv;9>1~;fmy!;;#^=umEiDdEtuWm*NhT%4V(sL0;VusBi$`_W-~E07T|c;A-S|;)(6a5U7vpCisYB#4wRQ>i7V;#>=3A+0ImT+USCga?|I>h7_Dl$U0SD+%%6l9Ldw5CB8qP~dFmd*O!Vm*NhTmu9UHt}p=LK;icQzyJV%kr5{!l8=)<5Ux-F4wQapMCMT7YUFp~%K;9Q{1C2S0253{;85UV>3iXd88`fB;6oMCMT7YUFp~g5j6q4wO9*uCM?AzyJVB;85Ud>3iXd&5U$Vwgyomw4wMaKAdwL#FyKJoWDu_q0MC-2l8=&#AOL=6Mc`22VBm1zisYB#4wTLiuYdrLlZ)z`;h&R_lZxb*;-8a25U-E`kCP6RerJm0m*Q&E|L-8@?+~%z05Cw|_W-~E07T|c;A-S|;#Lr{003*^eBp}Zm*NhTmu878QS=oMvJe13;ZWdW<#*!90fStgPY#p@5VCLp;r9T*002bhP~d9hcjAuY#1OK80G#0tloL!3mr(FQ;ZWde%FhJq=0KfnMMBz~2YUEQ8vj6~4;A-S|;>ZDwQJ+r^l!InQ5VK$aP~d9hcjAQRoZ$|X6HG;yLJ+fX0HE+l;85Uh=6vCbfrv+w`_zyJV5=1|~j0C4d{=1|~jJ$xhH^=gb>N#0JtYV?=SD4?+w5};4t7I;E@ +OTA000C5YBJ_fRV5#kdd(`4wOq>-;vLew-C=@0I(;Jk+CNXluKRu0jeh`Foh7)q5uE@0000000062AORN&0R=G(?IHkXZ*FBe083C#IsgFRor^kx?6