From 5a3ab1440aa47f7072cd190bbb84cf2fff14b82a Mon Sep 17 00:00:00 2001 From: pnoker Date: Tue, 19 May 2026 18:19:12 +0800 Subject: [PATCH] test: improve backend test coverage governance --- Makefile | 10 +- dc3-api/pom.xml | 87 ++++++++ .../agentic/AgenticApplicationTest.java | 32 +++ .../center/auth/AuthApplicationTest.java | 32 +++ .../center/data/DataApplicationTest.java | 32 +++ .../manager/ManagerApplicationTest.java | 32 +++ .../pnoker/center/SingleApplicationTest.java | 46 ++++ dc3-center/pom.xml | 6 + .../auth/biz/impl/TokenServiceImplTest.java | 21 ++ .../service/impl/FilterServiceImplTest.java | 200 ++++++++++++++++++ dc3-common/dc3-common-log/pom.xml | 9 +- .../common/annotation/LogsAspectTest.java | 126 +++++++++++ .../src/test/resources/logback-test.xml | 4 + dc3-common/pom.xml | 87 ++++++++ dc3-coverage/pom.xml | 185 +++++++++++++--- dc3-coverage/scripts/check_coverage.py | 102 +++++++++ .../pnoker/e2e/E2eEnvironmentGuardIT.java | 35 +++ dc3-gateway/pom.xml | 9 +- .../gateway/GatewayApplicationTest.java | 32 +++ pom.xml | 10 +- 20 files changed, 1060 insertions(+), 37 deletions(-) create mode 100644 dc3-center/dc3-center-agentic/src/test/java/io/github/pnoker/center/agentic/AgenticApplicationTest.java create mode 100644 dc3-center/dc3-center-auth/src/test/java/io/github/pnoker/center/auth/AuthApplicationTest.java create mode 100644 dc3-center/dc3-center-data/src/test/java/io/github/pnoker/center/data/DataApplicationTest.java create mode 100644 dc3-center/dc3-center-manager/src/test/java/io/github/pnoker/center/manager/ManagerApplicationTest.java create mode 100644 dc3-center/dc3-center-single/src/test/java/io/github/pnoker/center/SingleApplicationTest.java create mode 100644 dc3-common/dc3-common-gateway/src/test/java/io/github/pnoker/common/gateway/service/impl/FilterServiceImplTest.java create mode 100644 dc3-common/dc3-common-log/src/test/java/io/github/pnoker/common/annotation/LogsAspectTest.java create mode 100644 dc3-common/dc3-common-log/src/test/resources/logback-test.xml create mode 100644 dc3-coverage/scripts/check_coverage.py create mode 100644 dc3-e2e/src/test/java/io/github/pnoker/e2e/E2eEnvironmentGuardIT.java create mode 100644 dc3-gateway/src/test/java/io/github/pnoker/gateway/GatewayApplicationTest.java diff --git a/Makefile b/Makefile index 8294322f6..8d75ed098 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/dc3-api/pom.xml b/dc3-api/pom.xml index 4ae740a4a..b5357a098 100644 --- a/dc3-api/pom.xml +++ b/dc3-api/pom.xml @@ -70,6 +70,12 @@ 2026.5.18 + + + false + false + 5.20.0 + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar @@ -117,6 +123,87 @@ org.xolstice.maven.plugins protobuf-maven-plugin + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + ${skip.unit.tests} + @{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US + + **/*Test.java + **/*Tests.java + + + **/*IT.java + **/Abstract*.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire.version} + + ${skip.integration.tests} + @{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US + + **/*IT.java + + + + + integration-tests + + integration-test + verify + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-unit-agent + + prepare-agent + + + argLine + + + + prepare-integration-agent + + prepare-agent-integration + + + failsafe.argLine + ${project.build.directory}/jacoco-it.exec + + + + report-unit + test + + report + + + + report-integration + verify + + report-integration + + + ${project.build.directory}/jacoco-it.exec + + + + diff --git a/dc3-center/dc3-center-agentic/src/test/java/io/github/pnoker/center/agentic/AgenticApplicationTest.java b/dc3-center/dc3-center-agentic/src/test/java/io/github/pnoker/center/agentic/AgenticApplicationTest.java new file mode 100644 index 000000000..aa7c4dc83 --- /dev/null +++ b/dc3-center/dc3-center-agentic/src/test/java/io/github/pnoker/center/agentic/AgenticApplicationTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/dc3-center/dc3-center-auth/src/test/java/io/github/pnoker/center/auth/AuthApplicationTest.java b/dc3-center/dc3-center-auth/src/test/java/io/github/pnoker/center/auth/AuthApplicationTest.java new file mode 100644 index 000000000..79699342c --- /dev/null +++ b/dc3-center/dc3-center-auth/src/test/java/io/github/pnoker/center/auth/AuthApplicationTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/dc3-center/dc3-center-data/src/test/java/io/github/pnoker/center/data/DataApplicationTest.java b/dc3-center/dc3-center-data/src/test/java/io/github/pnoker/center/data/DataApplicationTest.java new file mode 100644 index 000000000..ad68de715 --- /dev/null +++ b/dc3-center/dc3-center-data/src/test/java/io/github/pnoker/center/data/DataApplicationTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/dc3-center/dc3-center-manager/src/test/java/io/github/pnoker/center/manager/ManagerApplicationTest.java b/dc3-center/dc3-center-manager/src/test/java/io/github/pnoker/center/manager/ManagerApplicationTest.java new file mode 100644 index 000000000..857151096 --- /dev/null +++ b/dc3-center/dc3-center-manager/src/test/java/io/github/pnoker/center/manager/ManagerApplicationTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/dc3-center/dc3-center-single/src/test/java/io/github/pnoker/center/SingleApplicationTest.java b/dc3-center/dc3-center-single/src/test/java/io/github/pnoker/center/SingleApplicationTest.java new file mode 100644 index 000000000..ea4760f57 --- /dev/null +++ b/dc3-center/dc3-center-single/src/test/java/io/github/pnoker/center/SingleApplicationTest.java @@ -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 . + */ + +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"); + } + +} diff --git a/dc3-center/pom.xml b/dc3-center/pom.xml index 3f240fba4..c6771e669 100644 --- a/dc3-center/pom.xml +++ b/dc3-center/pom.xml @@ -74,6 +74,12 @@ + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/biz/impl/TokenServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/biz/impl/TokenServiceImplTest.java index e6276ba65..bc053bcac 100644 --- a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/biz/impl/TokenServiceImplTest.java +++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/biz/impl/TokenServiceImplTest.java @@ -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); diff --git a/dc3-common/dc3-common-gateway/src/test/java/io/github/pnoker/common/gateway/service/impl/FilterServiceImplTest.java b/dc3-common/dc3-common-gateway/src/test/java/io/github/pnoker/common/gateway/service/impl/FilterServiceImplTest.java new file mode 100644 index 000000000..ff1a350a8 --- /dev/null +++ b/dc3-common/dc3-common-gateway/src/test/java/io/github/pnoker/common/gateway/service/impl/FilterServiceImplTest.java @@ -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 . + */ + +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; + } + +} diff --git a/dc3-common/dc3-common-log/pom.xml b/dc3-common/dc3-common-log/pom.xml index 18672fdb2..be56b1125 100644 --- a/dc3-common/dc3-common-log/pom.xml +++ b/dc3-common/dc3-common-log/pom.xml @@ -91,6 +91,13 @@ ${logstash.logback.version} + + + org.springframework.boot + spring-boot-starter-test + test + + - \ No newline at end of file + diff --git a/dc3-common/dc3-common-log/src/test/java/io/github/pnoker/common/annotation/LogsAspectTest.java b/dc3-common/dc3-common-log/src/test/java/io/github/pnoker/common/annotation/LogsAspectTest.java new file mode 100644 index 000000000..d76256d32 --- /dev/null +++ b/dc3-common/dc3-common-log/src/test/java/io/github/pnoker/common/annotation/LogsAspectTest.java @@ -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 . + */ + +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; + } + + } + +} diff --git a/dc3-common/dc3-common-log/src/test/resources/logback-test.xml b/dc3-common/dc3-common-log/src/test/resources/logback-test.xml new file mode 100644 index 000000000..8d7c97c6a --- /dev/null +++ b/dc3-common/dc3-common-log/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/dc3-common/pom.xml b/dc3-common/pom.xml index 9cd722311..e6d0d5b8a 100644 --- a/dc3-common/pom.xml +++ b/dc3-common/pom.xml @@ -72,6 +72,12 @@ 2026.5.18 + + + false + false + 5.20.0 + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar @@ -120,6 +126,87 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + ${skip.unit.tests} + @{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US + + **/*Test.java + **/*Tests.java + + + **/*IT.java + **/Abstract*.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${surefire.version} + + ${skip.integration.tests} + @{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US + + **/*IT.java + + + + + integration-tests + + integration-test + verify + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-unit-agent + + prepare-agent + + + argLine + + + + prepare-integration-agent + + prepare-agent-integration + + + failsafe.argLine + ${project.build.directory}/jacoco-it.exec + + + + report-unit + test + + report + + + + report-integration + verify + + report-integration + + + ${project.build.directory}/jacoco-it.exec + + + + diff --git a/dc3-coverage/pom.xml b/dc3-coverage/pom.xml index 2684b5c49..ff060ae3f 100644 --- a/dc3-coverage/pom.xml +++ b/dc3-coverage/pom.xml @@ -37,20 +37,41 @@ true - - 0.00 - 0.00 + + 0.20 + 0.15 + false io.github.pnoker - dc3-common-public + dc3-common-agentic + ${dc3.version} + + + io.github.pnoker + dc3-common-auth + ${dc3.version} + + + io.github.pnoker + dc3-common-constant + ${dc3.version} + + + io.github.pnoker + dc3-common-data + ${dc3.version} + + + io.github.pnoker + dc3-common-driver ${dc3.version} @@ -60,7 +81,117 @@ io.github.pnoker - dc3-common-constant + dc3-common-facade-local-auth + ${dc3.version} + + + io.github.pnoker + dc3-common-facade-local-data + ${dc3.version} + + + io.github.pnoker + dc3-common-facade-local-manager + ${dc3.version} + + + io.github.pnoker + dc3-common-gateway + ${dc3.version} + + + io.github.pnoker + dc3-common-manager + ${dc3.version} + + + io.github.pnoker + dc3-common-model + ${dc3.version} + + + io.github.pnoker + dc3-common-mqtt + ${dc3.version} + + + io.github.pnoker + dc3-common-postgres + ${dc3.version} + + + io.github.pnoker + dc3-common-public + ${dc3.version} + + + io.github.pnoker + dc3-common-quartz + ${dc3.version} + + + io.github.pnoker + dc3-common-rabbitmq + ${dc3.version} + + + io.github.pnoker + dc3-common-repository + ${dc3.version} + + + io.github.pnoker + dc3-common-resource-registrar + ${dc3.version} + + + io.github.pnoker + dc3-common-thread + ${dc3.version} + + + io.github.pnoker + dc3-common-web + ${dc3.version} + + + io.github.pnoker + dc3-driver-coap + ${dc3.version} + + + io.github.pnoker + dc3-driver-listening-virtual + ${dc3.version} + + + io.github.pnoker + dc3-driver-modbus-tcp + ${dc3.version} + + + io.github.pnoker + dc3-driver-mqtt + ${dc3.version} + + + io.github.pnoker + dc3-driver-opc-da + ${dc3.version} + + + io.github.pnoker + dc3-driver-opc-ua + ${dc3.version} + + + io.github.pnoker + dc3-driver-plcs7 + ${dc3.version} + + + io.github.pnoker + dc3-driver-virtual ${dc3.version} @@ -93,30 +224,30 @@ + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + - aggregate-check + aggregate-coverage-check verify - check + exec - - - BUNDLE - - - LINE - COVEREDRATIO - ${coverage.line.minimum} - - - BRANCH - COVEREDRATIO - ${coverage.branch.minimum} - - - - + ${coverage.check.skip} + python3 + + ${project.basedir}/scripts/check_coverage.py + ${project.build.directory}/site/jacoco-aggregate/jacoco.xml + --minimum-line + ${coverage.line.minimum} + --minimum-branch + ${coverage.branch.minimum} + diff --git a/dc3-coverage/scripts/check_coverage.py b/dc3-coverage/scripts/check_coverage.py new file mode 100644 index 000000000..94c0e2b77 --- /dev/null +++ b/dc3-coverage/scripts/check_coverage.py @@ -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()) diff --git a/dc3-e2e/src/test/java/io/github/pnoker/e2e/E2eEnvironmentGuardIT.java b/dc3-e2e/src/test/java/io/github/pnoker/e2e/E2eEnvironmentGuardIT.java new file mode 100644 index 000000000..c6658f2d0 --- /dev/null +++ b/dc3-e2e/src/test/java/io/github/pnoker/e2e/E2eEnvironmentGuardIT.java @@ -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 . + */ + +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"); + } + +} diff --git a/dc3-gateway/pom.xml b/dc3-gateway/pom.xml index c21767060..9fcb7bddd 100644 --- a/dc3-gateway/pom.xml +++ b/dc3-gateway/pom.xml @@ -79,6 +79,13 @@ dc3-common-gateway + + + org.springframework.boot + spring-boot-starter-test + test + + @@ -90,4 +97,4 @@ - \ No newline at end of file + diff --git a/dc3-gateway/src/test/java/io/github/pnoker/gateway/GatewayApplicationTest.java b/dc3-gateway/src/test/java/io/github/pnoker/gateway/GatewayApplicationTest.java new file mode 100644 index 000000000..f7ccfde7f --- /dev/null +++ b/dc3-gateway/src/test/java/io/github/pnoker/gateway/GatewayApplicationTest.java @@ -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 . + */ + +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(); + } + +} diff --git a/pom.xml b/pom.xml index 260d42083..341a964b7 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,8 @@ false false + 5.20.0 + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar @@ -266,14 +268,14 @@ ${surefire.version} ${skip.unit.tests} - @{argLine} -Xshare:off -Duser.language=en -Duser.country=US + @{argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US **/*Test.java **/*Tests.java - **/*IT.java - **/Abstract*.java + **/*IT.java + **/Abstract*.java @@ -283,7 +285,7 @@ ${surefire.version} ${skip.integration.tests} - @{failsafe.argLine} -Xshare:off -Duser.language=en -Duser.country=US + @{failsafe.argLine} ${mockito.javaagent.argLine} -Xshare:off -Duser.language=en -Duser.country=US **/*IT.java