From bce9f25ba19c49e2d7dadf08f2dce5d16a7dead5 Mon Sep 17 00:00:00 2001 From: Josef Hardi Date: Tue, 30 Jun 2026 10:04:21 -0700 Subject: [PATCH] test(issues): pin plural @JsonTypeName for discussion events Add contract-guard JSON tests that assert the on-the-wire @type discriminator for CommentPosted, CommentUpdated, DiscussionThreadStatusChanged, and DiscussionThreadCreated stays the canonical plural `webprotege.events.discussions.*` form. The expected values are hard-coded (not read from each class's CHANNEL constant) so the build fails if a discriminator ever drifts back to the singular `webprotege.events.discussion.*` form that was silently dropping these events on the wire. Refs #54 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../DiscussionEventChannelJsonTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/test/java/edu/stanford/protege/webprotege/issues/DiscussionEventChannelJsonTest.java diff --git a/src/test/java/edu/stanford/protege/webprotege/issues/DiscussionEventChannelJsonTest.java b/src/test/java/edu/stanford/protege/webprotege/issues/DiscussionEventChannelJsonTest.java new file mode 100644 index 0000000..9c17ec1 --- /dev/null +++ b/src/test/java/edu/stanford/protege/webprotege/issues/DiscussionEventChannelJsonTest.java @@ -0,0 +1,116 @@ +package edu.stanford.protege.webprotege.issues; + +import com.google.common.collect.ImmutableList; +import edu.stanford.protege.webprotege.common.EventId; +import edu.stanford.protege.webprotege.common.ProjectId; +import edu.stanford.protege.webprotege.common.UserId; +import org.junit.jupiter.api.Test; +import org.semanticweb.owlapi.model.OWLClass; +import org.semanticweb.owlapi.vocab.OWLRDFVocabulary; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import uk.ac.manchester.cs.owl.owlapi.OWLClassImpl; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Contract-guard tests that pin the on-the-wire {@code @JsonTypeName} discriminator for + * every event in the discussion namespace. + * + *

These events are routed across the JSON-RPC envelope by their {@code @JsonTypeName} + * value. If a discriminator drifts (for example the canonical plural {@code discussions} + * silently reverts to the singular {@code discussion}), the consumer treats the message as + * an unknown type and drops it — no exception is thrown on either side. See + * protegeproject/webprotege-backend-api#54. + * + *

The expected values below are hard-coded on purpose: asserting against each class's + * own {@code CHANNEL} constant would let a constant rename slip through undetected. The + * whole point of this test is to fail the build if any of these strings ever change. + */ +@JsonTest +class DiscussionEventChannelJsonTest { + + private static final String PROJECT_ID = "00000000-1111-2222-3333-444444444444"; + + private static final String COMMENT_POSTED = "webprotege.events.discussions.CommentPosted"; + + private static final String COMMENT_UPDATED = "webprotege.events.discussions.CommentUpdated"; + + private static final String STATUS_CHANGED = "webprotege.events.discussions.DiscussionThreadStatusChanged"; + + private static final String THREAD_CREATED = "webprotege.events.discussions.DiscussionThreadCreated"; + + @Autowired + private JacksonTester commentPostedTester; + + @Autowired + private JacksonTester commentUpdatedTester; + + @Autowired + private JacksonTester statusChangedTester; + + @Autowired + private JacksonTester threadCreatedTester; + + private static EventId eventId() { + return new EventId("event-id"); + } + + private static ProjectId projectId() { + return new ProjectId(PROJECT_ID); + } + + private static ThreadId threadId() { + return ThreadId.valueOf("thread-id"); + } + + private static Comment comment() { + return new Comment(CommentId.valueOf("comment-id"), + UserId.valueOf("the-user"), + 0L, + Optional.empty(), + "body", + "renderedBody"); + } + + @Test + void commentPostedEventSerializesWithPluralDiscriminator() throws Exception { + var event = new CommentPostedEvent(eventId(), projectId(), threadId(), comment(), + Optional.empty(), 0, 0); + JsonContent json = commentPostedTester.write(event); + assertThat(json).extractingJsonPathStringValue("$.['@type']").isEqualTo(COMMENT_POSTED); + assertThat(CommentPostedEvent.CHANNEL).isEqualTo(COMMENT_POSTED); + } + + @Test + void commentUpdatedEventSerializesWithPluralDiscriminator() throws Exception { + var event = new CommentUpdatedEvent(eventId(), projectId(), threadId(), comment()); + JsonContent json = commentUpdatedTester.write(event); + assertThat(json).extractingJsonPathStringValue("$.['@type']").isEqualTo(COMMENT_UPDATED); + assertThat(CommentUpdatedEvent.CHANNEL).isEqualTo(COMMENT_UPDATED); + } + + @Test + void discussionThreadStatusChangedEventSerializesWithPluralDiscriminator() throws Exception { + var event = new DiscussionThreadStatusChangedEvent(eventId(), projectId(), threadId(), + Optional.empty(), 0, Status.OPEN); + JsonContent json = statusChangedTester.write(event); + assertThat(json).extractingJsonPathStringValue("$.['@type']").isEqualTo(STATUS_CHANGED); + assertThat(DiscussionThreadStatusChangedEvent.CHANNEL).isEqualTo(STATUS_CHANGED); + } + + @Test + void discussionThreadCreatedEventSerializesWithPluralDiscriminator() throws Exception { + OWLClass entity = new OWLClassImpl(OWLRDFVocabulary.OWL_THING.getIRI()); + var thread = new EntityDiscussionThread(threadId(), projectId(), entity, Status.OPEN, + ImmutableList.of()); + var event = new DiscussionThreadCreatedEvent(eventId(), projectId(), thread); + JsonContent json = threadCreatedTester.write(event); + assertThat(json).extractingJsonPathStringValue("$.['@type']").isEqualTo(THREAD_CREATED); + assertThat(DiscussionThreadCreatedEvent.CHANNEL).isEqualTo(THREAD_CREATED); + } +}