diff --git a/app/src/test/java/net/osmtracker/db/TrackContentProviderTest.java b/app/src/test/java/net/osmtracker/db/TrackContentProviderTest.java new file mode 100644 index 00000000..d040e6c9 --- /dev/null +++ b/app/src/test/java/net/osmtracker/db/TrackContentProviderTest.java @@ -0,0 +1,728 @@ +package net.osmtracker.db; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import androidx.test.core.app.ApplicationProvider; + +import net.osmtracker.db.model.Track; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * Intended-behaviour tests for {@link TrackContentProvider}. + * + *
Every test in this class describes what {@link TrackContentProvider} should do. + * Tests that are currently broken by a known bug are annotated with {@link Ignore} + * and reference the bug ID documented in {@code docs/BUGS_TrackContentProvider.md}. + * + *
The companion class {@link TrackContentProviderTestBugs} contains the tests that confirm each + * bug exists by asserting the current (broken) behaviour. + * + *
Currently fails — Bug B11: the delete(TRACK_ID) case removes waypoints + * and trackpoints but does not delete notes. Notes are orphaned. + * Remove {@literal @}Ignore and delete + * {@code TrackContentProviderTestBugs#bug_B11_delete_trackById_doesNotDeleteNotes} + * once the bug is fixed. + */ + @Ignore("Bug B11 — delete(TRACK_ID) does not delete notes. See docs/BUGS_TrackContentProvider.md") + @Test + public void delete_trackById_alsoRemovesNotes() { + long trackId = insertTrack(); + insertNote(trackId, "n-del-1"); + insertNote(trackId, "n-del-2"); + + Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); + resolver.delete(trackUri, null, null); + + Cursor c = resolver.query(TrackContentProvider.notesUri(trackId), + null, null, null, null); + try { + assertNotNull(c); + assertEquals("notes should be deleted with track", 0, c.getCount()); + } finally { + if (c != null) c.close(); + } + } + + @Test + public void delete_waypointByUuid_removesWaypoint() { + long trackId = insertTrack(); + insertWaypoint(trackId, "wp-to-delete"); + insertWaypoint(trackId, "wp-to-keep"); + + Uri deleteUri = Uri.withAppendedPath( + TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, "wp-to-delete"); + int count = resolver.delete(deleteUri, null, null); + assertEquals("should delete 1 waypoint", 1, count); + + // Verify the other waypoint remains + Cursor c = resolver.query(TrackContentProvider.waypointsUri(trackId), + null, null, null, null); + try { + assertNotNull(c); + assertEquals("1 waypoint should remain", 1, c.getCount()); + } finally { + if (c != null) c.close(); + } + } + + @Test + public void delete_noteByUuid_removesNote() { + long trackId = insertTrack(); + insertNote(trackId, "note-to-delete"); + insertNote(trackId, "note-to-keep"); + + Uri deleteUri = Uri.withAppendedPath( + TrackContentProvider.CONTENT_URI_NOTE_UUID, "note-to-delete"); + int count = resolver.delete(deleteUri, null, null); + assertEquals("should delete 1 note", 1, count); + + Cursor c = resolver.query(TrackContentProvider.notesUri(trackId), + null, null, null, null); + try { + assertNotNull(c); + assertEquals("1 note should remain", 1, c.getCount()); + } finally { + if (c != null) c.close(); + } + } + + @Test + public void delete_unknownUri_throws() { + Uri badUri = Uri.parse("content://" + TrackContentProvider.AUTHORITY + "/nonexistent"); + assertThrows(IllegalArgumentException.class, + () -> resolver.delete(badUri, null, null)); + } + + // ── Group V: getType() ─────────────────────────────────────────────────── + + @Test + public void getType_track_returnsDirType() { + String type = resolver.getType(TrackContentProvider.CONTENT_URI_TRACK); + assertNotNull(type); + assertTrue("should be a dir type", type.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)); + assertTrue("should mention track table", type.contains(TrackContentProvider.Schema.TBL_TRACK)); + } + + @Test + public void getType_trackTrackpoints_returnsDirType() { + long trackId = insertTrack(); + String type = resolver.getType(TrackContentProvider.trackPointsUri(trackId)); + assertNotNull(type); + assertTrue("should be a dir type", type.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)); + assertTrue("should mention trackpoint", type.contains(TrackContentProvider.Schema.TBL_TRACKPOINT)); + } + + @Test + public void getType_trackWaypoints_returnsDirType() { + long trackId = insertTrack(); + String type = resolver.getType(TrackContentProvider.waypointsUri(trackId)); + assertNotNull(type); + assertTrue("should be a dir type", type.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)); + assertTrue("should mention waypoint", type.contains(TrackContentProvider.Schema.TBL_WAYPOINT)); + } + + @Test + public void getType_trackNotes_returnsDirType() { + long trackId = insertTrack(); + String type = resolver.getType(TrackContentProvider.notesUri(trackId)); + assertNotNull(type); + assertTrue("should be a dir type", type.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)); + assertTrue("should mention note", type.contains(TrackContentProvider.Schema.TBL_NOTE)); + } + + // Bug B12 — the following 9 getType() tests are @Ignored because they throw + // IllegalArgumentException for valid URIs. See docs/BUGS_TrackContentProvider.md. + + @Ignore("Bug B12 — getType() throws for TRACK_ID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_trackId_returnsItemType() { + long trackId = insertTrack(); + Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); + String type = resolver.getType(trackUri); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for TRACK_ACTIVE URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_trackActive_returnsDirType() { + String type = resolver.getType(TrackContentProvider.CONTENT_URI_TRACK_ACTIVE); + assertNotNull(type); + assertTrue("should be a dir type", type.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for WAYPOINT_ID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_waypointId_returnsItemType() { + String type = resolver.getType(TrackContentProvider.waypointUri(1)); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for WAYPOINT_UUID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_waypointUuid_returnsItemType() { + Uri uri = Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, "test-uuid"); + String type = resolver.getType(uri); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for TRACKPOINT_ID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_trackpointId_returnsItemType() { + String type = resolver.getType(TrackContentProvider.trackpointUri(1)); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for NOTE_ID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_noteId_returnsItemType() { + String type = resolver.getType(TrackContentProvider.noteUri(1)); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for NOTE_UUID URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_noteUuid_returnsItemType() { + Uri uri = Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_NOTE_UUID, "test-uuid"); + String type = resolver.getType(uri); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for TRACK_START URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_trackStart_returnsItemType() { + long trackId = insertTrack(); + String type = resolver.getType(TrackContentProvider.trackStartUri(trackId)); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + @Ignore("Bug B12 — getType() throws for TRACK_END URI. See docs/BUGS_TrackContentProvider.md") + @Test + public void getType_trackEnd_returnsItemType() { + long trackId = insertTrack(); + String type = resolver.getType(TrackContentProvider.trackEndUri(trackId)); + assertNotNull(type); + assertTrue("should be an item type", type.startsWith(ContentResolver.CURSOR_ITEM_BASE_TYPE)); + } + + // ── Group VI: URI helpers ──────────────────────────────────────────────── + + @Test + public void waypointsUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.waypointsUri(42); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/track/42/waypoints", + uri.toString()); + } + + @Test + public void trackPointsUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.trackPointsUri(42); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/track/42/trackpoints", + uri.toString()); + } + + @Test + public void notesUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.notesUri(42); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/track/42/notes", + uri.toString()); + } + + @Test + public void trackStartUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.trackStartUri(42); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/track/42/start", + uri.toString()); + } + + @Test + public void trackEndUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.trackEndUri(42); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/track/42/end", + uri.toString()); + } + + @Test + public void waypointUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.waypointUri(7); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/waypoint/7", + uri.toString()); + } + + @Test + public void noteUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.noteUri(7); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/note/7", + uri.toString()); + } + + @Test + public void trackpointUri_hasCorrectFormat() { + Uri uri = TrackContentProvider.trackpointUri(7); + assertEquals("content://" + TrackContentProvider.AUTHORITY + "/trackpoint/7", + uri.toString()); + } +} diff --git a/app/src/test/java/net/osmtracker/db/TrackContentProviderTestBugs.java b/app/src/test/java/net/osmtracker/db/TrackContentProviderTestBugs.java new file mode 100644 index 00000000..804df56f --- /dev/null +++ b/app/src/test/java/net/osmtracker/db/TrackContentProviderTestBugs.java @@ -0,0 +1,200 @@ +package net.osmtracker.db; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * Bug-confirming tests for {@link TrackContentProvider}. + * + *
Every test in this class documents a known bug by asserting the current + * broken behaviour. Each test passes because the bug exists — the tests are expected + * to fail once the corresponding bug is fixed. + * + *
When a bug is fixed: + *
See {@code docs/BUGS_TrackContentProvider.md} for the full description of each bug. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 25) +public class TrackContentProviderTestBugs { + + private ContentResolver resolver; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + resolver = context.getContentResolver(); + } + + // ── Shared helpers ──────────────────────────────────────────────────────── + + private long insertTrack() { + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_START_DATE, System.currentTimeMillis()); + Uri uri = resolver.insert(TrackContentProvider.CONTENT_URI_TRACK, values); + assertNotNull(uri); + return ContentUris.parseId(uri); + } + + private Uri insertNote(long trackId, String uuid) { + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_TRACK_ID, trackId); + values.put(TrackContentProvider.Schema.COL_LATITUDE, 40.7); + values.put(TrackContentProvider.Schema.COL_LONGITUDE, -74.0); + values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis()); + values.put(TrackContentProvider.Schema.COL_NAME, "Note " + uuid); + values.put(TrackContentProvider.Schema.COL_UUID, uuid); + return resolver.insert(TrackContentProvider.notesUri(trackId), values); + } + + // ── Bug B11: delete(TRACK_ID) does not delete notes ────────────────────── + + /** + * Bug B11 — When deleting a track by ID, the associated notes are NOT deleted. + * + *
The {@code delete()} method in {@code TrackContentProvider} handles + * {@code URI_CODE_TRACK_ID} by deleting waypoints and trackpoints, but it does + * not delete notes from the note table. This leaves orphaned note rows in the database. + * + *
This test passes because the bug exists (notes remain after track deletion). + * When Bug B11 is fixed, this test will fail and should be deleted. + * Remove {@literal @}Ignore from + * {@code TrackContentProviderTest#delete_trackById_alsoRemovesNotes}. + * + * @see TrackContentProviderTest#delete_trackById_alsoRemovesNotes + */ + @Test + public void bug_B11_delete_trackById_doesNotDeleteNotes() { + long trackId = insertTrack(); + insertNote(trackId, "orphan-note-1"); + insertNote(trackId, "orphan-note-2"); + + // Delete the track + Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); + resolver.delete(trackUri, null, null); + + // Verify notes are still present (the bug) + Cursor c = resolver.query(TrackContentProvider.notesUri(trackId), + null, null, null, null); + try { + assertNotNull(c); + assertTrue("Bug B11: notes should remain after track deletion (they are orphaned)", + c.getCount() > 0); + assertEquals("Bug B11: both notes should be orphaned", 2, c.getCount()); + } finally { + if (c != null) c.close(); + } + } + + // ── Bug B12: getType() throws for 9 valid URI codes ────────────────────── + + /** + * Bug B12 — {@code getType()} throws {@code IllegalArgumentException} for + * {@code URI_CODE_TRACK_ID}. Only 4 of 13 valid URI codes are handled. + * + *
Each test below confirms that getType() throws for a valid URI. + * When Bug B12 is fixed, these tests will fail and should be deleted. + * Remove {@literal @}Ignore from the corresponding tests in + * {@link TrackContentProviderTest}. + */ + @Test + public void bug_B12_getType_throwsForTrackId() { + long trackId = insertTrack(); + Uri uri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); + assertThrows("Bug B12: getType should not throw for track/#", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForTrackActive() { + assertThrows("Bug B12: getType should not throw for track/active", + IllegalArgumentException.class, + () -> resolver.getType(TrackContentProvider.CONTENT_URI_TRACK_ACTIVE)); + } + + @Test + public void bug_B12_getType_throwsForWaypointId() { + Uri uri = TrackContentProvider.waypointUri(1); + assertThrows("Bug B12: getType should not throw for waypoint/#", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForWaypointUuid() { + Uri uri = Uri.withAppendedPath( + TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, "test-uuid"); + assertThrows("Bug B12: getType should not throw for waypoint/uuid/*", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForTrackpointId() { + Uri uri = TrackContentProvider.trackpointUri(1); + assertThrows("Bug B12: getType should not throw for trackpoint/#", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForNoteId() { + Uri uri = TrackContentProvider.noteUri(1); + assertThrows("Bug B12: getType should not throw for note/#", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForNoteUuid() { + Uri uri = Uri.withAppendedPath( + TrackContentProvider.CONTENT_URI_NOTE_UUID, "test-uuid"); + assertThrows("Bug B12: getType should not throw for note/uuid/*", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForTrackStart() { + long trackId = insertTrack(); + Uri uri = TrackContentProvider.trackStartUri(trackId); + assertThrows("Bug B12: getType should not throw for track/#/start", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } + + @Test + public void bug_B12_getType_throwsForTrackEnd() { + long trackId = insertTrack(); + Uri uri = TrackContentProvider.trackEndUri(trackId); + assertThrows("Bug B12: getType should not throw for track/#/end", + IllegalArgumentException.class, + () -> resolver.getType(uri)); + } +}