mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-08-28 22:21:16 +08:00
test: enforce truthful coverage and behavioral quality
This commit is contained in:
+176
@@ -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;
|
||||
}
|
||||
}
|
||||
+147
@@ -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
@@ -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>
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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"))
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -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);
|
||||
|
||||
+2
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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);
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-6
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user