/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.streams.gplus.providers; import org.apache.streams.core.StreamsDatum; import org.apache.streams.google.gplus.configuration.UserInfo; import org.apache.streams.gplus.provider.GPlusUserActivityCollector; import org.apache.streams.gplus.serializer.util.GPlusActivityDeserializer; import org.apache.streams.jackson.StreamsJacksonMapper; import org.apache.streams.util.api.requests.backoff.BackOffStrategy; import org.apache.streams.util.api.requests.backoff.impl.ConstantTimeBackOffStrategy; import com.carrotsearch.randomizedtesting.RandomizedTest; import com.carrotsearch.randomizedtesting.annotations.Repeat; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.google.api.services.plus.Plus; import com.google.api.services.plus.model.Activity; import com.google.api.services.plus.model.ActivityFeed; import org.joda.time.DateTime; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit tests for {@link org.apache.streams.gplus.provider.GPlusUserActivityCollector} */ public class TestGPlusUserActivityCollector extends RandomizedTest { private static final String ACTIVITY_TEMPLATE = "{ \"kind\": \"plus#activity\", \"etag\": \"\\\"Vea_b94Y77GDGgRK7gFNPnolKQw/v1-6aVSBGT4qiStMoz7f2_AN2fM\\\"\", \"title\": \"\", \"published\": \"%s\", \"updated\": \"2014-10-27T06:26:33.927Z\", \"id\": \"z13twrlznpvtzz52w22mdt1y0k3of1djw04\", \"url\": \"https://plus.google.com/116771159471120611293/posts/GR7CGR8N5VL\", \"actor\": { \"id\": \"116771159471120611293\", \"displayName\": \"displayName\", \"url\": \"https://plus.google.com/116771159471120611293\", \"image\": { \"url\": \"https://lh6.googleusercontent.com/-C0fiZBxdvw0/AAAAAAAAAAI/AAAAAAAAJ5k/K4pgR3_-_ms/photo.jpg?sz=50\" } }, \"verb\": \"share\", \"object\": { \"objectType\": \"activity\", \"id\": \"z13zgvtiurjgfti1v234iflghvq2c1dge04\", \"actor\": { \"id\": \"104954254300557350002\", \"displayName\": \"displayName\", \"url\": \"https://plus.google.com/104954254300557350002\", \"image\": { \"url\": \"https://lh4.googleusercontent.com/-SO1scj4p2LA/AAAAAAAAAAI/AAAAAAAAI-s/efA9LBVe144/photo.jpg?sz=50\" } }, \"content\": \"\", \"url\": \"https://plus.google.com/104954254300557350002/posts/AwewXhtn7ws\", \"replies\": { \"totalItems\": 0, \"selfLink\": \"https://content.googleapis.com/plus/v1/activities/z13twrlznpvtzz52w22mdt1y0k3of1djw04/comments\" }, \"plusoners\": { \"totalItems\": 9, \"selfLink\": \"https://content.googleapis.com/plus/v1/activities/z13twrlznpvtzz52w22mdt1y0k3of1djw04/people/plusoners\" }, \"resharers\": { \"totalItems\": 0, \"selfLink\": \"https://content.googleapis.com/plus/v1/activities/z13twrlznpvtzz52w22mdt1y0k3of1djw04/people/resharers\" }, \"attachments\": [ { \"objectType\": \"photo\", \"id\": \"104954254300557350002.6074732746360957410\", \"content\": \"26/10/2014 - 1\", \"url\": \"https://plus.google.com/photos/104954254300557350002/albums/6074732747132702225/6074732746360957410\", \"image\": { \"url\": \"https://lh4.googleusercontent.com/-oO3fnARlDm0/VE3JP1xHKeI/AAAAAAAAeCY/-X2jzc6HruA/w506-h750/2014%2B-%2B1\", \"type\": \"image/jpeg\" }, \"fullImage\": { \"url\": \"https://lh4.googleusercontent.com/-oO3fnARlDm0/VE3JP1xHKeI/AAAAAAAAeCY/-X2jzc6HruA/w600-h1141/2014%2B-%2B1\", \"type\": \"image/jpeg\", \"height\": 1141, \"width\": 600 } } ] }, \"annotation\": \"Truth 😜\", \"provider\": { \"title\": \"Reshared Post\" }, \"access\": { \"kind\": \"plus#acl\", \"description\": \"Public\", \"items\": [ { \"type\": \"public\" } ] } }"; private static final ObjectMapper MAPPER = StreamsJacksonMapper.getInstance(); private static final String IN_RANGE_IDENTIFIER = "data in range"; static { SimpleModule simpleModule = new SimpleModule(); simpleModule.addDeserializer(Activity.class, new GPlusActivityDeserializer()); MAPPER.registerModule(simpleModule); MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } private static ActivityFeed createMockActivityFeed( int numBefore, int numAfter, int numInRange, DateTime after, DateTime before, boolean page) { ActivityFeed feed = new ActivityFeed(); List<Activity> list = new LinkedList<>(); for (int i = 0; i < numAfter; ++i) { DateTime published = before.plus(randomIntBetween(0, Integer.MAX_VALUE)); Activity activity = createActivityWithPublishedDate(published); list.add(activity); } for (int i = 0; i < numInRange; ++i) { DateTime published; if ((before == null && after == null) || before == null) { published = DateTime.now(); // no date range or end time date range so just make the time now. } else if (after == null) { published = before.minusMillis(randomIntBetween(1, Integer.MAX_VALUE)); //no beginning to range } else { // has to be in range long range = before.getMillis() - after.getMillis(); published = after.plus(range / 2); //in the middle } Activity activity = createActivityWithPublishedDate(published); activity.setTitle(IN_RANGE_IDENTIFIER); list.add(activity); } for (int i = 0; i < numBefore; ++i) { DateTime published = after.minusMillis(randomIntBetween(1, Integer.MAX_VALUE)); Activity activity = createActivityWithPublishedDate(published); list.add(activity); } if (page) { feed.setNextPageToken("A"); } else { feed.setNextPageToken(null); } feed.setItems(list); return feed; } private static Activity createActivityWithPublishedDate(DateTime dateTime) { Activity activity = new Activity(); activity.setPublished(new com.google.api.client.util.DateTime(dateTime.getMillis())); activity.setId("a"); return activity; } /** * Creates a randomized activity and randomized date range. * <p/> * The activity feed is separated into three chunks, * |. . . data too recent to be in date range . . .||. . . data in date range. . .||. . . data too old to be in date range| * [index 0, ............................................................................................., index length-1] * <p/> * Inside of those chunks data has no order, but the list is ordered by those three chunks. * <p/> * The test will check to see if the num of data in the date range make onto the output queue. */ @Test @Repeat(iterations = 3) public void testWithBeforeAndAfterDates() throws InterruptedException { //initialize counts assuming no date ranges will be used int numActivities = randomIntBetween(0, 1000); int numActivitiesInDateRange = numActivities; int numberOutOfRange = 0; int numBeforeRange = 0; int numAfterRange = 0; //determine if date ranges will be used DateTime beforeDate = null; DateTime afterDate = null; if (randomInt() % 2 == 0) { beforeDate = DateTime.now().minusDays(randomIntBetween(1, 5)); } if (randomInt() % 2 == 0) { if (beforeDate == null) { afterDate = DateTime.now().minusDays(randomIntBetween(1, 10)); } else { afterDate = beforeDate.minusDays(randomIntBetween(1, 10)); } } //update counts if date ranges are going to be used. if (beforeDate != null || afterDate != null) { //assign amount to be in range numActivitiesInDateRange = randomIntBetween(0, numActivities); numberOutOfRange = numActivities - numActivitiesInDateRange; } if (beforeDate == null && afterDate != null) { //assign all out of range to be before the start of the range numBeforeRange = numberOutOfRange; } else if (beforeDate != null && afterDate == null) { //assign all out of range to be after the start of the range numAfterRange = numberOutOfRange; } else if (beforeDate != null && afterDate != null) { //assign half before range and half after the range numAfterRange = (numberOutOfRange / 2) + (numberOutOfRange % 2); numBeforeRange = numberOutOfRange / 2; } Plus plus = createMockPlus(numBeforeRange, numAfterRange, numActivitiesInDateRange, afterDate, beforeDate); BackOffStrategy strategy = new ConstantTimeBackOffStrategy(1); BlockingQueue<StreamsDatum> datums = new LinkedBlockingQueue<>(); UserInfo userInfo = new UserInfo(); userInfo.setUserId("A"); userInfo.setAfterDate(afterDate); userInfo.setBeforeDate(beforeDate); GPlusUserActivityCollector collector = new GPlusUserActivityCollector(plus, datums, strategy, userInfo); collector.run(); assertEquals(numActivitiesInDateRange, datums.size()); while (!datums.isEmpty()) { StreamsDatum datum = datums.take(); assertNotNull(datum); assertNotNull(datum.getDocument()); assertTrue(datum.getDocument() instanceof String); assertTrue(((String)datum.getDocument()).contains(IN_RANGE_IDENTIFIER)); //only in range documents are on the out going queue. } } private Plus createMockPlus(final int numBefore, final int numAfter, final int numInRange, final DateTime after, final DateTime before) { Plus plus = mock(Plus.class); final Plus.Activities activities = createMockPlusActivities(numBefore, numAfter, numInRange, after, before); doAnswer(invocationOnMock -> activities).when(plus).activities(); return plus; } private Plus.Activities createMockPlusActivities( final int numBefore, final int numAfter, final int numInRange, final DateTime after, final DateTime before) { Plus.Activities activities = mock(Plus.Activities.class); try { Plus.Activities.List list = createMockPlusActivitiesList(numBefore, numAfter, numInRange, after, before); when(activities.list(anyString(), anyString())).thenReturn(list); } catch (IOException ioe) { fail("Should not have thrown exception while creating mock. : " + ioe.getMessage()); } return activities; } private Plus.Activities.List createMockPlusActivitiesList( final int numBefore, final int numAfter, final int numInRange, final DateTime after, final DateTime before) { Plus.Activities.List list = mock(Plus.Activities.List.class); when(list.setMaxResults(anyLong())).thenReturn(list); when(list.setPageToken(anyString())).thenReturn(list); ActivityFeedAnswer answer = new ActivityFeedAnswer(numBefore, numAfter, numInRange, after, before); try { doAnswer(answer).when(list).execute(); } catch (IOException ioe) { fail("Should not have thrown exception while creating mock. : " + ioe.getMessage()); } return list; } private static class ActivityFeedAnswer implements Answer<ActivityFeed> { private int afterCount = 0; private int beforeCount = 0; private int inCount = 0; private int maxBatch = 100; private int numAfter; private int numInRange; private int numBefore; private DateTime after; private DateTime before; private ActivityFeedAnswer(int numBefore, int numAfter, int numInRange, DateTime after, DateTime before) { this.numBefore = numBefore; this.numAfter = numAfter; this.numInRange = numInRange; this.after = after; this.before = before; } @Override public ActivityFeed answer(InvocationOnMock invocationOnMock) throws Throwable { int totalCount = 0; int batchAfter = 0; int batchBefore = 0; int batchIn = 0; if (afterCount != numAfter) { if (numAfter - afterCount >= maxBatch) { afterCount += maxBatch; batchAfter += maxBatch; totalCount += batchAfter; } else { batchAfter += numAfter - afterCount; totalCount += numAfter - afterCount; afterCount = numAfter; } } if (totalCount < maxBatch && inCount != numInRange) { if (numInRange - inCount >= maxBatch - totalCount) { inCount += maxBatch - totalCount; batchIn += maxBatch - totalCount; totalCount += batchIn; } else { batchIn += numInRange - inCount; totalCount += numInRange - inCount; inCount = numInRange; } } if (totalCount < maxBatch && beforeCount != numBefore) { if (numBefore - batchBefore >= maxBatch - totalCount) { batchBefore += maxBatch - totalCount; totalCount = maxBatch; beforeCount += batchBefore; } else { batchBefore += numBefore - beforeCount; totalCount += numBefore - beforeCount; beforeCount = numBefore; } } return createMockActivityFeed( batchBefore, batchAfter, batchIn, after, before, numAfter != afterCount || inCount != numInRange || beforeCount != numBefore); } } }