test: improve backend test coverage governance

This commit is contained in:
pnoker
2026-05-19 18:19:12 +08:00
parent 89282a71c6
commit 5a3ab1440a
20 changed files with 1060 additions and 37 deletions
+6 -4
View File
@@ -37,9 +37,11 @@ GROUP_SERVICES_core := $(GROUP_SERVICES_center) gateway
GROUP_SERVICES_drivers := listening-virtual modbus-tcp mqtt opc-da opc-ua plcs7 virtual
SELECTED_SERVICES := $(strip $(SERVICES) $(GROUP_SERVICES_$(GROUP)))
MVN_SETTINGS := .mvn/settings.xml
MVN := mvn -s $(MVN_SETTINGS)
MVN_SUB := mvn -s ../$(MVN_SETTINGS)
MVN_SETTINGS ?=
MVN_SETTINGS_ARG := $(if $(strip $(MVN_SETTINGS)),-s $(MVN_SETTINGS),)
MVN_SUB_SETTINGS_ARG := $(if $(strip $(MVN_SETTINGS)),-s ../$(MVN_SETTINGS),)
MVN := mvn $(MVN_SETTINGS_ARG)
MVN_SUB := mvn $(MVN_SUB_SETTINGS_ARG)
CHANGE_FILE ?= dc3/doc/CHANGE.md
FROM ?=
@@ -116,7 +118,7 @@ test-it:
$(MVN) -B -Dmaven.test.skip=false -Dskip.unit.tests=true verify
test-e2e:
DC3_E2E=true $(MVN) -B -Dmaven.test.skip=false -pl dc3-e2e -am -Pe2e verify
DC3_E2E=true $(MVN) -B -Dmaven.test.skip=false -Dskip.unit.tests=true -pl dc3-e2e -am -Pe2e verify
coverage:
$(MVN) -B -Dmaven.test.skip=false -pl dc3-coverage -am verify
+87
View File
@@ -70,6 +70,12 @@
<properties>
<!-- DC3 Version -->
<dc3.version>2026.5.18</dc3.version>
<!-- Test execution toggles (consumed by surefire/failsafe) -->
<skip.unit.tests>false</skip.unit.tests>
<skip.integration.tests>false</skip.integration.tests>
<mockito.version>5.20.0</mockito.version>
<mockito.javaagent.argLine>-javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar</mockito.javaagent.argLine>
</properties>
<modules>
@@ -117,6 +123,87 @@
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<skip>${skip.unit.tests}</skip>
<argLine>@{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*IT.java</exclude>
<exclude>**/Abstract*.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<skip>${skip.integration.tests}</skip>
<argLine>@{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*IT.java</include>
</includes>
</configuration>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-unit-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<propertyName>argLine</propertyName>
</configuration>
</execution>
<execution>
<id>prepare-integration-agent</id>
<goals>
<goal>prepare-agent-integration</goal>
</goals>
<configuration>
<propertyName>failsafe.argLine</propertyName>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
</configuration>
</execution>
<execution>
<id>report-unit</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>report-integration</id>
<phase>verify</phase>
<goals>
<goal>report-integration</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -0,0 +1,32 @@
/*
* 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.center.agentic;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class AgenticApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(AgenticApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
}
@@ -0,0 +1,32 @@
/*
* 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.center.auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class AuthApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(AuthApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
}
@@ -0,0 +1,32 @@
/*
* 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.center.data;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class DataApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(DataApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
}
@@ -0,0 +1,32 @@
/*
* 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.center.manager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class ManagerApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(ManagerApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
}
@@ -0,0 +1,46 @@
/*
* 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.center;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class SingleApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(SingleApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
@Test
void applicationScansAllCenterMapperPackages() {
MapperScan mapperScan = SingleApplication.class.getAnnotation(MapperScan.class);
assertThat(mapperScan).isNotNull();
assertThat(mapperScan.basePackages())
.containsExactlyInAnyOrder(
"io.github.pnoker.common.dal.mapper",
"io.github.pnoker.common.auth.mapper",
"io.github.pnoker.common.data.mapper",
"io.github.pnoker.common.manager.mapper");
}
}
+6
View File
@@ -74,6 +74,12 @@
</modules>
<dependencies>
<!-- Unit test toolkit inherited by center application modules. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -17,6 +17,7 @@
package io.github.pnoker.common.auth.biz.impl;
import io.github.pnoker.common.auth.cache.TokenDenylistCache;
import io.github.pnoker.common.auth.entity.bean.TokenValid;
import io.github.pnoker.common.auth.entity.bo.TenantBO;
import io.github.pnoker.common.auth.entity.bo.TenantBindBO;
@@ -40,6 +41,8 @@ import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -65,6 +68,9 @@ class TokenServiceImplTest {
@Mock
private TenantBindService tenantBindService;
@Mock
private TokenDenylistCache tokenDenylistCache;
@InjectMocks
private TokenServiceImpl tokenService;
@@ -229,12 +235,27 @@ class TokenServiceImplTest {
when(tenantService.getByCode(TENANT_CODE)).thenReturn(tenant);
when(userLoginService.getByLoginName(LOGIN, false)).thenReturn(userLogin);
when(tenantBindService.getByTenantIdAndUserId(TENANT_ID, USER_ID)).thenReturn(bind);
when(tokenDenylistCache.isRevoked(eq(LOGIN), eq(TENANT_CODE), anyLong())).thenReturn(false);
String token = KeyUtil.generateToken(LOGIN, SALT, TENANT_ID);
TokenValid result = tokenService.checkValid(LOGIN, SALT, token, TENANT_CODE);
assertThat(result.isValid()).isTrue();
assertThat(result.getExpireTime()).isNotNull();
}
@Test
void checkValidReturnsInvalidWhenTokenWasRevoked() {
when(tenantService.getByCode(TENANT_CODE)).thenReturn(tenant);
when(userLoginService.getByLoginName(LOGIN, false)).thenReturn(userLogin);
when(tenantBindService.getByTenantIdAndUserId(TENANT_ID, USER_ID)).thenReturn(bind);
when(tokenDenylistCache.isRevoked(eq(LOGIN), eq(TENANT_CODE), anyLong())).thenReturn(true);
String token = KeyUtil.generateToken(LOGIN, SALT, TENANT_ID);
TokenValid result = tokenService.checkValid(LOGIN, SALT, token, TENANT_CODE);
assertThat(result.isValid()).isFalse();
assertThat(result.getExpireTime()).isNotNull();
}
@Test
void checkValidSwallowsParseFailureAndReturnsInvalid() {
when(tenantService.getByCode(TENANT_CODE)).thenReturn(tenant);
@@ -0,0 +1,200 @@
/*
* 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.gateway.service.impl;
import io.github.pnoker.common.constant.common.RequestConstant;
import io.github.pnoker.common.entity.common.RequestHeader;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.exception.UnAuthorizedException;
import io.github.pnoker.common.facade.api.TenantFacade;
import io.github.pnoker.common.facade.api.TokenFacade;
import io.github.pnoker.common.facade.api.UserFacade;
import io.github.pnoker.common.facade.api.UserLoginFacade;
import io.github.pnoker.common.facade.entity.bo.FacadeTenantBO;
import io.github.pnoker.common.facade.entity.bo.FacadeUserBO;
import io.github.pnoker.common.facade.entity.bo.FacadeUserLoginBO;
import io.github.pnoker.common.utils.JsonUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FilterServiceImplTest {
@Mock
private TenantFacade tenantFacade;
@Mock
private UserLoginFacade userLoginFacade;
@Mock
private UserFacade userFacade;
@Mock
private TokenFacade tokenFacade;
@InjectMocks
private FilterServiceImpl filterService;
@Test
void getTenantRequiresEnabledTenantAndCachesLookup() {
FacadeTenantBO tenant = tenant(11L, "acme", EnableFlagEnum.ENABLE);
when(tenantFacade.getByCode("acme")).thenReturn(tenant);
ServerHttpRequest request = request("acme", "alice", null);
assertThat(filterService.getTenant(request)).isSameAs(tenant);
assertThat(filterService.getTenant(request)).isSameAs(tenant);
verify(tenantFacade, times(1)).getByCode("acme");
}
@Test
void getTenantRejectsMissingOrDisabledTenant() {
assertThatThrownBy(() -> filterService.getTenant(request(null, "alice", null)))
.isInstanceOf(UnAuthorizedException.class);
verifyNoInteractions(tenantFacade);
when(tenantFacade.getByCode("disabled")).thenReturn(tenant(11L, "disabled", EnableFlagEnum.DISABLE));
assertThatThrownBy(() -> filterService.getTenant(request("disabled", "alice", null)))
.isInstanceOf(UnAuthorizedException.class);
}
@Test
void getUserLoginRequiresEnabledLoginAndCachesLookup() {
FacadeUserLoginBO userLogin = userLogin("alice", 7L, EnableFlagEnum.ENABLE);
when(userLoginFacade.getByName("alice")).thenReturn(userLogin);
ServerHttpRequest request = request("acme", "alice", null);
assertThat(filterService.getUserLogin(request)).isSameAs(userLogin);
assertThat(filterService.getUserLogin(request)).isSameAs(userLogin);
verify(userLoginFacade, times(1)).getByName("alice");
}
@Test
void getUserBuildsForwardedHeaderAndCachesUserLookup() {
FacadeTenantBO tenant = tenant(11L, "acme", EnableFlagEnum.ENABLE);
FacadeUserLoginBO userLogin = userLogin("alice", 7L, EnableFlagEnum.ENABLE);
FacadeUserBO user = user(7L, "Alice", "alice");
when(userFacade.getById(7L)).thenReturn(user);
RequestHeader.UserHeader header = filterService.getUser(userLogin, tenant);
RequestHeader.UserHeader cachedHeader = filterService.getUser(userLogin, tenant);
assertThat(header.getUserId()).isEqualTo(7L);
assertThat(header.getNickName()).isEqualTo("Alice");
assertThat(header.getUserName()).isEqualTo("alice");
assertThat(header.getTenantId()).isEqualTo(11L);
assertThat(cachedHeader.getUserName()).isEqualTo("alice");
verify(userFacade, times(1)).getById(7L);
}
@Test
void getUserRejectsLoginWithoutUserIdAndMissingUser() {
FacadeTenantBO tenant = tenant(11L, "acme", EnableFlagEnum.ENABLE);
assertThatThrownBy(() -> filterService.getUser(userLogin("alice", null, EnableFlagEnum.ENABLE), tenant))
.isInstanceOf(UnAuthorizedException.class);
verify(userFacade, never()).getById(null);
when(userFacade.getById(7L)).thenReturn(null);
assertThatThrownBy(() -> filterService.getUser(userLogin("alice", 7L, EnableFlagEnum.ENABLE), tenant))
.isInstanceOf(UnAuthorizedException.class);
}
@Test
void checkValidParsesHeaderAndDoesNotCacheTokenValidation() {
FacadeTenantBO tenant = tenant(11L, "acme", EnableFlagEnum.ENABLE);
FacadeUserLoginBO userLogin = userLogin("alice", 7L, EnableFlagEnum.ENABLE);
String tokenHeader = JsonUtil.toJsonString(new RequestHeader.TokenHeader("salt", "token"));
ServerHttpRequest request = request("acme", "alice", tokenHeader);
when(tokenFacade.checkValid("acme", "alice", "salt", "token")).thenReturn(true);
filterService.checkValid(request, tenant, userLogin);
filterService.checkValid(request, tenant, userLogin);
verify(tokenFacade, times(2)).checkValid("acme", "alice", "salt", "token");
}
@Test
void checkValidRejectsMalformedMissingOrInvalidToken() {
FacadeTenantBO tenant = tenant(11L, "acme", EnableFlagEnum.ENABLE);
FacadeUserLoginBO userLogin = userLogin("alice", 7L, EnableFlagEnum.ENABLE);
assertThatThrownBy(() -> filterService.checkValid(request("acme", "alice", "{"), tenant, userLogin))
.isInstanceOf(UnAuthorizedException.class);
assertThatThrownBy(() -> filterService.checkValid(request("acme", "alice",
JsonUtil.toJsonString(new RequestHeader.TokenHeader("salt", ""))), tenant, userLogin))
.isInstanceOf(UnAuthorizedException.class);
when(tokenFacade.checkValid("acme", "alice", "salt", "token")).thenReturn(false);
assertThatThrownBy(() -> filterService.checkValid(request("acme", "alice",
JsonUtil.toJsonString(new RequestHeader.TokenHeader("salt", "token"))), tenant, userLogin))
.isInstanceOf(UnAuthorizedException.class);
}
private static ServerHttpRequest request(String tenant, String login, String token) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest.get("/api/manager/device");
if (tenant != null) {
builder.header(RequestConstant.Header.X_AUTH_TENANT, tenant);
}
if (login != null) {
builder.header(RequestConstant.Header.X_AUTH_LOGIN, login);
}
if (token != null) {
builder.header(RequestConstant.Header.X_AUTH_TOKEN, token);
}
return builder.build();
}
private static FacadeTenantBO tenant(Long id, String code, EnableFlagEnum enableFlag) {
FacadeTenantBO tenant = new FacadeTenantBO();
tenant.setId(id);
tenant.setTenantCode(code);
tenant.setEnableFlag(enableFlag);
return tenant;
}
private static FacadeUserLoginBO userLogin(String name, Long userId, EnableFlagEnum enableFlag) {
FacadeUserLoginBO userLogin = new FacadeUserLoginBO();
userLogin.setLoginName(name);
userLogin.setUserId(userId);
userLogin.setEnableFlag(enableFlag);
return userLogin;
}
private static FacadeUserBO user(Long id, String nickName, String userName) {
FacadeUserBO user = new FacadeUserBO();
user.setId(id);
user.setNickName(nickName);
user.setUserName(userName);
return user;
}
}
+8 -1
View File
@@ -91,6 +91,13 @@
<version>${logstash.logback.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>
@@ -0,0 +1,126 @@
/*
* 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.annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.junit.jupiter.api.Test;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LogsAspectTest {
private final LogsAspect logsAspect = new LogsAspect();
@Test
void doAroundReturnsProceedResult() throws Throwable {
ProceedingJoinPoint joinPoint = joinPoint("create");
Logs logs = annotatedMethod("defaultOperation").getAnnotation(Logs.class);
when(joinPoint.proceed()).thenReturn("ok");
Object result = logsAspect.doAround(joinPoint, logs);
assertThat(result).isEqualTo("ok");
verify(joinPoint).proceed();
}
@Test
void doAroundRethrowsProceedFailure() throws Throwable {
ProceedingJoinPoint joinPoint = joinPoint("delete");
Logs logs = annotatedMethod("warningOperation").getAnnotation(Logs.class);
IllegalStateException failure = new NoStackTraceException("boom");
when(joinPoint.proceed()).thenThrow(failure);
assertThatThrownBy(() -> logsAspect.doAround(joinPoint, logs))
.isSameAs(failure);
verify(joinPoint).proceed();
}
@Test
void logsAnnotationKeepsRuntimeMethodContract() throws NoSuchMethodException {
Logs logs = annotatedMethod("defaultOperation").getAnnotation(Logs.class);
assertThat(logs.value()).isEqualTo("sync-resource");
assertThat(logs.type()).isEqualTo(LogsType.INFO);
assertThat(logs.tag()).isEmpty();
assertThat(logs.save()).isFalse();
assertThat(Logs.class.getAnnotation(Retention.class).value())
.isEqualTo(java.lang.annotation.RetentionPolicy.RUNTIME);
assertThat(Logs.class.getAnnotation(Target.class).value()).containsExactly(ElementType.METHOD);
}
@Test
void logsAnnotationAllowsExplicitMetadata() throws NoSuchMethodException {
Logs logs = annotatedMethod("warningOperation").getAnnotation(Logs.class);
assertThat(logs.value()).isEqualTo("warn-resource");
assertThat(logs.type()).isEqualTo(LogsType.WARN);
assertThat(logs.tag()).isEqualTo("resource");
assertThat(logs.save()).isTrue();
assertThat(LogsType.values())
.containsExactly(LogsType.INFO, LogsType.WARN, LogsType.DEBUG, LogsType.ERROR);
}
private static ProceedingJoinPoint joinPoint(String methodName) {
Signature signature = mock(Signature.class);
when(signature.getDeclaringType()).thenReturn(AnnotatedOperations.class);
when(signature.getName()).thenReturn(methodName);
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
when(joinPoint.getSignature()).thenReturn(signature);
return joinPoint;
}
private static java.lang.reflect.Method annotatedMethod(String name) throws NoSuchMethodException {
return AnnotatedOperations.class.getDeclaredMethod(name);
}
private static final class AnnotatedOperations {
@Logs("sync-resource")
void defaultOperation() {
}
@Logs(value = "warn-resource", type = LogsType.WARN, tag = "resource", save = true)
void warningOperation() {
}
}
private static final class NoStackTraceException extends IllegalStateException {
private NoStackTraceException(String message) {
super(message);
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<logger name="io.github.pnoker.common.annotation.LogsAspect" level="OFF"/>
</configuration>
+87
View File
@@ -72,6 +72,12 @@
<!-- DC3 Version -->
<dc3.version>2026.5.18</dc3.version>
<!-- Test execution toggles (consumed by surefire/failsafe) -->
<skip.unit.tests>false</skip.unit.tests>
<skip.integration.tests>false</skip.integration.tests>
<mockito.version>5.20.0</mockito.version>
<mockito.javaagent.argLine>-javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar</mockito.javaagent.argLine>
</properties>
<modules>
@@ -120,6 +126,87 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<skip>${skip.unit.tests}</skip>
<argLine>@{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*IT.java</exclude>
<exclude>**/Abstract*.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<skip>${skip.integration.tests}</skip>
<argLine>@{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*IT.java</include>
</includes>
</configuration>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-unit-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<propertyName>argLine</propertyName>
</configuration>
</execution>
<execution>
<id>prepare-integration-agent</id>
<goals>
<goal>prepare-agent-integration</goal>
</goals>
<configuration>
<propertyName>failsafe.argLine</propertyName>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
</configuration>
</execution>
<execution>
<id>report-unit</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>report-integration</id>
<phase>verify</phase>
<goals>
<goal>report-integration</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
+158 -27
View File
@@ -37,20 +37,41 @@
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
<!-- Coverage thresholds gated by jacoco:check. Tightened per-stage. -->
<coverage.line.minimum>0.00</coverage.line.minimum>
<coverage.branch.minimum>0.00</coverage.branch.minimum>
<!-- 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>
<coverage.check.skip>false</coverage.check.skip>
</properties>
<dependencies>
<!--
Modules listed here contribute to the aggregate report. As new test
stages land, the contributing modules are added below so their
jacoco.exec / jacoco-it.exec files are picked up.
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.
-->
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-public</artifactId>
<artifactId>dc3-common-agentic</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-auth</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-constant</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-data</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-driver</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
@@ -60,7 +81,117 @@
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-constant</artifactId>
<artifactId>dc3-common-facade-local-auth</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-facade-local-data</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-facade-local-manager</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-gateway</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-manager</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-model</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-mqtt</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-postgres</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-public</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-quartz</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-rabbitmq</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-repository</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-resource-registrar</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-thread</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-common-web</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-listening-virtual</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-modbus-tcp</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-mqtt</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-opc-da</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-opc-ua</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-virtual</artifactId>
<version>${dc3.version}</version>
</dependency>
</dependencies>
@@ -93,30 +224,30 @@
</excludes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>aggregate-check</id>
<id>aggregate-coverage-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
<goal>exec</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>${coverage.line.minimum}</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>${coverage.branch.minimum}</minimum>
</limit>
</limits>
</rule>
</rules>
<skip>${coverage.check.skip}</skip>
<executable>python3</executable>
<arguments>
<argument>${project.basedir}/scripts/check_coverage.py</argument>
<argument>${project.build.directory}/site/jacoco-aggregate/jacoco.xml</argument>
<argument>--minimum-line</argument>
<argument>${coverage.line.minimum}</argument>
<argument>--minimum-branch</argument>
<argument>${coverage.branch.minimum}</argument>
</arguments>
</configuration>
</execution>
</executions>
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Validate the aggregate JaCoCo XML report against repository thresholds."""
from __future__ import annotations
import argparse
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Counter:
name: str
missed: int
covered: int
@property
def total(self) -> int:
return self.missed + self.covered
@property
def ratio(self) -> float:
if self.total == 0:
return 1.0
return self.covered / self.total
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Fail the build if aggregate JaCoCo coverage is below threshold.",
)
parser.add_argument("xml_report", type=Path, help="Path to jacoco.xml")
parser.add_argument("--minimum-line", type=float, required=True)
parser.add_argument("--minimum-branch", type=float, required=True)
return parser.parse_args()
def read_counters(xml_report: Path) -> dict[str, Counter]:
if not xml_report.is_file():
raise FileNotFoundError(f"JaCoCo XML report does not exist: {xml_report}")
root = ET.parse(xml_report).getroot()
counters: dict[str, Counter] = {}
for element in root.findall("counter"):
name = element.attrib["type"]
counters[name] = Counter(
name=name,
missed=int(element.attrib["missed"]),
covered=int(element.attrib["covered"]),
)
return counters
def format_percent(value: float) -> str:
return f"{value * 100:.2f}%"
def check(counter: Counter, minimum: float) -> bool:
print(
f"Aggregate {counter.name.lower()} coverage: "
f"{format_percent(counter.ratio)} "
f"({counter.covered}/{counter.total}), "
f"minimum {format_percent(minimum)}"
)
return counter.ratio >= minimum
def main() -> int:
args = parse_args()
try:
counters = read_counters(args.xml_report)
except Exception as exc: # noqa: BLE001 - build tool should show the exact cause.
print(f"Coverage check failed: {exc}", file=sys.stderr)
return 1
required = {
"LINE": args.minimum_line,
"BRANCH": args.minimum_branch,
}
missing = [name for name in required if name not in counters]
if missing:
print(
f"Coverage check failed: missing counters {', '.join(missing)}",
file=sys.stderr,
)
return 1
passed = True
for name, minimum in required.items():
passed = check(counters[name], minimum) and passed
if not passed:
print("Coverage check failed: aggregate coverage is below threshold.", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,35 @@
/*
* 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.e2e;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("e2e")
class E2eEnvironmentGuardIT {
@Test
void e2eProfileRequiresExplicitEnvironmentOptIn() {
assertThat(System.getenv("DC3_E2E"))
.as("Set DC3_E2E=true when running -Pe2e so the gated E2E classes execute")
.matches("(?i)true|1|yes|on");
}
}
+8 -1
View File
@@ -79,6 +79,13 @@
<artifactId>dc3-common-gateway</artifactId>
</dependency>
<!-- Unit test toolkit -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -90,4 +97,4 @@
</plugins>
</build>
</project>
</project>
@@ -0,0 +1,32 @@
/*
* 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.gateway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.assertj.core.api.Assertions.assertThat;
class GatewayApplicationTest {
@Test
void applicationIsSpringBootEntryPoint() {
assertThat(GatewayApplication.class.isAnnotationPresent(SpringBootApplication.class)).isTrue();
}
}
+6 -4
View File
@@ -76,6 +76,8 @@
<!-- Test execution toggles (consumed by surefire/failsafe) -->
<skip.unit.tests>false</skip.unit.tests>
<skip.integration.tests>false</skip.integration.tests>
<mockito.version>5.20.0</mockito.version>
<mockito.javaagent.argLine>-javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar</mockito.javaagent.argLine>
</properties>
<modules>
@@ -266,14 +268,14 @@
<version>${surefire.version}</version>
<configuration>
<skip>${skip.unit.tests}</skip>
<argLine>@{argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<argLine>@{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<include>**/*IT.java</include>
<include>**/Abstract*.java</include>
<exclude>**/*IT.java</exclude>
<exclude>**/Abstract*.java</exclude>
</excludes>
</configuration>
</plugin>
@@ -283,7 +285,7 @@
<version>${surefire.version}</version>
<configuration>
<skip>${skip.integration.tests}</skip>
<argLine>@{failsafe.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<argLine>@{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US</argLine>
<includes>
<include>**/*IT.java</include>
</includes>