From ef5c5198e706f77b58305a05591036e5f597539f Mon Sep 17 00:00:00 2001 From: Henrique Date: Mon, 31 Aug 2026 18:21:00 -0300 Subject: [PATCH] fix: reject unresolved POST and PUT references Fail fast when a string reference cannot be resolved for an effective POST or PUT object. This prevents write requests from silently invalidating or skipping the object and returning a successful response. Add DB-free regression coverage for table and non-table references, CRUD method directives, preserved GET and DELETE behavior, and resolved references. closes #275 --- .../apijson/orm/AbstractObjectParser.java | 5 + .../AbstractObjectParserReferenceTest.java | 309 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java index 5fd00c17..c3fa337b 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java @@ -486,6 +486,11 @@ else if (value instanceof String) { // //key{}@ getRealKey, 引用赋值路径 if (target == null) { // String#equals(null)会出错 Log.d(TAG, "onParse target == null >> return true;"); + if (method == POST || method == PUT) { + throw new UnsupportedOperationException("引用赋值 " + path + "/" + key + ": " + targetPath + + " 解析失败,POST 和 PUT 接口必须保证每个对象执行都成功!"); + } + if (Log.DEBUG) { parser.putWarnIfNeed(AbstractParser.KEY_REF, path + "/" + key + ": " + targetPath + " 引用赋值获取路径对应的值为 null!请检查路径是否错误!"); } diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java new file mode 100644 index 00000000..78b3b161 --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractObjectParserReferenceTest.java @@ -0,0 +1,309 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import apijson.orm.exception.CommonException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractObjectParserReferenceTest { + private static final String REFERENCE_KEY = "id@"; + private static final String TARGET_PATH = "Privacy/id"; + private static final String TRAILING_KEY = "afterReference"; + private static final long RESOLVED_VALUE = 42L; + + private static JSONParser, ? extends List> previousJSONParser; + + @BeforeClass + public static void installJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + @Test + public void rejectsUnresolvedPostTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, true, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void rejectsUnresolvedPutTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.PUT, true, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void rejectsUnresolvedPostNonTableReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, false, null); + + assertUnsupportedReference(fixture); + } + + @Test + public void keepsGetTableInvalidationForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.GET, true, null); + + assertNull(fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + } + + @Test + public void keepsDeleteTableInvalidationForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.DELETE, true, null); + + assertNull(fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + } + + @Test + public void keepsGetNonTableSkipForUnresolvedReference() throws Exception { + Fixture fixture = newFixture(RequestMethod.GET, false, null); + + assertSame(fixture.objectParser, fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertFalse(fixture.objectParser.getSQLRequest().containsKey(REFERENCE_KEY)); + assertEquals(Boolean.TRUE, fixture.objectParser.getSQLRequest().get(TRAILING_KEY)); + } + + @Test + public void resolvesPostReferenceAndContinuesParsing() throws Exception { + Fixture fixture = newFixture(RequestMethod.POST, true, RESOLVED_VALUE); + + assertSame(fixture.objectParser, fixture.objectParser.parse(fixture.name, false)); + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertFalse(fixture.objectParser.getSQLRequest().containsKey(REFERENCE_KEY)); + assertEquals(Long.valueOf(RESOLVED_VALUE), fixture.objectParser.getSQLRequest().get("id")); + assertEquals(Boolean.TRUE, fixture.objectParser.getSQLRequest().get(TRAILING_KEY)); + } + + @Test + public void rejectsUnresolvedCrudReferenceWithPostDirective() throws Exception { + Map user = new LinkedHashMap<>(); + user.put(REFERENCE_KEY, TARGET_PATH); + user.put(TRAILING_KEY, Boolean.TRUE); + + Map request = new LinkedHashMap<>(); + request.put("User", user); + request.put("@post", "User"); + + ReferenceParser parser = new ReferenceParser(null); + Map verifiedRequest = parser.verifyCrud(request); + Map verifiedUser = JSON.get(verifiedRequest, "User"); + assertSame(RequestMethod.POST, verifiedUser.get("@method")); + + try { + parser.onObjectParse(verifiedUser, null, "User", null, false, null); + fail("The effective POST method must reject an unresolved reference"); + } + catch (Exception e) { + assertUnsupportedOperation(e); + } + + assertNotNull(parser.lastObjectParser); + assertSame(RequestMethod.POST, parser.lastObjectParser.getMethod()); + assertEquals(TARGET_PATH, parser.lastPath); + assertNotNull(parser.lastObjectParser.getSQLRequest()); + assertFalse(parser.lastObjectParser.getSQLRequest().containsKey(TRAILING_KEY)); + } + + private static void assertUnsupportedReference(Fixture fixture) throws Exception { + try { + fixture.objectParser.parse(fixture.name, false); + fail("Unresolved POST/PUT references must be rejected"); + } + catch (Exception e) { + assertUnsupportedOperation(e); + } + + assertEquals(TARGET_PATH, fixture.parser.lastPath); + assertNotNull(fixture.objectParser.getSQLRequest()); + assertFalse("Entries after the unresolved reference must not be parsed", + fixture.objectParser.getSQLRequest().containsKey(TRAILING_KEY)); + } + + private static void assertUnsupportedOperation(Exception e) { + Throwable cause = e instanceof CommonException && e.getCause() != null ? e.getCause() : e; + assertTrue("Expected UnsupportedOperationException but got " + cause.getClass().getName(), + cause instanceof UnsupportedOperationException); + } + + private static Fixture newFixture(RequestMethod method, boolean isTable, Object target) throws Exception { + Map request = new LinkedHashMap<>(); + request.put(REFERENCE_KEY, TARGET_PATH); + request.put(TRAILING_KEY, Boolean.TRUE); + + ReferenceParser parser = new ReferenceParser(target); + TestObjectParser objectParser = new TestObjectParser(request, isTable); + objectParser.setParser(parser); + objectParser.setMethod(method); + + return new Fixture(parser, objectParser, isTable ? "User" : "metadata"); + } + + private static final class Fixture { + private final ReferenceParser parser; + private final TestObjectParser objectParser; + private final String name; + + private Fixture(ReferenceParser parser, TestObjectParser objectParser, String name) { + this.parser = parser; + this.objectParser = objectParser; + this.name = name; + } + } + + private static final class TestObjectParser + extends AbstractObjectParser, List> { + private TestObjectParser(Map request, boolean isTable) throws Exception { + super(request, null, null, false, isTable, false); + } + + @Override + public SQLConfig, List> newSQLConfig(RequestMethod method, + String table, String alias, Map request, + List, List>> joinList, boolean isProcedure) { + return null; + } + } + + private static final class ReferenceParser + extends AbstractParser, List> { + private final Object target; + private String lastPath; + private TestObjectParser lastObjectParser; + + private ReferenceParser(Object target) { + super(RequestMethod.CRUD, false); + this.target = target; + } + + private Map verifyCrud(Map request) throws Exception { + return batchVerify(RequestMethod.CRUD, null, 0, null, request, 10, this); + } + + @Override + public Object getValueByPath(String valuePath) { + lastPath = valuePath; + return target; + } + + @Override + protected Map getRequestStructure(RequestMethod method, String tag, int version) { + return new LinkedHashMap<>(); + } + + @Override + protected Map objectVerify(RequestMethod method, String tag, int version, String name, + Map request, int maxUpdateCount, + SQLCreator, List> creator, Map object) { + return request; + } + + @Override + public Object onFunctionParse(String key, String function, String parentPath, String currentName, + Map currentObject, boolean containRaw) { + return null; + } + + @Override + public ObjectParser, List> createObjectParser( + Map request, String parentPath, + SQLConfig, List> arrayConfig, + boolean isSubquery, boolean isTable, boolean isArrayMainTable) throws Exception { + TestObjectParser objectParser = new TestObjectParser(request, isTable); + objectParser.setParser(this); + lastObjectParser = objectParser; + return objectParser; + } + + @Override + public Parser, List> createParser() { + return new ReferenceParser(target); + } + + @Override + public FunctionParser, List> createFunctionParser() { + return null; + } + + @Override + public Verifier, List> createVerifier() { + return null; + } + + @Override + public SQLConfig, List> createSQLConfig() { + return null; + } + + @Override + public SQLExecutor, List> createSQLExecutor() { + return null; + } + } +}