package org.buddycloud.channelserver.channel.validate; import java.util.Date; import junit.framework.Assert; import org.buddycloud.channelserver.channel.ChannelManager; import org.buddycloud.channelserver.packetHandler.iq.TestHandler; import org.buddycloud.channelserver.pubsub.model.GlobalItemID; import org.buddycloud.channelserver.pubsub.model.NodeItem; import org.buddycloud.channelserver.pubsub.model.impl.GlobalItemIDImpl; import org.buddycloud.channelserver.pubsub.model.impl.NodeItemImpl; import org.buddycloud.channelserver.utils.node.item.payload.ActivityStreams; import org.buddycloud.channelserver.utils.node.item.payload.Buddycloud; import org.dom4j.Element; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; /** * TODO(work) Additional work required: * * - Check for (and add) activity stream verb * * - Test 'media' if present * * - Test 'meta' if present * */ public class AtomEntryTest extends TestHandler { private static final String ISO_8601_REGEX = "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}(Z|[+-][0-9]{2}:[0-9]{2})"; private AtomEntry validator; private IQ publishRequest; private Element publishEntry; private IQ replyRequest; private Element replyEntry; private IQ ratingRequest; private Element ratingEntry; private ChannelManager channelManager; JID jid = new JID("juliet@shakespeare.lit/balcony"); String node = "/users/romeo@shakespeare.lit/posts"; String server = "channels.shakespeare.lit"; @Before public void setUp() throws Exception { publishRequest = readStanzaAsIq("/iq/pubsub/publish/request.stanza"); publishEntry = publishRequest.getChildElement().element("publish").element("item"); replyRequest = readStanzaAsIq("/iq/pubsub/publish/reply.stanza"); replyEntry = replyRequest.getChildElement().element("publish").element("item"); ratingRequest = readStanzaAsIq("/iq/pubsub/publish/rating.stanza"); ratingEntry = ratingRequest.getChildElement().element("publish").element("item"); channelManager = Mockito.mock(ChannelManager.class); NodeItem item = new NodeItemImpl(node, "1", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.anyString())).thenReturn(item); } private AtomEntry getEntryObject(Element item) { AtomEntry validate = new AtomEntry(item); validate.setNode(node); validate.setTo(server); validate.setUser(jid); validate.setChannelManager(channelManager); return validate; } @Test public void notProvidingAnEntryReturnsError() throws Exception { validator = new AtomEntry(null); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.MISSING_ENTRY_ELEMENT, validator.getErrorMessage()); } @Test public void missingIdAttributeGetsAdded() throws Exception { Assert.assertEquals("96da02ee1baef61e767742844207bec4", publishEntry.element("entry").elementText("id")); Element item = (Element) this.publishEntry.clone(); item.element("entry").element("id").detach(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertTrue(entry.elementText("id").contains("tag:" + server + "," + node + ",")); } @Test public void emptyIdElementHasValueAdded() throws Exception { Assert.assertEquals("96da02ee1baef61e767742844207bec4", publishEntry.element("entry").elementText("id")); Element item = (Element) this.publishEntry.clone(); item.element("entry").element("id").detach(); item.element("entry").addElement("id"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertTrue(entry.elementText("id").contains("tag:" + server + "," + node + ",")); } @Test public void idElementIsIgnored() throws Exception { String id = "96da02ee1baef61e767742844207bec4"; Assert.assertEquals(id, publishEntry.element("entry").elementText("id")); Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertFalse(entry.elementText("id").contains(id)); } @Test public void missingTitleElementIsAdded() throws Exception { Assert.assertEquals("Post title", publishEntry.element("entry").elementText("title")); Element item = (Element) this.publishEntry.clone(); item.element("entry").element("title").detach(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals("Post", entry.elementText("title")); } @Test public void missingContentElementReturnsInvalid() throws Exception { Assert.assertNotNull(publishEntry.element("entry").element("content")); Element item = (Element) this.publishEntry.clone(); item.element("entry").element("content").detach(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.MISSING_CONTENT_ELEMENT, validator.getErrorMessage()); } @Test public void missingUpdatedElementHasValueAdded() throws Exception { Assert.assertEquals("2014-01-01T00:00:00.000Z", publishEntry.element("entry").elementText("updated")); Element item = (Element) this.publishEntry.clone(); item.element("entry").element("updated").detach(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertTrue(entry.elementText("updated").matches(ISO_8601_REGEX)); } @Test public void updateDateIsIgnored() throws Exception { String dateString = "2014-01-01T00:00:00.000Z"; Assert.assertEquals(dateString, publishEntry.element("entry").elementText("updated")); Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertFalse(entry.elementText("updated").equals(dateString)); } @Test public void authorEntryIsAdded() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Element author = entry.element("author"); Assert.assertNotNull(author); Assert.assertEquals(AtomEntry.AUTHOR_URI_PREFIX + jid.toBareJID(), author.elementText("uri")); Assert.assertEquals(jid.toBareJID(), author.elementText("name")); Assert.assertEquals(AtomEntry.AUTHOR_TYPE, author.elementText("object-type")); } @Test public void geolocationEntryIsAdded() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertNotNull(entry.element("geoloc")); item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); entry = validator.getPayload(); Assert.assertNull(entry.element("geoloc")); } @Test public void globalInReplyToIdIsMadeALocalId() throws Exception { Assert.assertEquals("tag:channels.shakespeare.lit,/users/romeo@shakespeare.lit/posts,fc362eb42085f017ed9ccd9c4004b095", replyEntry .element("entry").element("in-reply-to").attributeValue("ref")); Element item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals("fc362eb42085f017ed9ccd9c4004b095", entry.element("in-reply-to").attributeValue("ref")); } @Test public void postGetsNoteTypeReplyGetsCommentType() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals(AtomEntry.POST_TYPE_NOTE, entry.element("object").elementText("object-type")); item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); entry = validator.getPayload(); Assert.assertEquals(AtomEntry.POST_TYPE_COMMENT, entry.element("object").elementText("object-type")); } @Test public void badContentTypeReturnsError() throws Exception { Element item = (Element) this.publishEntry.clone(); item.element("entry").element("content").attribute("type").setText("emojis"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.UNSUPPORTED_CONTENT_TYPE, validator.getErrorMessage()); } @Test public void noContentTypeGetsDefaultedToText() throws Exception { Element item = (Element) this.publishEntry.clone(); item.element("entry").element("content").attribute("type").detach(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals(AtomEntry.CONTENT_TEXT, entry.element("content").attributeValue("type")); } @Test public void contentTypeGetsAddedAsRequired() throws Exception { Element item = (Element) this.publishEntry.clone(); item.element("entry").element("content").attribute("type").setText(AtomEntry.CONTENT_XHTML); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals(AtomEntry.CONTENT_XHTML, entry.element("content").attributeValue("type")); } @Test public void activityVerbIsAdded() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element entry = validator.getPayload(); Assert.assertEquals(AtomEntry.ACTIVITY_VERB_POST, entry.elementText("verb")); } @Test public void replyParentItemNotFoundResultsInError() throws Exception { Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.anyString())).thenReturn(null); Element item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.PARENT_ITEM_NOT_FOUND, validator.getErrorMessage()); } @Test public void canNotReplyToAReply() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "2", new Date(), "<entry/>", "1", new Date()); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.anyString())).thenReturn(nodeItem); Element item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.MAX_THREAD_DEPTH_EXCEEDED, validator.getErrorMessage()); } @Test public void targetElementWithoutInReplyToReturnsError() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "2", new Date(), "<entry/>", "1"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(null); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("in-reply-to").detach(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.IN_REPLY_TO_MISSING, validator.getErrorMessage()); } @Test public void missingTargetIdElementReturnsError() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "2", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(null); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").detach(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.MISSING_TARGET_ID, validator.getErrorMessage()); } @Test public void emptyTargetIdElementReturnsError() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "2", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(null); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").detach(); item.element("entry").element("target").addElement("id"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.MISSING_TARGET_ID, validator.getErrorMessage()); } @Test public void ifTargetedPostDoesntExistErrorIsReturned() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "1", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(null); Element item = (Element) this.ratingEntry.clone(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.TARGETED_ITEM_NOT_FOUND, validator.getErrorMessage()); } @Test public void ifTargetedPostIsntInSameThreadErrorIsReturnedParentCheck() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "1", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(nodeItem); Element item = (Element) this.ratingEntry.clone(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.TARGET_MUST_BE_IN_SAME_THREAD, validator.getErrorMessage()); } @Test public void ifTargetedPostIsntInSameThreadErrorIsReturnedThreadCheck() throws Exception { NodeItem nodeItem1 = new NodeItemImpl(node, "1", new Date(), "<entry/>"); NodeItem nodeItem2 = new NodeItemImpl(node, "B", new Date(), "<entry/>", "A"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem1); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("2"))).thenReturn(nodeItem2); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("B"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.TARGET_MUST_BE_IN_SAME_THREAD, validator.getErrorMessage()); } @Test public void ifTargetedIdIsSameAsReplyToIdOnlyOneDatabaseLookupPerformed() throws Exception { NodeItem nodeItem = new NodeItemImpl(node, "1", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.eq("1"))).thenReturn(nodeItem); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); validator.isValid(); Mockito.verify(channelManager, Mockito.times(1)).getNodeItem(Mockito.eq(node), Mockito.eq("1")); } @Test public void targetElementGetsAddedAsExpected() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); String expectedId = "tag:channels.shakespeare.lit,/users/romeo@shakespeare.lit/posts,1"; Assert.assertEquals(expectedId, payload.element("target").elementText("id")); Assert.assertEquals("post", payload.element("target").elementText("object-type")); } @Test public void missingInReplyToErrorsIfRatingElementPresent() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("in-reply-to").detach(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.IN_REPLY_TO_MISSING, validator.getErrorMessage()); } @Test public void missingTargetErrorsIfRatingElementPresent() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); item.element("entry").element("target").detach(); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.TARGET_ELEMENT_MISSING, validator.getErrorMessage()); } @Test public void invalidRatingValueReturnsError() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); item.element("entry").element("rating").setText("awesome"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.INVALID_RATING_VALUE, validator.getErrorMessage()); } @Test public void outOfRangeRatingReturnsError() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("rating").setText("6.0"); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.RATING_OUT_OF_RANGE, validator.getErrorMessage()); } @Test public void nonWholeNumberRatingReturnsError() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("rating").setText("4.1"); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.INVALID_RATING_VALUE, validator.getErrorMessage()); } @Test public void ratingElementGetsAddedToPayloadAsExpected() throws Exception { String rating = "4"; Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("rating").setText(rating); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(ActivityStreams.NS_REVIEW, payload.getNamespaceForPrefix("review").getText()); Assert.assertEquals(rating + ".0", payload.element("rating").getTextTrim()); } @Test public void postTitleIsSetToRatingWhenRated() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(ActivityStreams.NS_REVIEW, payload.getNamespaceForPrefix("review").getText()); Assert.assertEquals("Rating", payload.elementText("title")); } @Test public void postVerbGetsSwitchedToRated() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(AtomEntry.ACTIVITY_VERB_RATED, payload.elementText("verb")); } @Test public void postContentGetsReplacedWithRating() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); String expectedContent = "rating:5.0"; Assert.assertFalse(item.element("entry").elementText("content").equals(expectedContent)); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(expectedContent, payload.elementText("content")); } @Test public void canNotRateARating() throws Exception { String testPayload = "<entry xmlns=\"http://www.w3.org/2005/Atom\" " + "xmlns:review=\"http://activitystrea.ms/schema/1.0/review\">" + "<review:rating>5.0</review:rating></entry>"; NodeItem nodeItem = new NodeItemImpl(node, "1", new Date(), testPayload); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.anyString())).thenReturn(nodeItem); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.CAN_ONLY_RATE_A_POST, validator.getErrorMessage()); } @Test public void onlyAllowsSingleRatingPerPersonPerPost() throws Exception { Mockito.when(channelManager.userHasRatedPost(Mockito.anyString(), Mockito.any(JID.class), Mockito.any(GlobalItemID.class))).thenReturn(true); Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); validator = getEntryObject(item); Assert.assertFalse(validator.isValid()); Assert.assertEquals(AtomEntry.ITEM_ALREADY_RATED, validator.getErrorMessage()); } @Test public void suppliesLocalItemId() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); String localItemId = validator.getLocalItemId(); Assert.assertNotNull(localItemId); } @Test public void suppliesGlobalItemId() throws Exception { Element item = (Element) this.publishEntry.clone(); validator = getEntryObject(item); String globalItemId = validator.getGlobalItemId(); Assert.assertNotNull(globalItemId); Assert.assertTrue(GlobalItemIDImpl.isGlobalId(globalItemId)); } @Test public void suppliesInReplyTo() throws Exception { Assert.assertEquals("tag:channels.shakespeare.lit,/users/romeo@shakespeare.lit/posts,fc362eb42085f017ed9ccd9c4004b095", replyEntry .element("entry").element("in-reply-to").attributeValue("ref")); NodeItem nodeItem = new NodeItemImpl(node, "2", new Date(), "<entry/>"); Mockito.when(channelManager.getNodeItem(Mockito.eq(node), Mockito.anyString())).thenReturn(nodeItem); Element item = (Element) this.replyEntry.clone(); validator = getEntryObject(item); Assert.assertEquals("fc362eb42085f017ed9ccd9c4004b095", validator.getInReplyTo()); } @Test public void addsMediaNamespace() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); item.element("entry").addElement("media").addElement("item").addAttribute("channel", "marty@mcfly.com"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(Buddycloud.NS_MEDIA, payload.getNamespaceForPrefix(Buddycloud.NS_MEDIA_PREFIX).getText()); } @Test public void existingMediaNamespaceDoesntCauseConflict() throws Exception { Element item = (Element) this.ratingEntry.clone(); item.element("entry").element("target").element("id").setText("1"); item.element("entry").addNamespace(Buddycloud.NS_MEDIA_PREFIX, Buddycloud.NS_MEDIA); Element media = item.element("entry").addElement(Buddycloud.NS_MEDIA_PREFIX + ":media"); media.addElement("item").addAttribute("channel", "marty@mcfly.com"); validator = getEntryObject(item); Assert.assertTrue(validator.isValid()); Element payload = validator.getPayload(); Assert.assertEquals(Buddycloud.NS_MEDIA, item.element("entry").getNamespaceForPrefix(Buddycloud.NS_MEDIA_PREFIX).getText()); Assert.assertNotNull(payload.element(Buddycloud.NS_MEDIA_PREFIX + ":media")); } }