mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-08-31 01:39:47 +08:00
test: improve backend test coverage governance
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+32
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+21
@@ -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);
|
||||
|
||||
+200
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+126
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user