Skip to content

Test Workshop: Add validation for attachment item counts and update messages#751

Draft
samyuktaprabhu wants to merge 2 commits intomainfrom
sam-1-opencode-withoutmcp-one-agent
Draft

Test Workshop: Add validation for attachment item counts and update messages#751
samyuktaprabhu wants to merge 2 commits intomainfrom
sam-1-opencode-withoutmcp-one-agent

Conversation

@samyuktaprabhu
Copy link
Contributor

/review

@hyperspace-insights
Copy link
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Add Validation for Attachment Item Counts (@Validation.MaxItems / @Validation.MinItems)

New Feature

✨ Introduced a new ItemsCountValidationHandler that validates the number of items in composition associations annotated with @Validation.MaxItems and @Validation.MinItems before CREATE and UPDATE operations on attachment entities.

  • Active entity operations (direct CREATE/UPDATE): violations throw a ServiceException with CONFLICT status, rejecting the request.
  • Draft mode (draft activate): violations are reported as warnings via the Messages API, allowing drafts to remain in an invalid state.
  • Annotation values can be a static integer (@Validation.MaxItems: 20) or a property reference from the parent entity (@Validation.MaxItems: 'maxCount').
  • Applications can override error messages using the Fiori elements i18n convention with keys like AttachmentMaxItemsExceeded_<EntityName>_<CompositionName>.

Changes

  • Registration.java: Registered the new ItemsCountValidationHandler as an application service event handler. Updated handler count in tests accordingly.
  • ItemsCountValidationHandler.java (new): Event handler implementing @Before hooks for CREATE and UPDATE events. Delegates validation to ItemsCountValidator and reports violations as errors or warnings based on draft state.
  • ItemsCountValidator.java (new): Core validation logic that resolves @Validation.MaxItems / @Validation.MinItems annotations (supporting integer literals and property references) and returns a list of ItemsCountViolation records.
  • ItemsCountViolation.java (new): Record representing a single validation violation, including violation type, composition name, entity name, actual count, limit, and i18n message key resolution.
  • messages.properties: Added i18n messages AttachmentMaxItemsExceeded and AttachmentMinItemsNotReached with parameters for the limit and actual count.
  • db-model.cds (test): Added @Validation.MaxItems: 20 / @Validation.MinItems: 2 to Roots.attachments and @Validation.MinItems: 1 to Items.itemAttachments for test coverage.
  • RegistrationTest.java: Updated expected handler count from 8 to 9 and added assertion for ItemsCountValidationHandler registration.
  • ItemsCountValidationHandlerTest.java (new): Unit tests covering CREATE/UPDATE validation, draft vs. active mode behavior, annotation detection, and violation reporting.
  • ItemsCountValidatorTest.java (new): Unit tests covering annotation resolution (integer, string, property reference, bare annotation), boundary conditions, and merge behavior with existing data.

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.18.5 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • Correlation ID: 92ced100-1ed9-11f1-816d-eb32b61e4271
  • Summary Prompt: Default Prompt
  • Event Trigger: pull_request.opened
  • Output Template: Default Template
  • LLM: anthropic--claude-4.6-sonnet

💌 Have ideas or want to contribute? Create an issue and share your thoughts with us!
📑 Check out the documentation for more information.
📬 Subscribe to the Hyperspace PR Bot DL to get the latest announcements and pilot features!

Made with ❤️ by Hyperspace.

Copy link
Contributor

@hyperspace-insights hyperspace-insights bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces a new ItemsCountValidationHandler with solid test coverage, but there are two substantive correctness issues: a NullPointerException risk when annotation values are null, and an early-exit bug in reportViolations that silently drops all violations after the first one in non-draft mode. Additionally, the UPDATE path has a design flaw where existing data (flat attachment records) is index-matched against parent entity records, which will silently corrupt the merge used for property-reference annotation resolution.

PR Bot Information

Version: 1.18.5 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • LLM: anthropic--claude-4.6-sonnet
  • Event Trigger: pull_request.opened
  • Correlation ID: 92ced100-1ed9-11f1-816d-eb32b61e4271


private static Optional<Integer> resolveValue(
Object value, Map<String, Object> parentData, String annotationName) {
if ("true".equals(value.toString())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: resolveValue throws NullPointerException when annotation value is null

CdsAnnotation::getValue can return null, and the chain .flatMap(value -> resolveValue(value, parentData, annotationName)) will pass null into resolveValue. The very first line value.toString() will throw an NPE in that case.

The test resolveAnnotationValueReturnsEmptyForNullAnnotationValue explicitly covers this scenario and would fail at runtime.

Consider guarding against null before calling resolveValue:

Suggested change
if ("true".equals(value.toString())) {
if (value == null || "true".equals(value.toString())) {
// null or bare annotation without value, ignore
return Optional.empty();
}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +151 to +161
for (ItemsCountViolation violation : violations) {
String messageKey = violation.getBaseMessageKey();
String target = violation.compositionName();

if (isDraft) {
messages.warn(messageKey, violation.limit(), violation.actualCount()).target(target);
} else {
throw new ServiceException(
ErrorStatuses.CONFLICT, messageKey, violation.limit(), violation.actualCount());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: reportViolations stops reporting after the first non-draft violation; remaining violations in the list are silently dropped

When isDraft is false, the method throws a ServiceException on the first violation and exits the loop. Any subsequent violations in the list are never reported, so the user only ever sees one error at a time even when multiple constraints are breached.

Consider collecting all violations into the exception (e.g. as multiple messages or a combined message) rather than throwing inside the loop, or at a minimum report the first violation clearly in the exception details. For the draft path this is already handled correctly via the loop.

Suggested change
for (ItemsCountViolation violation : violations) {
String messageKey = violation.getBaseMessageKey();
String target = violation.compositionName();
if (isDraft) {
messages.warn(messageKey, violation.limit(), violation.actualCount()).target(target);
} else {
throw new ServiceException(
ErrorStatuses.CONFLICT, messageKey, violation.limit(), violation.actualCount());
}
}
static void reportViolations(
List<ItemsCountViolation> violations, Messages messages, boolean isDraft) {
if (isDraft) {
for (ItemsCountViolation violation : violations) {
String messageKey = violation.getBaseMessageKey();
String target = violation.compositionName();
messages.warn(messageKey, violation.limit(), violation.actualCount()).target(target);
}
} else {
ItemsCountViolation first = violations.get(0);
throw new ServiceException(
ErrorStatuses.CONFLICT,
first.getBaseMessageKey(),
first.limit(),
first.actualCount());
}
}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +103 to +107
CqnSelect select = CqnUtils.toSelect(context.getCqn(), target);
List<Attachments> existingData =
attachmentsReader.readAttachments(context.getModel(), target, select);

List<ItemsCountViolation> violations = ItemsCountValidator.validate(target, data, existingData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: validateOnUpdate reads existing data using attachmentsReader, which traverses the entity path to the Attachments media entity. However, the retrieved List<Attachments> is a flat list of attachment records, not a list of parent-entity records. When this is passed to ItemsCountValidator.validate, the validator iterates over data (parent records) and tries to match them with existingData at the same index — but existingData contains attachment rows, not parent rows. The size comparison and field lookup are therefore mismatched, meaning the merge of existing parent data with request data does not work as intended.

Consider reading existing parent-entity records (not attachment records) for the merge, or rethinking how existing data is supplied to the validator for the UPDATE path.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +89 to +94
for (int i = 0; i < dataList.size(); i++) {
CdsData data = dataList.get(i);
CdsData existingData =
(existingDataList != null && i < existingDataList.size())
? existingDataList.get(i)
: null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: Existing data is matched to request data by positional index, which is incorrect for UPDATE operations

The loop assumes existingDataList.get(i) corresponds to dataList.get(i), but an UPDATE payload can contain records in any order and the existing data from the database may be in a completely different order. A mis-match silently causes mergeData to mix values from unrelated records (e.g. the wrong parent's maxCount property reference is used) or fall back to null.

Records should be matched by their key rather than by position.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant