From dddf2e86903e6b683d5132d1b0d9615224cb3fa1 Mon Sep 17 00:00:00 2001 From: pnoker Date: Tue, 18 Aug 2026 23:24:58 +0800 Subject: [PATCH] test: enforce truthful coverage and behavioral quality --- .../impl/LocalCredentialServiceImplTest.java | 176 ++++++++++++++++++ .../RepositoryTestQualityGateTest.java | 147 +++++++++++++++ dc3-coverage/pom.xml | 164 +++++++++++++++- .../BacnetIpDriverCustomServiceImplTest.java | 6 - .../Dlt645DriverCustomServiceImplTest.java | 6 - .../impl/Dnp3DriverCustomServiceImplTest.java | 6 - ...EthernetIpDriverCustomServiceImplTest.java | 5 - .../impl/FinsDriverCustomServiceImplTest.java | 6 - .../impl/HttpDriverCustomServiceImplTest.java | 5 +- .../Iec61850DriverCustomServiceImplTest.java | 6 - .../KafkaDriverCustomServiceImplTest.java | 6 - .../impl/KnxDriverCustomServiceImplTest.java | 6 - .../LorawanDriverCustomServiceImplTest.java | 6 - .../Lwm2mDriverCustomServiceImplTest.java | 6 - .../impl/MbusDriverCustomServiceImplTest.java | 6 - .../RedisDriverCustomServiceImplTest.java | 6 - .../SerialDriverCustomServiceImplTest.java | 6 - .../impl/SnmpDriverCustomServiceImplTest.java | 6 - .../TcpUdpDriverCustomServiceImplTest.java | 5 - dc3-web/tests/unit/utils-services.test.ts | 39 ++++ dc3-web/vitest.config.ts | 16 +- pom.xml | 6 + 22 files changed, 536 insertions(+), 105 deletions(-) create mode 100644 dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/LocalCredentialServiceImplTest.java create mode 100644 dc3-common/dc3-common-public/src/test/java/io/github/pnoker/common/contract/RepositoryTestQualityGateTest.java diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/LocalCredentialServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/LocalCredentialServiceImplTest.java new file mode 100644 index 000000000..425b473bb --- /dev/null +++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/LocalCredentialServiceImplTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2016-present the IoT DC3 original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.github.pnoker.common.auth.service.impl; + +import io.github.pnoker.common.auth.dal.LocalCredentialManager; +import io.github.pnoker.common.auth.entity.bo.LocalCredentialBO; +import io.github.pnoker.common.auth.entity.builder.LocalCredentialBuilder; +import io.github.pnoker.common.auth.entity.model.LocalCredentialDO; +import io.github.pnoker.common.auth.service.TenantMembershipService; +import io.github.pnoker.common.enums.CredentialTypeEnum; +import io.github.pnoker.common.enums.EnableFlagEnum; +import io.github.pnoker.common.enums.RequirePasswordChangeFlagEnum; +import io.github.pnoker.common.exception.UnAuthorizedException; +import io.github.pnoker.common.utils.PasswordUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LocalCredentialServiceImplTest { + + @Mock + private LocalCredentialBuilder localCredentialBuilder; + + @Mock + private LocalCredentialManager localCredentialManager; + + @Mock + private TenantMembershipService tenantMembershipService; + + private LocalCredentialServiceImpl service; + + @BeforeEach + void setUp() { + service = new LocalCredentialServiceImpl( + localCredentialBuilder, localCredentialManager, tenantMembershipService); + } + + @Test + void addNormalizesLoginAndHashesPasswordBeforePersistence() { + LocalCredentialBO credential = new LocalCredentialBO(); + credential.setPrincipalId(7L); + credential.setLoginName(" Alice "); + credential.setRawPassword("correct horse battery staple"); + LocalCredentialDO persisted = new LocalCredentialDO(); + when(localCredentialBuilder.buildDOByBO(credential)).thenReturn(persisted); + when(localCredentialManager.save(persisted)).thenReturn(true); + + service.add(credential); + + assertThat(credential.getLoginNameNormalized()).isEqualTo("alice"); + assertThat(credential.getCredentialType()).isEqualTo(CredentialTypeEnum.PASSWORD); + assertThat(credential.getEnableFlag()).isEqualTo(EnableFlagEnum.ENABLE); + assertThat(credential.getRequirePasswordChange()).isEqualTo(RequirePasswordChangeFlagEnum.REQUIRED); + assertThat(credential.getPasswordHash()).doesNotContain("correct horse battery staple"); + assertThat(PasswordUtil.verify("correct horse battery staple", credential.getPasswordHash())).isTrue(); + verify(localCredentialManager).save(persisted); + } + + @Test + void fifthFailedLoginLocksCredentialForFifteenMinutes() { + LocalCredentialDO credential = credentialDO(4); + when(localCredentialManager.getById(11L)).thenReturn(credential); + LocalDateTime before = LocalDateTime.now().plusMinutes(14); + + service.recordFailedLogin(11L); + + assertThat(credential.getFailedAttempts()).isEqualTo(5); + assertThat(credential.getLockedUntil()).isAfter(before); + assertThat(credential.getLockedUntil()).isBefore(LocalDateTime.now().plusMinutes(16)); + assertThat(credential.getOperateTime()).isNull(); + verify(localCredentialManager).updateById(credential); + } + + @Test + void successfulLoginClearsFailureState() { + LocalCredentialDO credential = credentialDO(5); + credential.setLockedUntil(LocalDateTime.now().plusMinutes(10)); + when(localCredentialManager.getById(12L)).thenReturn(credential); + + service.recordSuccessfulLogin(12L); + + assertThat(credential.getFailedAttempts()).isZero(); + assertThat(credential.getLockedUntil()).isNull(); + verify(localCredentialManager).updateById(credential); + } + + @Test + void activeLockRejectsCorrectPassword() { + LocalCredentialBO credential = new LocalCredentialBO(); + credential.setPasswordHash(PasswordUtil.encode("secret")); + credential.setLockedUntil(LocalDateTime.now().plusMinutes(1)); + + assertThat(service.verifyPassword(credential, "secret")).isFalse(); + } + + @Test + void changePasswordRejectsWrongCurrentPasswordWithoutWriting() { + LocalCredentialDO stored = credentialDO(0); + stored.setPasswordHash(PasswordUtil.encode("old-secret")); + LocalCredentialBO credential = new LocalCredentialBO(); + credential.setId(21L); + credential.setPasswordHash(stored.getPasswordHash()); + when(localCredentialManager.getOne(any())).thenReturn(stored); + when(localCredentialBuilder.buildBOByDO(stored)).thenReturn(credential); + + assertThatThrownBy(() -> service.changePassword("alice", "wrong", "new-secret")) + .isInstanceOf(UnAuthorizedException.class); + + verify(localCredentialManager, never()).updateById(any()); + } + + @Test + void changePasswordRehashesAndResetsSecurityState() { + LocalCredentialDO stored = credentialDO(3); + stored.setId(21L); + stored.setPasswordHash(PasswordUtil.encode("old-secret")); + stored.setLockedUntil(LocalDateTime.now().plusMinutes(10)); + LocalCredentialBO credential = new LocalCredentialBO(); + credential.setId(21L); + credential.setPasswordHash(stored.getPasswordHash()); + when(localCredentialManager.getOne(any())).thenReturn(stored); + when(localCredentialBuilder.buildBOByDO(stored)).thenReturn(credential); + when(localCredentialManager.getById(21L)).thenReturn(stored); + when(localCredentialManager.updateById(stored)).thenReturn(true); + ReflectionTestUtils.setField(service, "passwordExpireDays", 30L); + + service.changePassword(" Alice ", "old-secret", "new-secret"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LocalCredentialDO.class); + verify(localCredentialManager).updateById(captor.capture()); + LocalCredentialDO updated = captor.getValue(); + assertThat(PasswordUtil.verify("new-secret", updated.getPasswordHash())).isTrue(); + assertThat(PasswordUtil.verify("old-secret", updated.getPasswordHash())).isFalse(); + assertThat(updated.getPasswordExpireTime()).isEqualTo(updated.getPasswordUpdatedTime().plusDays(30)); + assertThat(updated.getRequirePasswordChange()).isZero(); + assertThat(updated.getFailedAttempts()).isZero(); + assertThat(updated.getLockedUntil()).isNull(); + assertThat(updated.getOperateTime()).isNull(); + } + + private static LocalCredentialDO credentialDO(int failedAttempts) { + LocalCredentialDO credential = new LocalCredentialDO(); + credential.setFailedAttempts(failedAttempts); + credential.setOperateTime(LocalDateTime.now()); + return credential; + } +} diff --git a/dc3-common/dc3-common-public/src/test/java/io/github/pnoker/common/contract/RepositoryTestQualityGateTest.java b/dc3-common/dc3-common-public/src/test/java/io/github/pnoker/common/contract/RepositoryTestQualityGateTest.java new file mode 100644 index 000000000..e43819a18 --- /dev/null +++ b/dc3-common/dc3-common-public/src/test/java/io/github/pnoker/common/contract/RepositoryTestQualityGateTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016-present the IoT DC3 original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.github.pnoker.common.contract; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Repository-wide guardrails against tests that can pass without verifying production behaviour. + */ +class RepositoryTestQualityGateTest { + + private static final String TEST_JAVA = "/src/test/java/"; + private static final String THIS_FILE = "RepositoryTestQualityGateTest.java"; + private static final List FORBIDDEN_PATTERNS = List.of( + new ForbiddenPattern("disabled test", Pattern.compile("@(Disabled|Ignore)\\b")), + new ForbiddenPattern("constant boolean assertion", Pattern.compile( + "assert(?:True\\(true\\)|False\\(false\\)|That\\((?:true|false)\\)\\.is(?:True|False)\\(\\))")), + new ForbiddenPattern("no-op schedule test", Pattern.compile( + "void\\s+schedule(?:DoesNothing|IsNoOp)\\s*\\(")), + new ForbiddenPattern("fixture helper self-test", Pattern.compile( + "void\\s+(?:driverConfigContains|pointConfigContains)\\w*\\s*\\(")), + new ForbiddenPattern("comment-only assertion", Pattern.compile( + "No exception thrown is the assertion", Pattern.CASE_INSENSITIVE))); + private static final Pattern ONLINE_TEST_ASSERTS_OFFLINE = Pattern.compile( + "(?s)void\\s+(?:healthReturnsOnline|healthIsOnline|\\w+OnlineWhen\\w*)\\s*\\([^)]*\\)\\s*\\{" + + ".{0,2000}?assertThat\\([^;]*EntityStatusEnum\\.OFFLINE"); + private static final Set REQUIRED_CROSS_CUTTING_COVERAGE = Set.of( + "dc3-common-api", "dc3-common-facade-grpc", "dc3-common-sql"); + + @Test + void javaTestsMustVerifyObservableBehaviour() throws IOException { + Path repository = findRepositoryRoot(); + List violations = new ArrayList<>(); + + try (Stream paths = Files.walk(repository)) { + paths.filter(Files::isRegularFile) + .filter(path -> normalized(path).contains(TEST_JAVA)) + .filter(path -> path.toString().endsWith("Test.java") || path.toString().endsWith("Tests.java")) + .filter(path -> !path.getFileName().toString().equals(THIS_FILE)) + .forEach(path -> inspect(repository, path, violations)); + } + + assertThat(violations) + .as("Tests must fail when production behaviour is wrong") + .isEmpty(); + } + + @Test + void aggregateCoverageMustIncludeEveryDriverAndCrossCuttingRuntimeModule() throws Exception { + Path repository = findRepositoryRoot(); + Set driverModules = childTexts( + repository.resolve("dc3-driver/pom.xml"), "modules", "module"); + Set coveredModules = childTexts( + repository.resolve("dc3-coverage/pom.xml"), "dependencies", "artifactId"); + + assertThat(driverModules).as("Driver reactor must not be empty").isNotEmpty(); + assertThat(coveredModules) + .as("Aggregate coverage dependencies") + .containsAll(driverModules) + .containsAll(REQUIRED_CROSS_CUTTING_COVERAGE); + assertThat(Files.readString(repository.resolve("dc3-coverage/pom.xml"))) + .as("Generated MapStruct implementations must not dilute coverage") + .contains("**/entity/builder/*BuilderImpl.class"); + } + + private static void inspect(Path repository, Path path, List violations) { + try { + String source = Files.readString(path); + for (ForbiddenPattern forbidden : FORBIDDEN_PATTERNS) { + if (forbidden.pattern().matcher(source).find()) { + violations.add("%s: %s".formatted(normalized(repository.relativize(path)), forbidden.reason())); + } + } + if (ONLINE_TEST_ASSERTS_OFFLINE.matcher(source).find()) { + violations.add("%s: online health test asserts OFFLINE" + .formatted(normalized(repository.relativize(path)))); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to inspect " + path, e); + } + } + + private static Path findRepositoryRoot() { + Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath(); + while (current != null && !Files.exists(current.resolve(".git"))) { + current = current.getParent(); + } + if (current == null) { + throw new IllegalStateException("Cannot locate the repository root from " + System.getProperty("user.dir")); + } + return current; + } + + private static Set childTexts(Path pom, String containerName, String childName) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setExpandEntityReferences(false); + Element project = factory.newDocumentBuilder().parse(pom.toFile()).getDocumentElement(); + Set values = new HashSet<>(); + NodeList containers = project.getElementsByTagName(containerName); + for (int containerIndex = 0; containerIndex < containers.getLength(); containerIndex++) { + Element container = (Element) containers.item(containerIndex); + NodeList children = container.getElementsByTagName(childName); + for (int childIndex = 0; childIndex < children.getLength(); childIndex++) { + values.add(children.item(childIndex).getTextContent().strip()); + } + } + return values; + } + + private static String normalized(Path path) { + return path.toString().replace('\\', '/'); + } + + private record ForbiddenPattern(String reason, Pattern pattern) { + } +} diff --git a/dc3-coverage/pom.xml b/dc3-coverage/pom.xml index 0e4633eb5..27fdba8c5 100644 --- a/dc3-coverage/pom.xml +++ b/dc3-coverage/pom.xml @@ -37,22 +37,30 @@ true - - 0.20 - 0.15 + + 0.24 + 0.20 false io.github.pnoker dc3-common-agentic + + io.github.pnoker + dc3-common-api + io.github.pnoker dc3-common-auth @@ -73,6 +81,10 @@ io.github.pnoker dc3-common-exception + + io.github.pnoker + dc3-common-facade-grpc + io.github.pnoker dc3-common-facade-local-auth @@ -125,6 +137,10 @@ io.github.pnoker dc3-common-resource-registrar + + io.github.pnoker + dc3-common-sql + io.github.pnoker dc3-common-thread @@ -133,16 +149,101 @@ io.github.pnoker dc3-common-web + + io.github.pnoker + dc3-driver-bacnet-ip + ${dc3.version} + + + io.github.pnoker + dc3-driver-ble + ${dc3.version} + + + io.github.pnoker + dc3-driver-can + ${dc3.version} + io.github.pnoker dc3-driver-coap ${dc3.version} + + io.github.pnoker + dc3-driver-dlms + ${dc3.version} + + + io.github.pnoker + dc3-driver-dlt645 + ${dc3.version} + + + io.github.pnoker + dc3-driver-dnp3 + ${dc3.version} + + + io.github.pnoker + dc3-driver-ethernet-ip + ${dc3.version} + + + io.github.pnoker + dc3-driver-fins + ${dc3.version} + + + io.github.pnoker + dc3-driver-http + ${dc3.version} + + + io.github.pnoker + dc3-driver-iec104 + ${dc3.version} + + + io.github.pnoker + dc3-driver-iec61850 + ${dc3.version} + + + io.github.pnoker + dc3-driver-kafka + ${dc3.version} + + + io.github.pnoker + dc3-driver-knx + ${dc3.version} + io.github.pnoker dc3-driver-listening-virtual ${dc3.version} + + io.github.pnoker + dc3-driver-lorawan + ${dc3.version} + + + io.github.pnoker + dc3-driver-lwm2m + ${dc3.version} + + + io.github.pnoker + dc3-driver-mbus + ${dc3.version} + + + io.github.pnoker + dc3-driver-melsec + ${dc3.version} + io.github.pnoker dc3-driver-modbus-tcp @@ -158,6 +259,11 @@ dc3-driver-mqtt ${dc3.version} + + io.github.pnoker + dc3-driver-mysql + ${dc3.version} + io.github.pnoker dc3-driver-opc-da @@ -168,16 +274,61 @@ dc3-driver-opc-ua ${dc3.version} + + io.github.pnoker + dc3-driver-oracle + ${dc3.version} + io.github.pnoker dc3-driver-plcs7 ${dc3.version} + + io.github.pnoker + dc3-driver-postgresql + ${dc3.version} + + + io.github.pnoker + dc3-driver-redis + ${dc3.version} + + + io.github.pnoker + dc3-driver-serial + ${dc3.version} + + + io.github.pnoker + dc3-driver-sl651 + ${dc3.version} + + + io.github.pnoker + dc3-driver-snmp + ${dc3.version} + + + io.github.pnoker + dc3-driver-sqlserver + ${dc3.version} + + + io.github.pnoker + dc3-driver-tcp-udp + ${dc3.version} + io.github.pnoker dc3-driver-virtual ${dc3.version} + + io.github.pnoker + dc3-driver-zigbee + ${dc3.version} + @@ -202,6 +353,7 @@ **/entity/vo/** **/entity/model/** **/entity/query/** + **/entity/builder/*BuilderImpl.class com/serotonin/modbus4j/sero/log/** com/serotonin/modbus4j/sero/timer/** com/serotonin/modbus4j/sero/epoll/** diff --git a/dc3-driver/dc3-driver-bacnet-ip/src/test/java/io/github/pnoker/driver/service/impl/BacnetIpDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-bacnet-ip/src/test/java/io/github/pnoker/driver/service/impl/BacnetIpDriverCustomServiceImplTest.java index 5e6f58cb5..9c30e334f 100644 --- a/dc3-driver/dc3-driver-bacnet-ip/src/test/java/io/github/pnoker/driver/service/impl/BacnetIpDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-bacnet-ip/src/test/java/io/github/pnoker/driver/service/impl/BacnetIpDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class BacnetIpDriverCustomServiceImplTest { @@ -65,9 +64,4 @@ class BacnetIpDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-dlt645/src/test/java/io/github/pnoker/driver/service/impl/Dlt645DriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-dlt645/src/test/java/io/github/pnoker/driver/service/impl/Dlt645DriverCustomServiceImplTest.java index ace117222..8d4c5aa9b 100644 --- a/dc3-driver/dc3-driver-dlt645/src/test/java/io/github/pnoker/driver/service/impl/Dlt645DriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-dlt645/src/test/java/io/github/pnoker/driver/service/impl/Dlt645DriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @ExtendWith(MockitoExtension.class) @@ -64,11 +63,6 @@ class Dlt645DriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - @Test void encodeAddressRejectsInvalidAddress() { assertThatThrownBy(() -> Dlt645Frame.encodeAddress("1234")) diff --git a/dc3-driver/dc3-driver-dnp3/src/test/java/io/github/pnoker/driver/service/impl/Dnp3DriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-dnp3/src/test/java/io/github/pnoker/driver/service/impl/Dnp3DriverCustomServiceImplTest.java index e3cff4da8..6a5a1c185 100644 --- a/dc3-driver/dc3-driver-dnp3/src/test/java/io/github/pnoker/driver/service/impl/Dnp3DriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-dnp3/src/test/java/io/github/pnoker/driver/service/impl/Dnp3DriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class Dnp3DriverCustomServiceImplTest { @@ -63,9 +62,4 @@ class Dnp3DriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-ethernet-ip/src/test/java/io/github/pnoker/driver/service/impl/EthernetIpDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-ethernet-ip/src/test/java/io/github/pnoker/driver/service/impl/EthernetIpDriverCustomServiceImplTest.java index 642ed035a..a9a5df9d3 100644 --- a/dc3-driver/dc3-driver-ethernet-ip/src/test/java/io/github/pnoker/driver/service/impl/EthernetIpDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-ethernet-ip/src/test/java/io/github/pnoker/driver/service/impl/EthernetIpDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class EthernetIpDriverCustomServiceImplTest { @@ -61,8 +60,4 @@ class EthernetIpDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } } diff --git a/dc3-driver/dc3-driver-fins/src/test/java/io/github/pnoker/driver/service/impl/FinsDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-fins/src/test/java/io/github/pnoker/driver/service/impl/FinsDriverCustomServiceImplTest.java index b427a9b79..80a6d4514 100644 --- a/dc3-driver/dc3-driver-fins/src/test/java/io/github/pnoker/driver/service/impl/FinsDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-fins/src/test/java/io/github/pnoker/driver/service/impl/FinsDriverCustomServiceImplTest.java @@ -32,7 +32,6 @@ import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class FinsDriverCustomServiceImplTest { @@ -86,11 +85,6 @@ class FinsDriverCustomServiceImplTest { assertThat(report.getIssues()).isEmpty(); } - @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - @Test void wordCountIsTwoFor32BitTypesAndOneOtherwise() { assertThat(service.wordCount("INT32")).isEqualTo(2); diff --git a/dc3-driver/dc3-driver-http/src/test/java/io/github/pnoker/driver/service/impl/HttpDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-http/src/test/java/io/github/pnoker/driver/service/impl/HttpDriverCustomServiceImplTest.java index ee5deb522..f60ea80b5 100644 --- a/dc3-driver/dc3-driver-http/src/test/java/io/github/pnoker/driver/service/impl/HttpDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-http/src/test/java/io/github/pnoker/driver/service/impl/HttpDriverCustomServiceImplTest.java @@ -33,7 +33,6 @@ import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) @@ -88,8 +87,8 @@ class HttpDriverCustomServiceImplTest { } @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); + void scheduleDoesNotEmitDeviceStatus() { + service.schedule(); verifyNoInteractions(driverSenderService); } diff --git a/dc3-driver/dc3-driver-iec61850/src/test/java/io/github/pnoker/driver/service/impl/Iec61850DriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-iec61850/src/test/java/io/github/pnoker/driver/service/impl/Iec61850DriverCustomServiceImplTest.java index 80b603828..10e8005c4 100644 --- a/dc3-driver/dc3-driver-iec61850/src/test/java/io/github/pnoker/driver/service/impl/Iec61850DriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-iec61850/src/test/java/io/github/pnoker/driver/service/impl/Iec61850DriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class Iec61850DriverCustomServiceImplTest { @@ -64,9 +63,4 @@ class Iec61850DriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-kafka/src/test/java/io/github/pnoker/driver/service/impl/KafkaDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-kafka/src/test/java/io/github/pnoker/driver/service/impl/KafkaDriverCustomServiceImplTest.java index 2c98020c5..51491d257 100644 --- a/dc3-driver/dc3-driver-kafka/src/test/java/io/github/pnoker/driver/service/impl/KafkaDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-kafka/src/test/java/io/github/pnoker/driver/service/impl/KafkaDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.springframework.kafka.core.KafkaTemplate; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class KafkaDriverCustomServiceImplTest { @@ -65,9 +64,4 @@ class KafkaDriverCustomServiceImplTest { assertThat(report.isPassed()).isTrue(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-knx/src/test/java/io/github/pnoker/driver/service/impl/KnxDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-knx/src/test/java/io/github/pnoker/driver/service/impl/KnxDriverCustomServiceImplTest.java index 404aa0774..651e2e557 100644 --- a/dc3-driver/dc3-driver-knx/src/test/java/io/github/pnoker/driver/service/impl/KnxDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-knx/src/test/java/io/github/pnoker/driver/service/impl/KnxDriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class KnxDriverCustomServiceImplTest { @@ -64,9 +63,4 @@ class KnxDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-lorawan/src/test/java/io/github/pnoker/driver/service/impl/LorawanDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-lorawan/src/test/java/io/github/pnoker/driver/service/impl/LorawanDriverCustomServiceImplTest.java index 1eaf0d61d..5605f3d1a 100644 --- a/dc3-driver/dc3-driver-lorawan/src/test/java/io/github/pnoker/driver/service/impl/LorawanDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-lorawan/src/test/java/io/github/pnoker/driver/service/impl/LorawanDriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class LorawanDriverCustomServiceImplTest { @@ -63,9 +62,4 @@ class LorawanDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-lwm2m/src/test/java/io/github/pnoker/driver/service/impl/Lwm2mDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-lwm2m/src/test/java/io/github/pnoker/driver/service/impl/Lwm2mDriverCustomServiceImplTest.java index 0e9f67879..c1355c26e 100644 --- a/dc3-driver/dc3-driver-lwm2m/src/test/java/io/github/pnoker/driver/service/impl/Lwm2mDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-lwm2m/src/test/java/io/github/pnoker/driver/service/impl/Lwm2mDriverCustomServiceImplTest.java @@ -32,7 +32,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class Lwm2mDriverCustomServiceImplTest { @@ -76,9 +75,4 @@ class Lwm2mDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-mbus/src/test/java/io/github/pnoker/driver/service/impl/MbusDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-mbus/src/test/java/io/github/pnoker/driver/service/impl/MbusDriverCustomServiceImplTest.java index 7381019e4..6f9af4a6e 100644 --- a/dc3-driver/dc3-driver-mbus/src/test/java/io/github/pnoker/driver/service/impl/MbusDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-mbus/src/test/java/io/github/pnoker/driver/service/impl/MbusDriverCustomServiceImplTest.java @@ -29,7 +29,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class MbusDriverCustomServiceImplTest { @@ -63,11 +62,6 @@ class MbusDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - @Test void buildReqUd2HasValidStructureAndChecksum() { byte[] frame = MbusFrame.buildReqUd2(0); diff --git a/dc3-driver/dc3-driver-redis/src/test/java/io/github/pnoker/driver/service/impl/RedisDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-redis/src/test/java/io/github/pnoker/driver/service/impl/RedisDriverCustomServiceImplTest.java index 6b7de591f..8e67b1750 100644 --- a/dc3-driver/dc3-driver-redis/src/test/java/io/github/pnoker/driver/service/impl/RedisDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-redis/src/test/java/io/github/pnoker/driver/service/impl/RedisDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.springframework.data.redis.core.StringRedisTemplate; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class RedisDriverCustomServiceImplTest { @@ -65,9 +64,4 @@ class RedisDriverCustomServiceImplTest { assertThat(report.isPassed()).isFalse(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-serial/src/test/java/io/github/pnoker/driver/service/impl/SerialDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-serial/src/test/java/io/github/pnoker/driver/service/impl/SerialDriverCustomServiceImplTest.java index 59585767e..5b5453ae7 100644 --- a/dc3-driver/dc3-driver-serial/src/test/java/io/github/pnoker/driver/service/impl/SerialDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-serial/src/test/java/io/github/pnoker/driver/service/impl/SerialDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class SerialDriverCustomServiceImplTest { @@ -65,9 +64,4 @@ class SerialDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleDoesNothing() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-snmp/src/test/java/io/github/pnoker/driver/service/impl/SnmpDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-snmp/src/test/java/io/github/pnoker/driver/service/impl/SnmpDriverCustomServiceImplTest.java index 8a3f8f428..33b0fb18e 100644 --- a/dc3-driver/dc3-driver-snmp/src/test/java/io/github/pnoker/driver/service/impl/SnmpDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-snmp/src/test/java/io/github/pnoker/driver/service/impl/SnmpDriverCustomServiceImplTest.java @@ -33,7 +33,6 @@ import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; /** * Offline unit tests for {@link SnmpDriverCustomServiceImpl}. @@ -95,9 +94,4 @@ class SnmpDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } - } diff --git a/dc3-driver/dc3-driver-tcp-udp/src/test/java/io/github/pnoker/driver/service/impl/TcpUdpDriverCustomServiceImplTest.java b/dc3-driver/dc3-driver-tcp-udp/src/test/java/io/github/pnoker/driver/service/impl/TcpUdpDriverCustomServiceImplTest.java index 879165647..987728460 100644 --- a/dc3-driver/dc3-driver-tcp-udp/src/test/java/io/github/pnoker/driver/service/impl/TcpUdpDriverCustomServiceImplTest.java +++ b/dc3-driver/dc3-driver-tcp-udp/src/test/java/io/github/pnoker/driver/service/impl/TcpUdpDriverCustomServiceImplTest.java @@ -30,7 +30,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; @ExtendWith(MockitoExtension.class) class TcpUdpDriverCustomServiceImplTest { @@ -61,8 +60,4 @@ class TcpUdpDriverCustomServiceImplTest { assertThat(report.getIssues()).isNotEmpty(); } - @Test - void scheduleIsNoOp() { - assertThatNoException().isThrownBy(() -> service.schedule()); - } } diff --git a/dc3-web/tests/unit/utils-services.test.ts b/dc3-web/tests/unit/utils-services.test.ts index 2a9607014..76fa70e67 100644 --- a/dc3-web/tests/unit/utils-services.test.ts +++ b/dc3-web/tests/unit/utils-services.test.ts @@ -126,6 +126,45 @@ describe('utils (services)', () => { expect(authNameRules(t, 'Role')).toHaveLength(3); expect(remarkRules(t)[0]).toMatchObject({max: 300, trigger: 'blur'}); }); + + it('validates decimal, byte and positive-integer boundaries', async () => { + const {byteRules, decimalRules, positiveIntegerRules, requiredSelectRule, requiredStringRule} = + await import('@/utils/formRuleUtil'); + const t = vi.fn((key: string, args?: Record) => + `${key}:${args?.min ?? ''}:${args?.max ?? ''}` + ); + type RuleValidator = (rule: unknown, value: unknown, callback: (error?: Error) => void) => void; + const validate = (rule: {validator?: unknown}, value: unknown): Promise => + new Promise((resolve, reject) => { + const validator = rule.validator as RuleValidator; + validator(rule, value, error => (error ? reject(error) : resolve())); + }); + + const decimal = decimalRules('decimal only', 'value required'); + expect(decimal[0]).toMatchObject({required: true, whitespace: true, message: 'value required'}); + await expect(validate(decimal[1], '')).resolves.toBeUndefined(); + await expect(validate(decimal[1], '-12.345')).resolves.toBeUndefined(); + await expect(validate(decimal[1], '12.3456')).rejects.toThrow('decimal only'); + + const byte = byteRules(t, 'byte required')[1]; + await expect(validate(byte, 0)).resolves.toBeUndefined(); + await expect(validate(byte, 127)).resolves.toBeUndefined(); + await expect(validate(byte, 128)).rejects.toThrow('common.byteRange:0:127'); + await expect(validate(byte, 1.5)).rejects.toThrow('common.byteRange:0:127'); + + const positiveInteger = positiveIntegerRules(t, 'count required')[1]; + await expect(validate(positiveInteger, '1')).resolves.toBeUndefined(); + await expect(validate(positiveInteger, '0')).rejects.toThrow('common.positiveIntegerFormat'); + await expect(validate(positiveInteger, '-1')).rejects.toThrow('common.positiveIntegerFormat'); + await expect(validate(positiveInteger, '1.5')).rejects.toThrow('common.positiveIntegerFormat'); + + expect(requiredStringRule('name required', 'change')[0]).toMatchObject({ + required: true, + whitespace: true, + trigger: 'change', + }); + expect(requiredSelectRule('selection required')[0]).toMatchObject({required: true, trigger: 'change'}); + }); }); describe('commonUtil', () => { diff --git a/dc3-web/vitest.config.ts b/dc3-web/vitest.config.ts index ab1c2f4fb..6e7514d49 100644 --- a/dc3-web/vitest.config.ts +++ b/dc3-web/vitest.config.ts @@ -68,15 +68,15 @@ export default defineConfig({ 'src/config/types/**', 'src/config/ambient/**', ], - // Thresholds sit a couple of points below measured coverage so the - // gate fails on regression, not on noise. Bump again after the next - // round of test additions. Currently measured (post-A1 fixtures): - // branches 67% / functions 77% / lines 81% / statements 82%. + // Whole-surface baseline measured on 2026-08-18 after validator boundary + // coverage: branches 67.37%, functions 76.51%, lines 81.72%, statements + // 80.93%. The small margin absorbs runtime noise without hiding material + // regression. thresholds: { - branches: 65, - functions: 75, - lines: 78, - statements: 78, + branches: 66, + functions: 76, + lines: 81, + statements: 80, }, }, }, diff --git a/pom.xml b/pom.xml index 430f830d5..9c0260d06 100644 --- a/pom.xml +++ b/pom.xml @@ -1163,6 +1163,9 @@ argLine + + net.sf.jsqlparser.* + @@ -1173,6 +1176,9 @@ failsafe.argLine ${project.build.directory}/jacoco-it.exec + + net.sf.jsqlparser.* +