diff --git a/app/src/test/java/net/osmtracker/db/DatabaseHelperTest.java b/app/src/test/java/net/osmtracker/db/DatabaseHelperTest.java new file mode 100644 index 00000000..020f5909 --- /dev/null +++ b/app/src/test/java/net/osmtracker/db/DatabaseHelperTest.java @@ -0,0 +1,584 @@ +package net.osmtracker.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import androidx.test.core.app.ApplicationProvider; + +import net.osmtracker.db.model.Track; + +import org.junit.After; +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 java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Intended-behaviour tests for {@link DatabaseHelper}. + * + *
Every test in this class describes what {@link DatabaseHelper} 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.md}. + * + *
The companion class {@link DatabaseHelperTestBugs} contains the tests that confirm each + * bug exists by asserting the current (broken) behaviour. + * + *
Currently fails — Bug B10: the ALTER TABLE in onUpgrade() case 18 uses + * {@code integer default 0} without {@code not null}, so after an upgrade the column + * permits NULL, diverging from the fresh-install schema. + * Remove {@literal @}Ignore and delete + * {@code DatabaseHelperTestBugs#bug_B10_segmentId_missingNotNull_afterUpgradeFrom18} + * once the bug is fixed. + */ + @Ignore("Bug B10 — segment_id is nullable after upgrade from v18. See docs/BUGS_DatabaseHelper.md") + @Test + public void onUpgrade_from18to19_segmentId_shouldBeNotNull() { + SQLiteDatabase rawDb = SQLiteDatabase.create(null); + try { + rawDb.execSQL("create table trackpoint (" + + "_id integer primary key autoincrement," + + "track_id integer not null," + + "latitude double not null," + + "longitude double not null," + + "speed double null," + + "elevation double null," + + "accuracy double null," + + "point_timestamp long not null," + + "compass_heading double null," + + "compass_accuracy integer null," + + "atmospheric_pressure double null)"); + + dbHelper.onUpgrade(rawDb, 18, 19); + + assertTrue("segment_id must be NOT NULL after upgrade from v18 (Bug B10)", + isColumnNotNull(rawDb, TrackContentProvider.Schema.TBL_TRACKPOINT, + TrackContentProvider.Schema.COL_SEG_ID)); + } finally { + rawDb.close(); + } + } + + // ── Group VII: Version ──────────────────────────────────────────────────── + + /** The database version must be 19. */ + @Test + public void dbVersion_is19() { + assertEquals("DB_VERSION must be 19", 19, db.getVersion()); + } +} diff --git a/app/src/test/java/net/osmtracker/db/DatabaseHelperTestBugs.java b/app/src/test/java/net/osmtracker/db/DatabaseHelperTestBugs.java new file mode 100644 index 00000000..963769d6 --- /dev/null +++ b/app/src/test/java/net/osmtracker/db/DatabaseHelperTestBugs.java @@ -0,0 +1,211 @@ +package net.osmtracker.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.After; +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.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Bug-confirming tests for {@link DatabaseHelper}. + * + *
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_DatabaseHelper.md} for the full description of each bug. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 25) +public class DatabaseHelperTestBugs { + + private DatabaseHelper dbHelper; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + dbHelper = new DatabaseHelper(context); + } + + @After + public void tearDown() { + dbHelper.close(); + } + + // ── Shared helpers ──────────────────────────────────────────────────────── + + /** + * Returns true if the given column has notnull=1 in PRAGMA table_info. + */ + private boolean isColumnNotNull(SQLiteDatabase database, String table, String column) { + Cursor c = database.rawQuery("PRAGMA table_info(" + table + ")", null); + try { + int nameIdx = c.getColumnIndex("name"); + int notNullIdx = c.getColumnIndex("notnull"); + while (c.moveToNext()) { + if (column.equals(c.getString(nameIdx))) { + return c.getInt(notNullIdx) == 1; + } + } + } finally { + c.close(); + } + return false; + } + + // ── Bug B10: segment_id missing NOT NULL after upgrade from v18 ────────── + + /** + * Bug B10 — After upgrading from v18 to v19, {@code segment_id} is nullable. + * + *
The {@code ALTER TABLE} in {@code onUpgrade()} case 18 uses + * {@code "integer default 0"} without {@code "not null"}, so after an upgrade + * the column permits NULL values. In contrast, a fresh install via + * {@code onCreate()} defines the column as {@code "integer not null default 0"}. + * + *
This test passes because the bug exists (notnull is 0 after upgrade). + * When Bug B10 is fixed, this test will fail and should be deleted. + * Remove {@literal @}Ignore from + * {@code DatabaseHelperTest#onUpgrade_from18to19_segmentId_shouldBeNotNull}. + * + * @see DatabaseHelperTest#onUpgrade_from18to19_segmentId_shouldBeNotNull + */ + @Test + public void bug_B10_segmentId_missingNotNull_afterUpgradeFrom18() { + SQLiteDatabase rawDb = SQLiteDatabase.create(null); + try { + // Simulate v18 trackpoint schema (no segment_id) + rawDb.execSQL("create table trackpoint (" + + "_id integer primary key autoincrement," + + "track_id integer not null," + + "latitude double not null," + + "longitude double not null," + + "speed double null," + + "elevation double null," + + "accuracy double null," + + "point_timestamp long not null," + + "compass_heading double null," + + "compass_accuracy integer null," + + "atmospheric_pressure double null)"); + + dbHelper.onUpgrade(rawDb, 18, 19); + + assertFalse("Bug B10: segment_id should be NOT NULL but is nullable after upgrade", + isColumnNotNull(rawDb, TrackContentProvider.Schema.TBL_TRACKPOINT, + TrackContentProvider.Schema.COL_SEG_ID)); + } finally { + rawDb.close(); + } + } + + /** + * Bug B10 — NULL can be inserted into segment_id after an upgrade from v18. + * + *
Because the ALTER TABLE omits NOT NULL, a NULL value can be successfully + * inserted into segment_id on an upgraded database. This would never happen + * on a fresh install where NOT NULL is enforced. + */ + @Test + public void bug_B10_segmentId_allowsNullInsert_afterUpgrade() { + SQLiteDatabase rawDb = SQLiteDatabase.create(null); + try { + rawDb.execSQL("create table trackpoint (" + + "_id integer primary key autoincrement," + + "track_id integer not null," + + "latitude double not null," + + "longitude double not null," + + "speed double null," + + "elevation double null," + + "accuracy double null," + + "point_timestamp long not null," + + "compass_heading double null," + + "compass_accuracy integer null," + + "atmospheric_pressure double null)"); + + dbHelper.onUpgrade(rawDb, 18, 19); + + // Insert with explicit NULL for segment_id + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_TRACK_ID, 1); + values.put(TrackContentProvider.Schema.COL_LATITUDE, 48.0); + values.put(TrackContentProvider.Schema.COL_LONGITUDE, 2.0); + values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis()); + values.putNull(TrackContentProvider.Schema.COL_SEG_ID); + + long rowId = rawDb.insert(TrackContentProvider.Schema.TBL_TRACKPOINT, null, values); + assertTrue("Bug B10: NULL insert into segment_id should succeed on upgraded DB", + rowId > 0); + + // Verify the value is actually NULL + Cursor c = rawDb.query(TrackContentProvider.Schema.TBL_TRACKPOINT, + new String[]{TrackContentProvider.Schema.COL_SEG_ID}, + "_id = ?", new String[]{String.valueOf(rowId)}, + null, null, null); + try { + assertTrue(c.moveToFirst()); + assertTrue("Bug B10: segment_id should be NULL", c.isNull(0)); + } finally { + c.close(); + } + } finally { + rawDb.close(); + } + } + + /** + * Bug B10 baseline — on a fresh install, segment_id enforces NOT NULL + * (or at least defaults to 0 when NULL is attempted). + * + *
This establishes the correct baseline against which the upgrade path diverges. + * On fresh installs, SQLite's NOT NULL with DEFAULT 0 causes a NULL insert to + * use the default value of 0 instead. + */ + @Test + public void bug_B10_segmentId_freshInstall_defaultsToZeroWhenNullInserted() { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_TRACK_ID, 1); + values.put(TrackContentProvider.Schema.COL_LATITUDE, 48.0); + values.put(TrackContentProvider.Schema.COL_LONGITUDE, 2.0); + values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis()); + // Don't set segment_id — should default to 0 + + long rowId = db.insert(TrackContentProvider.Schema.TBL_TRACKPOINT, null, values); + assertTrue("insert should succeed on fresh DB", rowId > 0); + + Cursor c = db.query(TrackContentProvider.Schema.TBL_TRACKPOINT, + new String[]{TrackContentProvider.Schema.COL_SEG_ID}, + "_id = ?", new String[]{String.valueOf(rowId)}, + null, null, null); + try { + assertTrue(c.moveToFirst()); + assertEquals("segment_id should default to 0 on fresh install", + 0, c.getInt(0)); + } finally { + c.close(); + } + } +}