package de.westnordost.streetcomplete.data.osm.upload;
import android.content.SharedPreferences;
import android.os.Bundle;
import junit.framework.TestCase;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import de.westnordost.osmapi.changesets.ChangesetInfo;
import de.westnordost.osmapi.changesets.ChangesetsDao;
import de.westnordost.osmapi.common.Handler;
import de.westnordost.osmapi.common.errors.OsmConflictException;
import de.westnordost.osmapi.map.MapDataDao;
import de.westnordost.osmapi.map.data.BoundingBox;
import de.westnordost.osmapi.map.data.Element;
import de.westnordost.osmapi.map.data.OsmLatLon;
import de.westnordost.osmapi.map.data.OsmNode;
import de.westnordost.osmapi.user.User;
import de.westnordost.streetcomplete.Prefs;
import de.westnordost.streetcomplete.data.QuestStatus;
import de.westnordost.streetcomplete.data.changesets.OpenChangesetsDao;
import de.westnordost.streetcomplete.data.osm.OsmElementQuestType;
import de.westnordost.streetcomplete.data.osm.OsmQuest;
import de.westnordost.streetcomplete.data.osm.changes.StringMapChanges;
import de.westnordost.streetcomplete.data.osm.changes.StringMapChangesBuilder;
import de.westnordost.streetcomplete.data.osm.changes.StringMapEntryAdd;
import de.westnordost.streetcomplete.data.osm.changes.StringMapEntryChange;
import de.westnordost.streetcomplete.data.osm.changes.StringMapEntryDelete;
import de.westnordost.streetcomplete.data.osm.download.MapDataWithGeometryHandler;
import de.westnordost.streetcomplete.data.osm.persist.ElementGeometryDao;
import de.westnordost.streetcomplete.data.osm.persist.MergedElementDao;
import de.westnordost.streetcomplete.data.osm.persist.OsmQuestDao;
import de.westnordost.streetcomplete.data.statistics.QuestStatisticsDao;
import de.westnordost.streetcomplete.quests.AbstractQuestAnswerFragment;
import static org.mockito.Mockito.*;
public class OsmQuestChangesUploadTest extends TestCase
{
private static long A_NODE_ID = 5;
public void testCancel() throws InterruptedException
{
OsmQuestDao questDb = mock(OsmQuestDao.class);
ElementGeometryDao elementGeometryDao = mock(ElementGeometryDao.class);
MergedElementDao elementDB = mock(MergedElementDao.class);
OpenChangesetsDao openChangesetsDb = mock(OpenChangesetsDao.class);
when(questDb.getAll(null, QuestStatus.ANSWERED)).thenAnswer(
new Answer<List<OsmQuest>>()
{
@Override public List<OsmQuest> answer(InvocationOnMock invocation) throws Throwable
{
Thread.sleep(1000); // take your time...
ArrayList<OsmQuest> result = new ArrayList<>();
result.add(null);
return result;
}
});
final OsmQuestChangesUpload u = new OsmQuestChangesUpload(null, questDb, elementDB,
elementGeometryDao, null, openChangesetsDb, null, null);
final AtomicBoolean cancel = new AtomicBoolean(false);
Thread t = new Thread(new Runnable()
{
@Override public void run()
{
u.upload(cancel);
}
});
t.start();
cancel.set(true);
// cancelling the thread works if we come out here without exceptions. If the upload
// would actually try to start anything, there would be a nullpointer exception since we
// feeded it only with nulls to work with
t.join();
verify(elementGeometryDao).deleteUnreferenced();
verify(elementDB).deleteUnreferenced();
}
public void testDropChangeWhenElementDeleted()
{
OsmQuest quest = createAnsweredQuest(null);
OsmQuestDao questDb = mock(OsmQuestDao.class);
OsmQuestChangesUpload u = new OsmQuestChangesUpload(null, questDb, null, null, null, null, null, null);
assertFalse(u.uploadQuestChange(-1, quest, null, false, false));
verify(questDb).delete(quest.getId());
}
public void testDropChangeWhenUnresolvableElementChange()
{
OsmQuest quest = createAnsweredQuestWithNonAppliableChange();
Element element = createElement();
OsmQuestDao questDb = mock(OsmQuestDao.class);
MergedElementDao elementDao = mock(MergedElementDao.class);
OsmQuestChangesUpload u = new OsmQuestChangesUpload(null, questDb, null, null, null, null, null, null);
assertFalse(u.uploadQuestChange(123, quest, element, false, false));
verify(questDb).delete(quest.getId());
}
/* Simulates an element conflict while uploading the element, when updating the element from
mock server, it turns out that it has been deleted */
public void testHandleElementConflictAndThenDeleted()
{
final long changesetId = 123;
final long userId = 10;
OsmQuest quest = createAnsweredQuestWithAppliableChange();
Element element = createElement();
MergedElementDao elementDb = mock(MergedElementDao.class);
OsmQuestDao questDb = mock(OsmQuestDao.class);
MapDataDao mapDataDao = createMapDataDaoThatReportsConflictOnUploadAndNodeDeleted();
// a changeset dao+prefs that report that the changeset is open and the changeset is owned by the user
ChangesetsDao changesetsDao = mock(ChangesetsDao.class);
when(changesetsDao.get(changesetId)).thenReturn(createOpenChangesetForUser(userId));
SharedPreferences prefs = mock(SharedPreferences.class);
when(prefs.getLong(Prefs.OSM_USER_ID, -1)).thenReturn(userId);
OsmQuestChangesUpload u = new OsmQuestChangesUpload(mapDataDao, questDb, elementDb, null,
null, null, changesetsDao, prefs);
assertFalse(u.uploadQuestChange(changesetId, quest, element, false, false));
verify(questDb).delete(quest.getId());
verify(elementDb).delete(Element.Type.NODE, A_NODE_ID);
}
/* Simulates the changeset that is about to be used was created by a different user, so a new
* changeset needs to be created. (after that, it runs into the same case above, for simplicity
* sake*/
public void testHandleChangesetConflictDifferentUser()
{
final long userId = 10;
final long otherUserId = 15;
final long firstChangesetId = 123;
final long secondChangesetId = 124;
// reports that the changeset is open but does belong to another user
ChangesetsDao changesetsDao = mock(ChangesetsDao.class);
when(changesetsDao.get(firstChangesetId)).thenReturn(createOpenChangesetForUser(otherUserId));
when(changesetsDao.get(secondChangesetId)).thenReturn(createOpenChangesetForUser(userId));
doTestHandleChangesetConflict(changesetsDao, userId, firstChangesetId, secondChangesetId);
}
/* Simulates the changeset that is about to be used is already closed, so a new changeset needs
to be created. (after that, it runs into the same case above, for simplicity sake*/
public void testHandleChangesetConflictAlreadyClosed()
{
final long userId = 10;
final long firstChangesetId = 123;
final long secondChangesetId = 124;
// reports that the changeset is open but does belong to another user
ChangesetsDao changesetsDao = mock(ChangesetsDao.class);
when(changesetsDao.get(firstChangesetId)).thenReturn(createClosedChangesetForUser(userId));
when(changesetsDao.get(secondChangesetId)).thenReturn(createOpenChangesetForUser(userId));
doTestHandleChangesetConflict(changesetsDao, userId, firstChangesetId, secondChangesetId);
}
private void doTestHandleChangesetConflict(ChangesetsDao changesetsDao, long userId,
long firstChangesetId, long secondChangesetId)
{
OsmQuest quest = createAnsweredQuestWithAppliableChange();
Element element = createElement();
MergedElementDao elementDb = mock(MergedElementDao.class);
OsmQuestDao questDb = mock(OsmQuestDao.class);
OpenChangesetsDao manageChangesetsDb = mock(OpenChangesetsDao.class);
MapDataDao mapDataDao = createMapDataDaoThatReportsConflictOnUploadAndNodeDeleted();
when(mapDataDao.openChangeset(any(Map.class))).thenReturn(secondChangesetId);
SharedPreferences prefs = createPreferencesForUser(userId);
OsmQuestChangesUpload u = new OsmQuestChangesUpload(mapDataDao, questDb, elementDb, null,
null, manageChangesetsDb, changesetsDao, prefs);
assertFalse(u.uploadQuestChange(firstChangesetId, quest, element, false, false));
verify(manageChangesetsDb).replace("TestQuestType", secondChangesetId);
verify(questDb).delete(quest.getId());
verify(elementDb).delete(Element.Type.NODE, A_NODE_ID);
}
private static SharedPreferences createPreferencesForUser(long userId)
{
SharedPreferences prefs = mock(SharedPreferences.class);
when(prefs.getLong(Prefs.OSM_USER_ID, -1)).thenReturn(userId);
return prefs;
}
// this map data dao ensures that the program is running into a conflict and finally into
// an unresolvable one for the node (because it has been deleted).
private static MapDataDao createMapDataDaoThatReportsConflictOnUploadAndNodeDeleted()
{
MapDataDao mapDataDao = mock(MapDataDao.class);
doThrow(OsmConflictException.class).when(mapDataDao)
.uploadChanges(any(Long.class), any(Iterable.class), isNull(Handler.class));
when(mapDataDao.getNode(A_NODE_ID)).thenReturn(null);
return mapDataDao;
}
private static ChangesetInfo createOpenChangesetForUser(long id)
{
ChangesetInfo result = createChangesetForUser(id);
result.isOpen = true;
return result;
}
private static ChangesetInfo createClosedChangesetForUser(long id)
{
ChangesetInfo result = createChangesetForUser(id);
result.isOpen = false;
return result;
}
private static ChangesetInfo createChangesetForUser(long id)
{
ChangesetInfo result = new ChangesetInfo();
result.user = new User(id, "Hans Wurst");
return result;
}
public void testUploadNormally()
{
OsmQuest quest = createAnsweredQuestWithAppliableChange();
Element element = createElement();
OsmQuestDao questDb = mock(OsmQuestDao.class);
MapDataDao mapDataDao = mock(MapDataDao.class);
QuestStatisticsDao statisticsDao = mock(QuestStatisticsDao.class);
OsmQuestChangesUpload u = new OsmQuestChangesUpload(mapDataDao, questDb, null, null,
statisticsDao, null, null, null);
assertTrue(u.uploadQuestChange(1, quest, element, false, false));
verify(questDb).delete(quest.getId());
verify(statisticsDao).addOne("TestQuestType");
}
private static class TestQuestType implements OsmElementQuestType
{
@Override public void applyAnswerTo(Bundle answer, StringMapChangesBuilder changes) { }
@Override public String getCommitMessage() { return null; }
@Override public boolean download(BoundingBox bbox, MapDataWithGeometryHandler handler)
{
return true;
}
@Override public int importance() { return 0; }
@Override public AbstractQuestAnswerFragment createForm() { return null; }
@Override public String getIconName() { return null; }
}
private static OsmQuest createAnsweredQuestWithAppliableChange()
{
StringMapEntryChange aPossibleChange = new StringMapEntryAdd("somekey","value");
StringMapChanges changes = new StringMapChanges(Collections.singletonList(aPossibleChange));
return createAnsweredQuest(changes);
}
private static OsmQuest createAnsweredQuestWithNonAppliableChange()
{
StringMapEntryChange nonPossibleChange = new StringMapEntryDelete("somekey","value");
StringMapChanges changes = new StringMapChanges(Collections.singletonList(nonPossibleChange));
return createAnsweredQuest(changes);
}
private static OsmQuest createAnsweredQuest(StringMapChanges changes)
{
return new OsmQuest(3L, new TestQuestType(), Element.Type.NODE, A_NODE_ID,
QuestStatus.ANSWERED, changes, null, null);
}
private static Element createElement()
{
return new OsmNode(A_NODE_ID, 0, new OsmLatLon(1,2), new HashMap<String,String>());
}
}