forked from baskbuddy/zhangshun
14 changed files with 478 additions and 12 deletions
@ -0,0 +1,33 @@ |
|||||
|
package com.mydreamjob.salarycalculator; |
||||
|
|
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.Map; |
||||
|
import lombok.Getter; |
||||
|
import lombok.Setter; |
||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
/** 发布白名单为空时失败关闭;enabled/dataLevel 不表示政策已核验。 */ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
@Component |
||||
|
@ConfigurationProperties(prefix = "app.salary-publication") |
||||
|
public class SalaryPolicyPublicationProperties { |
||||
|
private Map<String, String> approvedPolicies = new LinkedHashMap<>(); |
||||
|
|
||||
|
@PostConstruct |
||||
|
void validate() { |
||||
|
if (approvedPolicies == null || approvedPolicies.entrySet().stream().anyMatch(entry -> |
||||
|
entry.getKey() == null || !entry.getKey().matches("[0-9]{6}") |
||||
|
|| entry.getValue() == null || entry.getValue().isBlank() |
||||
|
|| !entry.getValue().equals(entry.getValue().trim()))) { |
||||
|
throw new IllegalStateException("invalid salary publication allowlist"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean isPublished(String cityCode, String policyVersion) { |
||||
|
return cityCode != null && policyVersion != null && approvedPolicies != null |
||||
|
&& policyVersion.equals(approvedPolicies.get(cityCode)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
package com.mydreamjob.integration; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.mydreamjob.auth.AuthSessionService; |
||||
|
import com.mydreamjob.auth.UserRepository; |
||||
|
import java.util.*; |
||||
|
import org.junit.jupiter.api.*; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.http.MediaType; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.test.context.*; |
||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||
|
import org.testcontainers.containers.MySQLContainer; |
||||
|
import org.testcontainers.junit.jupiter.*; |
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; |
||||
|
|
||||
|
/** 默认发布配置必须拦截所有未核验种子;真实 MySQL,Redis mock、MQ 关闭。 */ |
||||
|
@SpringBootTest |
||||
|
@AutoConfigureMockMvc |
||||
|
@ActiveProfiles("mysql-it") |
||||
|
@Testcontainers(disabledWithoutDocker = false) |
||||
|
class SalaryPublicationDefaultMySqlTest { |
||||
|
@Container static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.4") |
||||
|
.withDatabaseName("salary_publication_test") |
||||
|
.withUsername("publication_test").withPassword("publication-test-only"); |
||||
|
@DynamicPropertySource static void database(DynamicPropertyRegistry r) { |
||||
|
r.add("spring.datasource.url", MYSQL::getJdbcUrl); |
||||
|
r.add("spring.datasource.username", MYSQL::getUsername); |
||||
|
r.add("spring.datasource.password", MYSQL::getPassword); |
||||
|
} |
||||
|
@MockBean RedisTemplate<String, Object> redis; |
||||
|
@Autowired JdbcTemplate jdbc; |
||||
|
@Autowired MockMvc mvc; |
||||
|
@Autowired AuthSessionService sessions; |
||||
|
@Autowired UserRepository users; |
||||
|
@Autowired ObjectMapper mapper; |
||||
|
@Autowired com.mydreamjob.salarycalculator.SalaryRequestFingerprint fingerprint; |
||||
|
private String token; |
||||
|
@BeforeEach void identity() { |
||||
|
token = "Bearer " + LxqTestSessionIssuer.issue(92001, "STUDENT", sessions, users, jdbc); |
||||
|
} |
||||
|
@AfterEach void cleanup() { |
||||
|
jdbc.update("delete from salary_calculation_history where student_user_id=?", 92001); |
||||
|
LxqTestSessionIssuer.cleanup(jdbc); |
||||
|
} |
||||
|
private String request() { |
||||
|
return "{\"cityCode\":\"110000\",\"grossMonthlySalary\":10000,\"monthIndex\":1," |
||||
|
+ "\"specialAdditionalDeduction\":0,\"expectedPolicyVersion\":\"2026-09-20-v1\"," |
||||
|
+ "\"idempotencyKey\":\"publication-test\"}"; |
||||
|
} |
||||
|
private Map<String, List<String>> snapshot() { |
||||
|
Map<String, List<String>> result = new TreeMap<>(); |
||||
|
for (String table : jdbc.queryForList("show tables", String.class)) { |
||||
|
List<String> rows = jdbc.query("select * from `" + table + "`", (rs, n) -> { |
||||
|
List<String> columns = new ArrayList<>(); |
||||
|
for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { |
||||
|
columns.add(rs.getMetaData().getColumnName(i) + "=" + rs.getString(i)); |
||||
|
} |
||||
|
return columns.toString(); |
||||
|
}); |
||||
|
rows.sort(Comparator.naturalOrder()); |
||||
|
result.put(table, rows); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
@Test void seedsRemainButDefaultCityListIsEmptyAndReadOnly() throws Exception { |
||||
|
assertThat(jdbc.queryForObject("select count(*) from salary_city_policy", Integer.class)).isEqualTo(359); |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(get("/salary-calculator/cities").header("Authorization", token)) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data").isEmpty()); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
@Test void directCityLookupIsClosedWithoutSideEffects() throws Exception { |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(get("/salary-calculator/cities/110000").header("Authorization", token)) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10601)); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
@Test void calculationIsClosedWithoutSideEffects() throws Exception { |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(post("/salary-calculator/calculations").header("Authorization", token) |
||||
|
.contentType(MediaType.APPLICATION_JSON).content(request())) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10601)); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
@Test void anonymousIsStillUnauthorized() throws Exception { |
||||
|
mvc.perform(get("/salary-calculator/cities")).andExpect(status().isUnauthorized()); |
||||
|
} |
||||
|
@Test void historyListIsStillAvailable() throws Exception { |
||||
|
mvc.perform(get("/salary-calculator/calculations").header("Authorization", token)) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
@Test void existingHistoryIsReadableButCannotBypassPublicationThroughReplay() throws Exception { |
||||
|
var body = mapper.readValue(request(), com.mydreamjob.salarycalculator.dto.SalaryCalculationRequest.class); |
||||
|
jdbc.update("insert into salary_calculation_history(student_user_id,city_code,city_name,data_level," |
||||
|
+ "policy_version,tax_rules_version,month_index,gross_monthly_salary,net_salary," |
||||
|
+ "idempotency_key,request_hash,input_snapshot_json,policy_snapshot_json,result_snapshot_json)" |
||||
|
+ " values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)", |
||||
|
92001, "110000", "test-city", "CITY", "2026-09-20-v1", "test-tax-version", |
||||
|
1, 10000, 8000, "publication-test", fingerprint.fingerprint(body), "{}", "{}", "{}"); |
||||
|
Long id = jdbc.queryForObject("select id from salary_calculation_history where student_user_id=?", |
||||
|
Long.class, 92001); |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(get("/salary-calculator/calculations/" + id).header("Authorization", token)) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.id").value(id)); |
||||
|
mvc.perform(post("/salary-calculator/calculations").header("Authorization", token) |
||||
|
.contentType(MediaType.APPLICATION_JSON).content(request())) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10601)); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
@Test void n03AndN04RemainAvailable() throws Exception { |
||||
|
mvc.perform(get("/onboarding/company/checklist-items").header("Authorization", token)) |
||||
|
.andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value(2002)); |
||||
|
// 接口仍然存在,原角色门禁不被薪资发布配置影响。
|
||||
|
mvc.perform(get("/v3/api-docs")).andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.paths['/skills/word-cloud']").exists()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,169 @@ |
|||||
|
package com.mydreamjob.integration; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.mydreamjob.auth.AuthSessionService; |
||||
|
import com.mydreamjob.auth.UserRepository; |
||||
|
import com.mydreamjob.salarycalculator.SalaryPolicyPublicationProperties; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Comparator; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.TreeMap; |
||||
|
import org.junit.jupiter.api.AfterEach; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.http.MediaType; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.test.context.ActiveProfiles; |
||||
|
import org.springframework.test.context.DynamicPropertyRegistry; |
||||
|
import org.springframework.test.context.DynamicPropertySource; |
||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||
|
import org.testcontainers.containers.MySQLContainer; |
||||
|
import org.testcontainers.junit.jupiter.Container; |
||||
|
import org.testcontainers.junit.jupiter.Testcontainers; |
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; |
||||
|
|
||||
|
/** 独立复验撤销、版本漂移、旧历史与归属边界;不证明现实政策准确性。 */ |
||||
|
@SpringBootTest |
||||
|
@AutoConfigureMockMvc |
||||
|
@ActiveProfiles("mysql-it") |
||||
|
@Testcontainers(disabledWithoutDocker = false) |
||||
|
class SalaryPublicationIndependentMySqlVerificationTest { |
||||
|
private static final long OWNER = 93001; |
||||
|
private static final long OTHER = 93002; |
||||
|
private static final String VERSION = "2026-09-20-v1"; |
||||
|
@Container static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.4") |
||||
|
.withDatabaseName("salary_independent_test") |
||||
|
.withUsername("salary_test").withPassword("independent-test-only"); |
||||
|
@DynamicPropertySource static void database(DynamicPropertyRegistry registry) { |
||||
|
registry.add("spring.datasource.url", MYSQL::getJdbcUrl); |
||||
|
registry.add("spring.datasource.username", MYSQL::getUsername); |
||||
|
registry.add("spring.datasource.password", MYSQL::getPassword); |
||||
|
} |
||||
|
@MockBean RedisTemplate<String, Object> redis; |
||||
|
@Autowired JdbcTemplate jdbc; |
||||
|
@Autowired MockMvc mvc; |
||||
|
@Autowired AuthSessionService sessions; |
||||
|
@Autowired UserRepository users; |
||||
|
@Autowired SalaryPolicyPublicationProperties publication; |
||||
|
@Autowired ObjectMapper mapper; |
||||
|
private String ownerToken; |
||||
|
|
||||
|
@BeforeEach void identity() { |
||||
|
assertThat(publication.getApprovedPolicies()).isEmpty(); |
||||
|
ownerToken = token(OWNER, "STUDENT"); |
||||
|
} |
||||
|
@AfterEach void restore() { |
||||
|
publication.setApprovedPolicies(new LinkedHashMap<>()); |
||||
|
jdbc.update("update salary_city_policy set policy_version=?, enabled=true where city_code=?", VERSION, "110000"); |
||||
|
jdbc.update("delete from salary_calculation_history where student_user_id in (?,?)", OWNER, OTHER); |
||||
|
LxqTestSessionIssuer.cleanup(jdbc); |
||||
|
} |
||||
|
private String token(long id, String role) { |
||||
|
return "Bearer " + LxqTestSessionIssuer.issue(id, role, sessions, users, jdbc); |
||||
|
} |
||||
|
private void approve() { |
||||
|
publication.setApprovedPolicies(new LinkedHashMap<>(Map.of("110000", VERSION))); |
||||
|
} |
||||
|
private String request() { |
||||
|
return "{\"cityCode\":\"110000\",\"grossMonthlySalary\":10000,\"monthIndex\":1," |
||||
|
+ "\"specialAdditionalDeduction\":0,\"expectedPolicyVersion\":\"" + VERSION + "\"," |
||||
|
+ "\"idempotencyKey\":\"independent-replay\"}"; |
||||
|
} |
||||
|
private long calculate() throws Exception { |
||||
|
String response = mvc.perform(post("/salary-calculator/calculations") |
||||
|
.header("Authorization", ownerToken).contentType(MediaType.APPLICATION_JSON).content(request())) |
||||
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); |
||||
|
return mapper.readTree(response).path("data").path("historyId").asLong(); |
||||
|
} |
||||
|
private Map<String, List<String>> snapshot() { |
||||
|
Map<String, List<String>> result = new TreeMap<>(); |
||||
|
for (String table : jdbc.queryForList("show tables", String.class)) { |
||||
|
List<String> rows = jdbc.query("select * from `" + table + "`", (rs, n) -> { |
||||
|
List<String> columns = new ArrayList<>(); |
||||
|
for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { |
||||
|
columns.add(rs.getMetaData().getColumnName(i) + "=" + rs.getString(i)); |
||||
|
} |
||||
|
return columns.toString(); |
||||
|
}); |
||||
|
rows.sort(Comparator.naturalOrder()); |
||||
|
result.put(table, rows); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
private void deniedCalculationIsReadOnly() throws Exception { |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(post("/salary-calculator/calculations").header("Authorization", ownerToken) |
||||
|
.contentType(MediaType.APPLICATION_JSON).content(request())) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10601)) |
||||
|
.andExpect(jsonPath("$.data.policyVersion").doesNotExist()) |
||||
|
.andExpect(jsonPath("$.data.result").doesNotExist()); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
|
||||
|
@Test void allowlistDoesNotPublishOtherCitiesOrVersions() throws Exception { |
||||
|
approve(); |
||||
|
mvc.perform(get("/salary-calculator/cities").header("Authorization", ownerToken)) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.length()").value(1)) |
||||
|
.andExpect(jsonPath("$.data[0].cityCode").value("110000")); |
||||
|
mvc.perform(get("/salary-calculator/cities/310000").header("Authorization", ownerToken)) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10601)); |
||||
|
publication.setApprovedPolicies(new LinkedHashMap<>(Map.of("110000", "wrong-test-version"))); |
||||
|
deniedCalculationIsReadOnly(); |
||||
|
} |
||||
|
@Test void policyVersionDriftClosesEvenExistingIdempotencyReplay() throws Exception { |
||||
|
approve(); |
||||
|
calculate(); |
||||
|
jdbc.update("update salary_city_policy set policy_version=? where city_code=?", "unapproved-next-test-version", "110000"); |
||||
|
deniedCalculationIsReadOnly(); |
||||
|
} |
||||
|
@Test void approvedCurrentPolicyCannotReplayUnapprovedStoredVersion() throws Exception { |
||||
|
approve(); |
||||
|
long id = calculate(); |
||||
|
jdbc.update("update salary_calculation_history set policy_version=? where id=?", "unapproved-old-test-version", id); |
||||
|
deniedCalculationIsReadOnly(); |
||||
|
} |
||||
|
@Test void revokedPublicationPreservesOwnHistoryAndDeleteButNotCrossOwnerAccess() throws Exception { |
||||
|
approve(); |
||||
|
long id = calculate(); |
||||
|
String otherToken = token(OTHER, "STUDENT"); |
||||
|
publication.setApprovedPolicies(new LinkedHashMap<>()); |
||||
|
deniedCalculationIsReadOnly(); |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(get("/salary-calculator/calculations/" + id).header("Authorization", ownerToken)) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.id").value(id)); |
||||
|
mvc.perform(get("/salary-calculator/calculations/" + id).header("Authorization", otherToken)) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10603)); |
||||
|
mvc.perform(delete("/salary-calculator/calculations/" + id).header("Authorization", otherToken)) |
||||
|
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value(10603)); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
mvc.perform(delete("/salary-calculator/calculations/" + id).header("Authorization", ownerToken)) |
||||
|
.andExpect(status().isOk()); |
||||
|
assertThat(jdbc.queryForObject("select count(*) from salary_calculation_history where id=?", Integer.class, id)).isZero(); |
||||
|
} |
||||
|
@Test void approvedButDisabledPolicyCannotReplay() throws Exception { |
||||
|
approve(); |
||||
|
calculate(); |
||||
|
jdbc.update("update salary_city_policy set enabled=false where city_code=?", "110000"); |
||||
|
deniedCalculationIsReadOnly(); |
||||
|
} |
||||
|
@Test void approvalDoesNotOverrideCompanyRoleRestriction() throws Exception { |
||||
|
approve(); |
||||
|
String companyToken = token(OTHER, "COMPANY"); |
||||
|
var before = snapshot(); |
||||
|
mvc.perform(get("/salary-calculator/cities/110000").header("Authorization", companyToken)) |
||||
|
.andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value(2002)); |
||||
|
mvc.perform(post("/salary-calculator/calculations").header("Authorization", companyToken) |
||||
|
.contentType(MediaType.APPLICATION_JSON).content(request())) |
||||
|
.andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value(2002)); |
||||
|
assertThat(snapshot()).isEqualTo(before); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
package com.mydreamjob.salarycalculator; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
|
||||
|
class SalaryPolicyPublicationPropertiesTest { |
||||
|
@Configuration(proxyBeanMethods = false) |
||||
|
@EnableConfigurationProperties(SalaryPolicyPublicationProperties.class) |
||||
|
static class Config {} |
||||
|
private final ApplicationContextRunner context = new ApplicationContextRunner() |
||||
|
.withUserConfiguration(Config.class); |
||||
|
|
||||
|
@Test void emptyConfigurationFailsClosed() { |
||||
|
context.run(ctx -> { |
||||
|
assertThat(ctx).hasNotFailed(); |
||||
|
assertThat(ctx.getBean(SalaryPolicyPublicationProperties.class) |
||||
|
.isPublished("110000", "2026-09-20-v1")).isFalse(); |
||||
|
}); |
||||
|
} |
||||
|
@Test void genericEnabledSwitchCannotPublishSeeds() { |
||||
|
context.withPropertyValues("app.salary-publication.enabled=true").run(ctx -> |
||||
|
assertThat(ctx.getBean(SalaryPolicyPublicationProperties.class) |
||||
|
.isPublished("110000", "2026-09-20-v1")).isFalse()); |
||||
|
} |
||||
|
@Test void bindingPublishesOnlyExactCityAndVersion() { |
||||
|
context.withPropertyValues("app.salary-publication.approved-policies.110000=verified-test-version") |
||||
|
.run(ctx -> { |
||||
|
var properties = ctx.getBean(SalaryPolicyPublicationProperties.class); |
||||
|
assertThat(properties.isPublished("110000", "verified-test-version")).isTrue(); |
||||
|
assertThat(properties.isPublished("310000", "verified-test-version")).isFalse(); |
||||
|
assertThat(properties.isPublished("110000", "old-version")).isFalse(); |
||||
|
assertThat(properties.isPublished("110000", null)).isFalse(); |
||||
|
}); |
||||
|
} |
||||
|
@Test void wildcardCityFailsStartup() { |
||||
|
context.withPropertyValues("app.salary-publication.approved-policies.any=test-version") |
||||
|
.run(ctx -> assertThat(ctx).hasFailed()); |
||||
|
} |
||||
|
@Test void blankVersionFailsStartup() { |
||||
|
context.withPropertyValues("app.salary-publication.approved-policies.110000= ") |
||||
|
.run(ctx -> assertThat(ctx).hasFailed()); |
||||
|
} |
||||
|
@Test void whitespaceVersionCannotSilentlyEnable() { |
||||
|
var properties = new SalaryPolicyPublicationProperties(); |
||||
|
properties.setApprovedPolicies(Map.of("110000", " version ")); |
||||
|
org.assertj.core.api.Assertions.assertThatThrownBy(properties::validate) |
||||
|
.isInstanceOf(IllegalStateException.class) |
||||
|
.hasMessage("invalid salary publication allowlist"); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue