test: enforce truthful coverage and behavioral quality

This commit is contained in:
pnoker
2026-08-18 23:24:58 +08:00
parent caf34ba85e
commit dddf2e8690
22 changed files with 536 additions and 105 deletions
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<LocalCredentialDO> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ForbiddenPattern> 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<String> 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<String> violations = new ArrayList<>();
try (Stream<Path> 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<String> driverModules = childTexts(
repository.resolve("dc3-driver/pom.xml"), "modules", "module");
Set<String> 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<String> 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<String> 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<String> 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) {
}
}
+158 -6
View File
@@ -37,22 +37,30 @@
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
<!-- Coverage thresholds gated by the aggregate XML report. Tightened per-stage. -->
<coverage.line.minimum>0.20</coverage.line.minimum>
<coverage.branch.minimum>0.15</coverage.branch.minimum>
<!--
Truthful all-runtime-module baseline measured on 2026-08-18:
line 24.90%, branch 21.28%. Keep a small cross-platform margin while
still failing any material regression.
-->
<coverage.line.minimum>0.24</coverage.line.minimum>
<coverage.branch.minimum>0.20</coverage.branch.minimum>
<coverage.check.skip>false</coverage.check.skip>
</properties>
<dependencies>
<!--
Modules listed here contribute to the aggregate report. Generated API
artifacts and pure deployment wrappers stay out of this baseline so
the report follows hand-written code that currently has tests.
All hand-written runtime modules contribute to the aggregate report.
Generated protobuf artifacts, test-support modules and pure deployment
wrappers stay out of the baseline.
-->
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-agentic</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-api</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-auth</artifactId>
@@ -73,6 +81,10 @@
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-exception</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-facade-grpc</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-facade-local-auth</artifactId>
@@ -125,6 +137,10 @@
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-resource-registrar</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-sql</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-thread</artifactId>
@@ -133,16 +149,101 @@
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-web</artifactId>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-bacnet-ip</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-ble</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-can</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-coap</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-dlms</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-dlt645</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-dnp3</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-ethernet-ip</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-fins</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-http</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-iec104</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-iec61850</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-kafka</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-knx</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-listening-virtual</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-lorawan</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-lwm2m</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-mbus</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-melsec</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-modbus-tcp</artifactId>
@@ -158,6 +259,11 @@
<artifactId>dc3-driver-mqtt</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-mysql</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-opc-da</artifactId>
@@ -168,16 +274,61 @@
<artifactId>dc3-driver-opc-ua</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-oracle</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-plcs7</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-postgresql</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-redis</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-serial</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-sl651</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-snmp</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-sqlserver</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-tcp-udp</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-virtual</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-zigbee</artifactId>
<version>${dc3.version}</version>
</dependency>
</dependencies>
<build>
@@ -202,6 +353,7 @@
<exclude>**/entity/vo/**</exclude>
<exclude>**/entity/model/**</exclude>
<exclude>**/entity/query/**</exclude>
<exclude>**/entity/builder/*BuilderImpl.class</exclude>
<exclude>com/serotonin/modbus4j/sero/log/**</exclude>
<exclude>com/serotonin/modbus4j/sero/timer/**</exclude>
<exclude>com/serotonin/modbus4j/sero/epoll/**</exclude>
@@ -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());
}
}
@@ -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"))
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
@@ -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);
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
+39
View File
@@ -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<string, unknown>) =>
`${key}:${args?.min ?? ''}:${args?.max ?? ''}`
);
type RuleValidator = (rule: unknown, value: unknown, callback: (error?: Error) => void) => void;
const validate = (rule: {validator?: unknown}, value: unknown): Promise<void> =>
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', () => {
+8 -8
View File
@@ -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,
},
},
},
+6
View File
@@ -1163,6 +1163,9 @@
</goals>
<configuration>
<propertyName>argLine</propertyName>
<excludes>
<exclude>net.sf.jsqlparser.*</exclude>
</excludes>
</configuration>
</execution>
<execution>
@@ -1173,6 +1176,9 @@
<configuration>
<propertyName>failsafe.argLine</propertyName>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
<excludes>
<exclude>net.sf.jsqlparser.*</exclude>
</excludes>
</configuration>
</execution>
<execution>