|
@@ -0,0 +1,348 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.AdminFoodController;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.AdminRecipeController;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.ReportParseAdminController;
|
|
|
|
|
+import com.etotem.cfc.entity.Food;
|
|
|
|
|
+import com.etotem.cfc.entity.Recipe;
|
|
|
|
|
+import com.etotem.cfc.service.FoodService;
|
|
|
|
|
+import com.etotem.cfc.service.RecipeService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.lang.reflect.Method;
|
|
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.util.Scanner;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测:商品服务商「营养/食谱数据供给」(能力键 {@code supplier.nutrition.supply},SC-S-10)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>需求/设计锚点</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-S-10(营养/食谱数据供给)——
|
|
|
|
|
+ * 目标:供食谱数据;流程:上架食谱 → 同步;边界:<b>数据合规;格式校验</b>;模块:食谱库。
|
|
|
|
|
+ * 关联 SC-S-09(报告解析服务接入/对接解析中台)为本能力的上游供给链路。
|
|
|
|
|
+ * 用例:{@code tests/test-cases/vendor.md} TC-S-10 / TC-S-10-E(格式非法 400 / XSS 转义 /
|
|
|
|
|
+ * 非本商户覆盖 403)/ TC-S-10-P(跨商户访问 403)/ TC-S-N06(XSS 不执行)。</li>
|
|
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.11(饮食推荐)/ §4.13(管理员接口);
|
|
|
|
|
+ * 契约载体 {@code AdminRecipeController}(/api/admin/recipes)+ {@code AdminFoodController}
|
|
|
|
|
+ * (/api/admin/foods)+ {@code ReportParseAdminController}(/api/admin/report-parse,
|
|
|
|
|
+ * SC-S-09 中台链路 backfill-food 回填食材)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>被校验的契约行为</b>(SC-S-10 正常流 + 边界)</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li><b>正常流·供给入口</b>:服务商「上架食谱 → 同步 → 供食谱数据」——需存在供给宿主服务
|
|
|
|
|
+ * {@code RecipeSupplyService}(含 {@code syncFromSupplier} 同步供给入口)与食材/食谱的
|
|
|
|
|
+ * 商户归属载体 {@code Recipe.supplierId} / schema 的 {@code supplier_id};</li>
|
|
|
|
|
+ * <li><b>边界流·格式校验</b>:上架格式非法食谱(name 缺失 / ingredients JSON 残缺)与非法食材
|
|
|
|
|
+ * (负营养值)应被拒绝(400),不落库;</li>
|
|
|
|
|
+ * <li><b>边界流·数据合规(XSS)</b>:上架食谱/食材含 {@code <script>} 描述/名称应转义存储,
|
|
|
|
|
+ * 不得以原始脚本透传落库(TC-S-N06:内容转义存储/渲染,不执行);</li>
|
|
|
|
|
+ * <li><b>权限流·商户隔离</b>:更新/删除食谱须校验商户归属,无合法商户上下文的写操作应 403
|
|
|
|
|
+ * (TC-S-10-E-3 / TC-S-10-P:非本商户 403);报告解析中台(SC-S-09)回填的食材
|
|
|
|
|
+ * (backfill-food)应经 {@code FoodService} 合规供给通道写入,而非绕过供给直写。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>实现现状(src/main 核实)</b>:本分支 src/main 中 {@code RecipeSupplyService} 类、
|
|
|
|
|
+ * {@code Recipe.supplierId} 字段、schema.sql 的 {@code recipes} 建表(仅 DatabaseInitializer 有
|
|
|
|
|
+ * 运行时建表)均缺失;{@code AdminRecipeController.create/update/delete} 与
|
|
|
|
|
+ * {@code AdminFoodController.create} 为直通 CRUD,无格式校验、无 XSS 转义、无商户归属校验;
|
|
|
|
|
+ * {@code ReportParseAdminController} 无 {@code foodService} 供给写入点。因此本测试以
|
|
|
|
|
+ * 「反射结构契约 + Mockito 行为契约」探测实现缺失:能编译通过、能运行、断言失败(红),
|
|
|
|
|
+ * 且失败断言原文清晰可贴。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>GREEN 升级指引</b>:实现者创建 {@code RecipeSupplyService}(含 syncFromSupplier 入口)、
|
|
|
|
|
+ * 给 {@code Recipe} 增加 {@code supplierId} 并在 schema.sql 落 {@code recipes} 建表 +
|
|
|
|
|
+ * {@code supplier_id} 列,在 {@code AdminRecipeController}(create/update/delete)与
|
|
|
|
|
+ * {@code AdminFoodController.create} 补格式校验(Result.badRequest→400)与 XSS 转义(存储前
|
|
|
|
|
+ * 清洗)与商户归属守卫(Result.forbidden→403),并让 {@code ReportParseAdminController}
|
|
|
|
|
+ * backfill-food 经 {@code FoodService} 合规写入后,本测试自动转绿。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>测试范围</b>:纯 JUnit5 + Mockito / 反射,无 Spring / 无 DB / 无外部服务,
|
|
|
|
|
+ * 与同批 RED 契约测试(TeamProfitHookContractTest / TeamStarMultiplierContractTest /
|
|
|
|
|
+ * ReportParserProviderConnectContractTest)同风格。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class SupplierNutritionSupplyContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ /** 契约供给宿主(未实现)。 */
|
|
|
|
|
+ private static final String SUPPLY_HOST = "com.etotem.cfc.service.RecipeSupplyService";
|
|
|
|
|
+
|
|
|
|
|
+ private AdminRecipeController recipeController;
|
|
|
|
|
+ private AdminFoodController foodController;
|
|
|
|
|
+ private RecipeService recipeService;
|
|
|
|
|
+ private FoodService foodService;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ recipeController = new AdminRecipeController();
|
|
|
|
|
+ injectMocks(recipeController);
|
|
|
|
|
+ recipeService = readField(recipeController, "recipeService");
|
|
|
|
|
+
|
|
|
|
|
+ foodController = new AdminFoodController();
|
|
|
|
|
+ injectMocks(foodController);
|
|
|
|
|
+ foodService = readField(foodController, "foodService");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常流:供给宿主与归属载体(结构契约) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-1:RecipeSupplyService 供给宿主类应存在(服务商上架食谱→同步供给)")
|
|
|
|
|
+ void supplyHost_classExists() {
|
|
|
|
|
+ assertNotNull(loadClass(SUPPLY_HOST),
|
|
|
|
|
+ "[supplier.nutrition.supply] RecipeSupplyService 类不存在:SC-S-10 要求服务商"
|
|
|
|
|
+ + "「上架食谱 → 同步 → 供食谱数据」的供给宿主服务。实现未落地。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-2:RecipeSupplyService 应提供 syncFromSupplier 同步供给入口")
|
|
|
|
|
+ void supplyHost_hasSyncFromSupplierEntry() {
|
|
|
|
|
+ Class<?> clazz = loadClass(SUPPLY_HOST);
|
|
|
|
|
+ if (clazz == null) {
|
|
|
|
|
+ fail("[supplier.nutrition.supply] RecipeSupplyService 类不存在,无法验证同步供给入口"
|
|
|
|
|
+ + "(SC-S-10 正常流:上架食谱 → 同步)。");
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean hasSync = false;
|
|
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
|
|
+ if (m.getName().equals("syncFromSupplier")) {
|
|
|
|
|
+ hasSync = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertTrue(hasSync,
|
|
|
|
|
+ "[supplier.nutrition.supply] RecipeSupplyService 应提供 syncFromSupplier(...) "
|
|
|
|
|
+ + "方法作为服务商上架食谱→同步供给的入口(SC-S-10)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-3:Recipe 实体应含 supplierId 商户归属字段(商户隔离/供给归属载体)")
|
|
|
|
|
+ void recipe_hasSupplierIdOwnershipField() {
|
|
|
|
|
+ boolean hasSupplierId = false;
|
|
|
|
|
+ for (Field f : Recipe.class.getDeclaredFields()) {
|
|
|
|
|
+ if (f.getName().equals("supplierId")) {
|
|
|
|
|
+ hasSupplierId = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertTrue(hasSupplierId,
|
|
|
|
|
+ "[supplier.nutrition.supply] Recipe 实体应含 supplierId 字段:SC-S-10 商户供给链路的"
|
|
|
|
|
+ + "商户归属/跨商户隔离(TC-S-10-P 非本商户 403)需要归属载体。当前缺失。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-4:schema.sql 应落地 recipes 建表(当前仅 DatabaseInitializer 运行时建表)")
|
|
|
|
|
+ void schemaSql_hasRecipesTable() {
|
|
|
|
|
+ String schema = readSchemaSql();
|
|
|
|
|
+ assertTrue(schema.contains("recipes"),
|
|
|
|
|
+ "[supplier.nutrition.supply] schema.sql 缺少 recipes 建表:SC-S-10 食谱库供给"
|
|
|
|
|
+ + "应含完整 DDL 快照(当前仅 DatabaseInitializer 运行时建表,schema.sql 无镜像)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-5:schema.sql recipes 建表块应含 supplier_id 列(商户隔离存储载体)")
|
|
|
|
|
+ void schemaSql_recipesHasSupplierIdColumn() {
|
|
|
|
|
+ String schema = readSchemaSql();
|
|
|
|
|
+ int start = schema.indexOf("CREATE TABLE IF NOT EXISTS recipes");
|
|
|
|
|
+ if (start < 0) {
|
|
|
|
|
+ fail("[supplier.nutrition.supply] schema.sql 缺少 recipes 建表,无法校验 supplier_id 列"
|
|
|
|
|
+ + "(SC-S-10 食谱库供给 DDL 快照缺失)。");
|
|
|
|
|
+ }
|
|
|
|
|
+ int end = schema.indexOf(");", start);
|
|
|
|
|
+ String block = end > start && end < schema.length()
|
|
|
|
|
+ ? schema.substring(start, end)
|
|
|
|
|
+ : schema.substring(start);
|
|
|
|
|
+ assertTrue(block.contains("supplier_id"),
|
|
|
|
|
+ "[supplier.nutrition.supply] schema.sql recipes 建表块缺少 supplier_id 列:食谱库"
|
|
|
|
|
+ + "供给需按商户归属存储,支撑跨商户隔离(TC-S-10-P 403)。当前缺失。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 边界流:格式校验(行为契约,400) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-1:上架残缺食谱(name 缺失)应格式校验拒绝 400")
|
|
|
|
|
+ void recipeController_create_rejectsEmptyName() {
|
|
|
|
|
+ Recipe recipe = new Recipe();
|
|
|
|
|
+ recipe.setName("");
|
|
|
|
|
+ recipe.setMealType("lunch");
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> result = recipeController.create(recipe);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(400, result.getCode(),
|
|
|
|
|
+ "[supplier.nutrition.supply] 上架 name 为空的食谱应被格式校验拒绝(TC-S-10-E-1 格式校验 400)"
|
|
|
|
|
+ + ",当前直通落库返回 200,契约缺失。");
|
|
|
|
|
+ verify(recipeService, never()).create(any(Recipe.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-2:上架 ingredients 为残缺 JSON 的食谱应格式校验拒绝 400")
|
|
|
|
|
+ void recipeController_create_rejectsBrokenIngredientsJson() {
|
|
|
|
|
+ Recipe recipe = new Recipe();
|
|
|
|
|
+ recipe.setName("乱炖营养餐");
|
|
|
|
|
+ recipe.setIngredients("["); // 残缺 JSON
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> result = recipeController.create(recipe);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(400, result.getCode(),
|
|
|
|
|
+ "[supplier.nutrition.supply] 上架 ingredients 为残缺 JSON 的食谱应格式校验拒绝"
|
|
|
|
|
+ + "(TC-S-10-E-1 格式校验 400),当前直通落库返回 200,契约缺失。");
|
|
|
|
|
+ verify(recipeService, never()).create(any(Recipe.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-3:上架含负营养值的食材应数据合规拒绝 400")
|
|
|
|
|
+ void foodController_create_rejectsNegativeNutrients() {
|
|
|
|
|
+ Food food = new Food();
|
|
|
|
|
+ food.setName("问题食材");
|
|
|
|
|
+ food.setCalories(new BigDecimal("-100"));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Food> result = foodController.create(food);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(400, result.getCode(),
|
|
|
|
|
+ "[supplier.nutrition.supply] 上架负热量食材应被数据合规校验拒绝(SC-S-10 边界:数据合规),"
|
|
|
|
|
+ + "当前直通落库返回 200,契约缺失。");
|
|
|
|
|
+ verify(foodService, never()).create(any(Food.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 边界流:数据合规(XSS 转义存储,不执行) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-4:上架含 <script> 描述的食谱应转义存储(不执行)")
|
|
|
|
|
+ void recipeController_create_sanitizesXssDescription() {
|
|
|
|
|
+ Recipe recipe = new Recipe();
|
|
|
|
|
+ recipe.setName("合规早餐");
|
|
|
|
|
+ recipe.setDescription("<script>alert('xss')</script>营养建议");
|
|
|
|
|
+
|
|
|
|
|
+ recipeController.create(recipe);
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<Recipe> captor = ArgumentCaptor.forClass(Recipe.class);
|
|
|
|
|
+ verify(recipeService).create(captor.capture());
|
|
|
|
|
+ String storedDesc = captor.getValue().getDescription();
|
|
|
|
|
+ assertFalse(storedDesc != null && storedDesc.contains("<script>"),
|
|
|
|
|
+ "[supplier.nutrition.supply] 含 <script> 描述的食谱应转义存储(TC-S-10-E-2 / TC-S-N06:"
|
|
|
|
|
+ + "XSS 转义存储不执行),实际落库描述仍含原始脚本:" + storedDesc);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-5:上架含 <script> 名称的食材应转义存储(不执行)")
|
|
|
|
|
+ void foodController_create_sanitizesXssName() {
|
|
|
|
|
+ Food food = new Food();
|
|
|
|
|
+ food.setName("<script>alert('xss')</script>");
|
|
|
|
|
+
|
|
|
|
|
+ foodController.create(food);
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<Food> captor = ArgumentCaptor.forClass(Food.class);
|
|
|
|
|
+ verify(foodService).create(captor.capture());
|
|
|
|
|
+ String storedName = captor.getValue().getName();
|
|
|
|
|
+ assertFalse(storedName != null && storedName.contains("<script>"),
|
|
|
|
|
+ "[supplier.nutrition.supply] 含 <script> 名称的食材应转义存储(TC-S-N06:内容转义存储"
|
|
|
|
|
+ + "不执行),实际落库名称仍含原始脚本:" + storedName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 权限流:商户隔离(非本商户 403) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流-1:更新食谱应在无合法商户上下文时拒绝 403(跨商户覆盖防护)")
|
|
|
|
|
+ void recipeController_update_requiresOwnershipGuard() {
|
|
|
|
|
+ Recipe recipe = new Recipe();
|
|
|
|
|
+ recipe.setId(1L);
|
|
|
|
|
+ recipe.setName("被篡改食谱");
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> result = recipeController.update(recipe);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(403, result.getCode(),
|
|
|
|
|
+ "[supplier.nutrition.supply] 更新食谱应校验商户归属,无合法商户上下文应 403"
|
|
|
|
|
+ + "(TC-S-10-E-3 非本商户覆盖 403 / TC-S-10-P 跨商户访问 403),"
|
|
|
|
|
+ + "当前直通更新返回 200,契约缺失。");
|
|
|
|
|
+ verify(recipeService, never()).update(any(Recipe.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流-2:删除食谱应在无合法商户上下文时拒绝 403(跨商户删除防护)")
|
|
|
|
|
+ void recipeController_delete_requiresOwnershipGuard() {
|
|
|
|
|
+ java.util.Map<String, Object> params = new java.util.HashMap<>();
|
|
|
|
|
+ params.put("id", 1L);
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> result = recipeController.delete(params);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(403, result.getCode(),
|
|
|
|
|
+ "[supplier.nutrition.supply] 删除食谱应校验商户归属,无合法商户上下文应 403"
|
|
|
|
|
+ + "(TC-S-10-P 跨商户资源隔离),当前直通删除返回 200,契约缺失。");
|
|
|
|
|
+ verify(recipeService, never()).delete(any(Long.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流-3:报告解析中台(SC-S-09)回填食材应经 FoodService 合规供给写入")
|
|
|
|
|
+ void reportParseAdminController_backfillFood_delegatesCompliantSupplyWrite() {
|
|
|
|
|
+ boolean hasFoodService = false;
|
|
|
|
|
+ for (Field f : ReportParseAdminController.class.getDeclaredFields()) {
|
|
|
|
|
+ if (FoodService.class.equals(f.getType())) {
|
|
|
|
|
+ hasFoodService = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertTrue(hasFoodService,
|
|
|
|
|
+ "[supplier.nutrition.supply] ReportParseAdminController.backfill-food(SC-S-09 中台回填"
|
|
|
|
|
+ + "食材 → 营养供给)应经 FoodService 合规供给通道写入(数据合规边界),当前仅经"
|
|
|
|
|
+ + "reportCollectService 直写,无合规供给写入点。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 反射 / Mockito 辅助 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static <T> T readField(Object target, String name) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ T value = (T) f.get(target);
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 将所有非静态非常量字段注入 Mock(控制器无参构造 + 反射注入,无 Spring)。 */
|
|
|
|
|
+ private static void injectMocks(Object target) throws Exception {
|
|
|
|
|
+ for (Field f : target.getClass().getDeclaredFields()) {
|
|
|
|
|
+ if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, mock(f.getType()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String readSchemaSql() {
|
|
|
|
|
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
|
|
|
|
|
+ if (is == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
|
|
|
+ scanner.useDelimiter("\\A");
|
|
|
|
|
+ return scanner.hasNext() ? scanner.next() : "";
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|