|
| 1 | +package net.osmtracker.db; |
| 2 | + |
| 3 | +import android.content.ContentValues; |
| 4 | +import android.content.Context; |
| 5 | +import android.database.Cursor; |
| 6 | +import android.database.sqlite.SQLiteDatabase; |
| 7 | + |
| 8 | +import androidx.test.core.app.ApplicationProvider; |
| 9 | + |
| 10 | +import org.junit.After; |
| 11 | +import org.junit.Before; |
| 12 | +import org.junit.Test; |
| 13 | +import org.junit.runner.RunWith; |
| 14 | +import org.robolectric.RobolectricTestRunner; |
| 15 | +import org.robolectric.annotation.Config; |
| 16 | + |
| 17 | +import static org.junit.Assert.assertEquals; |
| 18 | +import static org.junit.Assert.assertFalse; |
| 19 | +import static org.junit.Assert.assertTrue; |
| 20 | + |
| 21 | +/** |
| 22 | + * Bug-confirming tests for {@link DatabaseHelper}. |
| 23 | + * |
| 24 | + * <p>Every test in this class documents a <strong>known bug</strong> by asserting the current |
| 25 | + * broken behaviour. Each test passes <em>because</em> the bug exists — the tests are expected |
| 26 | + * to <strong>fail</strong> once the corresponding bug is fixed. |
| 27 | + * |
| 28 | + * <p>When a bug is fixed: |
| 29 | + * <ol> |
| 30 | + * <li>Remove the {@literal @}Ignore annotation from the matching test in |
| 31 | + * {@link DatabaseHelperTest} (the intended-behaviour companion).</li> |
| 32 | + * <li>Delete (or permanently skip) the test in this class — it no longer represents |
| 33 | + * correct expected behaviour.</li> |
| 34 | + * <li>Run {@code ./gradlew testDebugUnitTest} — the formerly-{@literal @}Ignored test in |
| 35 | + * {@link DatabaseHelperTest} must now pass.</li> |
| 36 | + * </ol> |
| 37 | + * |
| 38 | + * <p>See {@code docs/BUGS_DatabaseHelper.md} for the full description of each bug. |
| 39 | + */ |
| 40 | +@RunWith(RobolectricTestRunner.class) |
| 41 | +@Config(sdk = 25) |
| 42 | +public class DatabaseHelperTestBugs { |
| 43 | + |
| 44 | + private DatabaseHelper dbHelper; |
| 45 | + |
| 46 | + @Before |
| 47 | + public void setUp() { |
| 48 | + Context context = ApplicationProvider.getApplicationContext(); |
| 49 | + dbHelper = new DatabaseHelper(context); |
| 50 | + } |
| 51 | + |
| 52 | + @After |
| 53 | + public void tearDown() { |
| 54 | + dbHelper.close(); |
| 55 | + } |
| 56 | + |
| 57 | + // ── Shared helpers ──────────────────────────────────────────────────────── |
| 58 | + |
| 59 | + /** |
| 60 | + * Returns true if the given column has notnull=1 in PRAGMA table_info. |
| 61 | + */ |
| 62 | + private boolean isColumnNotNull(SQLiteDatabase database, String table, String column) { |
| 63 | + Cursor c = database.rawQuery("PRAGMA table_info(" + table + ")", null); |
| 64 | + try { |
| 65 | + int nameIdx = c.getColumnIndex("name"); |
| 66 | + int notNullIdx = c.getColumnIndex("notnull"); |
| 67 | + while (c.moveToNext()) { |
| 68 | + if (column.equals(c.getString(nameIdx))) { |
| 69 | + return c.getInt(notNullIdx) == 1; |
| 70 | + } |
| 71 | + } |
| 72 | + } finally { |
| 73 | + c.close(); |
| 74 | + } |
| 75 | + return false; |
| 76 | + } |
| 77 | + |
| 78 | + // ── Bug B10: segment_id missing NOT NULL after upgrade from v18 ────────── |
| 79 | + |
| 80 | + /** |
| 81 | + * Bug B10 — After upgrading from v18 to v19, {@code segment_id} is nullable. |
| 82 | + * |
| 83 | + * <p>The {@code ALTER TABLE} in {@code onUpgrade()} case 18 uses |
| 84 | + * {@code "integer default 0"} without {@code "not null"}, so after an upgrade |
| 85 | + * the column permits NULL values. In contrast, a fresh install via |
| 86 | + * {@code onCreate()} defines the column as {@code "integer not null default 0"}. |
| 87 | + * |
| 88 | + * <p>This test passes because the bug exists (notnull is 0 after upgrade). |
| 89 | + * When Bug B10 is fixed, this test will fail and should be deleted. |
| 90 | + * Remove {@literal @}Ignore from |
| 91 | + * {@code DatabaseHelperTest#onUpgrade_from18to19_segmentId_shouldBeNotNull}. |
| 92 | + * |
| 93 | + * @see DatabaseHelperTest#onUpgrade_from18to19_segmentId_shouldBeNotNull |
| 94 | + */ |
| 95 | + @Test |
| 96 | + public void bug_B10_segmentId_missingNotNull_afterUpgradeFrom18() { |
| 97 | + SQLiteDatabase rawDb = SQLiteDatabase.create(null); |
| 98 | + try { |
| 99 | + // Simulate v18 trackpoint schema (no segment_id) |
| 100 | + rawDb.execSQL("create table trackpoint (" |
| 101 | + + "_id integer primary key autoincrement," |
| 102 | + + "track_id integer not null," |
| 103 | + + "latitude double not null," |
| 104 | + + "longitude double not null," |
| 105 | + + "speed double null," |
| 106 | + + "elevation double null," |
| 107 | + + "accuracy double null," |
| 108 | + + "point_timestamp long not null," |
| 109 | + + "compass_heading double null," |
| 110 | + + "compass_accuracy integer null," |
| 111 | + + "atmospheric_pressure double null)"); |
| 112 | + |
| 113 | + dbHelper.onUpgrade(rawDb, 18, 19); |
| 114 | + |
| 115 | + assertFalse("Bug B10: segment_id should be NOT NULL but is nullable after upgrade", |
| 116 | + isColumnNotNull(rawDb, TrackContentProvider.Schema.TBL_TRACKPOINT, |
| 117 | + TrackContentProvider.Schema.COL_SEG_ID)); |
| 118 | + } finally { |
| 119 | + rawDb.close(); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Bug B10 — NULL can be inserted into segment_id after an upgrade from v18. |
| 125 | + * |
| 126 | + * <p>Because the ALTER TABLE omits NOT NULL, a NULL value can be successfully |
| 127 | + * inserted into segment_id on an upgraded database. This would never happen |
| 128 | + * on a fresh install where NOT NULL is enforced. |
| 129 | + */ |
| 130 | + @Test |
| 131 | + public void bug_B10_segmentId_allowsNullInsert_afterUpgrade() { |
| 132 | + SQLiteDatabase rawDb = SQLiteDatabase.create(null); |
| 133 | + try { |
| 134 | + rawDb.execSQL("create table trackpoint (" |
| 135 | + + "_id integer primary key autoincrement," |
| 136 | + + "track_id integer not null," |
| 137 | + + "latitude double not null," |
| 138 | + + "longitude double not null," |
| 139 | + + "speed double null," |
| 140 | + + "elevation double null," |
| 141 | + + "accuracy double null," |
| 142 | + + "point_timestamp long not null," |
| 143 | + + "compass_heading double null," |
| 144 | + + "compass_accuracy integer null," |
| 145 | + + "atmospheric_pressure double null)"); |
| 146 | + |
| 147 | + dbHelper.onUpgrade(rawDb, 18, 19); |
| 148 | + |
| 149 | + // Insert with explicit NULL for segment_id |
| 150 | + ContentValues values = new ContentValues(); |
| 151 | + values.put(TrackContentProvider.Schema.COL_TRACK_ID, 1); |
| 152 | + values.put(TrackContentProvider.Schema.COL_LATITUDE, 48.0); |
| 153 | + values.put(TrackContentProvider.Schema.COL_LONGITUDE, 2.0); |
| 154 | + values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis()); |
| 155 | + values.putNull(TrackContentProvider.Schema.COL_SEG_ID); |
| 156 | + |
| 157 | + long rowId = rawDb.insert(TrackContentProvider.Schema.TBL_TRACKPOINT, null, values); |
| 158 | + assertTrue("Bug B10: NULL insert into segment_id should succeed on upgraded DB", |
| 159 | + rowId > 0); |
| 160 | + |
| 161 | + // Verify the value is actually NULL |
| 162 | + Cursor c = rawDb.query(TrackContentProvider.Schema.TBL_TRACKPOINT, |
| 163 | + new String[]{TrackContentProvider.Schema.COL_SEG_ID}, |
| 164 | + "_id = ?", new String[]{String.valueOf(rowId)}, |
| 165 | + null, null, null); |
| 166 | + try { |
| 167 | + assertTrue(c.moveToFirst()); |
| 168 | + assertTrue("Bug B10: segment_id should be NULL", c.isNull(0)); |
| 169 | + } finally { |
| 170 | + c.close(); |
| 171 | + } |
| 172 | + } finally { |
| 173 | + rawDb.close(); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * Bug B10 baseline — on a fresh install, segment_id enforces NOT NULL |
| 179 | + * (or at least defaults to 0 when NULL is attempted). |
| 180 | + * |
| 181 | + * <p>This establishes the correct baseline against which the upgrade path diverges. |
| 182 | + * On fresh installs, SQLite's NOT NULL with DEFAULT 0 causes a NULL insert to |
| 183 | + * use the default value of 0 instead. |
| 184 | + */ |
| 185 | + @Test |
| 186 | + public void bug_B10_segmentId_freshInstall_defaultsToZeroWhenNullInserted() { |
| 187 | + SQLiteDatabase db = dbHelper.getWritableDatabase(); |
| 188 | + |
| 189 | + ContentValues values = new ContentValues(); |
| 190 | + values.put(TrackContentProvider.Schema.COL_TRACK_ID, 1); |
| 191 | + values.put(TrackContentProvider.Schema.COL_LATITUDE, 48.0); |
| 192 | + values.put(TrackContentProvider.Schema.COL_LONGITUDE, 2.0); |
| 193 | + values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis()); |
| 194 | + // Don't set segment_id — should default to 0 |
| 195 | + |
| 196 | + long rowId = db.insert(TrackContentProvider.Schema.TBL_TRACKPOINT, null, values); |
| 197 | + assertTrue("insert should succeed on fresh DB", rowId > 0); |
| 198 | + |
| 199 | + Cursor c = db.query(TrackContentProvider.Schema.TBL_TRACKPOINT, |
| 200 | + new String[]{TrackContentProvider.Schema.COL_SEG_ID}, |
| 201 | + "_id = ?", new String[]{String.valueOf(rowId)}, |
| 202 | + null, null, null); |
| 203 | + try { |
| 204 | + assertTrue(c.moveToFirst()); |
| 205 | + assertEquals("segment_id should default to 0 on fresh install", |
| 206 | + 0, c.getInt(0)); |
| 207 | + } finally { |
| 208 | + c.close(); |
| 209 | + } |
| 210 | + } |
| 211 | +} |
0 commit comments