DatabaseBackendTest.java

  1package eu.siacs.conversations.persistance;
  2
  3import android.content.ContentValues;
  4import android.database.sqlite.SQLiteBlobTooBigException;
  5import android.database.sqlite.SQLiteDatabase;
  6import android.util.Log;
  7import androidx.test.core.app.ApplicationProvider;
  8import androidx.test.ext.junit.runners.AndroidJUnit4;
  9
 10import org.json.JSONException;
 11import org.json.JSONObject;
 12import org.junit.After;
 13import org.junit.Assert;
 14import org.junit.Before;
 15import org.junit.BeforeClass;
 16import org.junit.Test;
 17import org.junit.runner.RunWith;
 18
 19import java.util.HashMap;
 20import java.util.UUID;
 21
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.entities.Conversation;
 24import eu.siacs.conversations.entities.MucOptions;
 25import eu.siacs.conversations.xml.Namespace;
 26import im.conversations.android.xmpp.model.disco.info.Feature;
 27import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 28
 29@RunWith(AndroidJUnit4.class)
 30public class DatabaseBackendTest {
 31    private record AccountFixture(String uuid, String username, String server) {
 32        void write(DatabaseBackend db) {
 33            final var cv = new ContentValues();
 34            cv.put("uuid", uuid);
 35            cv.put("username", username);
 36            cv.put("server", server);
 37            cv.put("password", "test");
 38            cv.put("options", 0);
 39            db.getWritableDatabase().insertWithOnConflict(
 40                "accounts", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
 41        }
 42    }
 43
 44    private record ConversationFixture(
 45        String conversationUuid,
 46        AccountFixture account,
 47        String name,
 48        String contactJid,
 49        String attributes,
 50        HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry> occupantCache
 51    ) {
 52        void writeConversation(DatabaseBackend db) {
 53            final var cv = new ContentValues();
 54            cv.put("uuid", conversationUuid);
 55            cv.put("name", name);
 56            cv.put("contactUuid", "");
 57            cv.put("accountUuid", account.uuid());
 58            cv.put("contactJid", contactJid);
 59            cv.put("created", System.currentTimeMillis());
 60            cv.put("status", Conversation.STATUS_AVAILABLE);
 61            cv.put("mode", Conversation.MODE_MULTI);
 62            cv.put("attributes", attributes);
 63            db.getWritableDatabase().insert("conversations", null, cv);
 64        }
 65
 66        void writeOccupants(DatabaseBackend db) {
 67            for (final var entry : occupantCache.entrySet()) {
 68                final var cv = new ContentValues();
 69                cv.put(MucOptions.User.CacheEntry.OCCUPANT_ID, entry.getKey().inner());
 70                cv.put(MucOptions.User.CacheEntry.CONVERSATION_UUID, conversationUuid);
 71                cv.put(MucOptions.User.CacheEntry.AVATAR, entry.getValue().avatar());
 72                cv.put(MucOptions.User.CacheEntry.NICK, entry.getValue().nick());
 73                db.getWritableDatabase().insert(
 74                    MucOptions.User.CacheEntry.TABLENAME, null, cv);
 75            }
 76        }
 77
 78        void writeAll(DatabaseBackend db) {
 79            writeConversation(db);
 80            writeOccupants(db);
 81        }
 82
 83        Conversation extractAndConfigure(DatabaseBackend db)
 84        {
 85            final var conversations = db.getConversations(Conversation.STATUS_AVAILABLE);
 86            Assert.assertNotNull("getConversations should not return null", conversations);
 87
 88            Conversation match = null;
 89            for (final var c : conversations) {
 90                if (conversationUuid.equals(c.getUuid())) {
 91                    match = c;
 92                    break;
 93                }
 94            }
 95            Assert.assertNotNull(
 96                "Fixture conversation " + conversationUuid + " not found", match);
 97
 98            match.setAccount(db.getAccounts().get(0));
 99            match.getMucOptions().updateConfiguration(INFO_QUERY_WITH_OCCUPANT_ID);
100            match.putAllInMucOccupantCache(db.getMucUsersForConversation(match));
101            return match;
102        }
103    }
104
105    private DatabaseBackend db;
106    private static final InfoQuery INFO_QUERY_WITH_OCCUPANT_ID = new InfoQuery();
107    private static final int MANY_USERS = 20_000;
108
109    private static AccountFixture ACCOUNT;
110    private static ConversationFixture ROW_TOO_BIG;
111    private static ConversationFixture CONFORMING;
112    private static ConversationFixture NO_CACHED_MUC_USERS;
113    private static ConversationFixture[] FIXTURES;
114
115    @BeforeClass
116    public static void setupClass() throws JSONException {
117        final var occupantIdFeature = new Feature();
118        occupantIdFeature.setVar(Namespace.OCCUPANT_ID);
119        INFO_QUERY_WITH_OCCUPANT_ID.addChild(occupantIdFeature);
120
121        ACCOUNT = new AccountFixture(
122            UUID.randomUUID().toString(), "test", "example.com");
123
124        final var rowTooBigAttrs = new JSONObject();
125        for (int i = 0; i < MANY_USERS; i++) {
126            final var occupantId = UUID.randomUUID().toString();
127            rowTooBigAttrs.put("occupantNick/" + occupantId, "User" + i);
128            rowTooBigAttrs.put("occupantAvatar/" + occupantId,
129                UUID.randomUUID().toString().repeat(5));
130        }
131        rowTooBigAttrs.put("mucNick", "testMucNick");
132
133        final var rowTooBigCache =
134            new HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry>();
135        rowTooBigCache.put(
136            new MucOptions.User.OccupantId(UUID.randomUUID().toString()),
137            new MucOptions.User.CacheEntry(UUID.randomUUID().toString(), "RowTooBigUser"));
138
139        ROW_TOO_BIG = new ConversationFixture(
140            UUID.randomUUID().toString(),
141            ACCOUNT,
142            "Big MUC",
143            "room@conference.example.com",
144            rowTooBigAttrs.toString(),
145            rowTooBigCache
146        );
147
148        final var conformingCache =
149            new HashMap<MucOptions.User.OccupantId, MucOptions.User.CacheEntry>();
150        conformingCache.put(
151            new MucOptions.User.OccupantId(UUID.randomUUID().toString()),
152            new MucOptions.User.CacheEntry(UUID.randomUUID().toString(), "ConformingUser"));
153
154        CONFORMING = new ConversationFixture(
155            UUID.randomUUID().toString(),
156            ACCOUNT,
157            "Normal MUC",
158            "normalroom@conference.example.com",
159            new JSONObject().put("mucNick", "testMucNick").toString(),
160            conformingCache
161        );
162
163        NO_CACHED_MUC_USERS = new ConversationFixture(
164            UUID.randomUUID().toString(),
165            ACCOUNT,
166            "Empty Cache MUC",
167            "emptycache@conference.example.com",
168            new JSONObject().put("mucNick", "testMucNick").toString(),
169            new HashMap<>()
170        );
171
172        FIXTURES = new ConversationFixture[] {
173            ROW_TOO_BIG, CONFORMING, NO_CACHED_MUC_USERS };
174    }
175
176    @Before
177    public void setUp() throws Exception {
178        db = DatabaseBackend.getInstance(
179                ApplicationProvider.getApplicationContext());
180        ACCOUNT.write(db);
181        for (final var fixture : FIXTURES) {
182            fixture.writeAll(db);
183        }
184    }
185
186    @After
187    public void tearDown() {
188        SQLiteDatabase sqDb = db.getWritableDatabase();
189        sqDb.delete(Conversation.TABLENAME, null, null);
190        sqDb.delete(Account.TABLENAME, null, null);
191        sqDb.delete(MucOptions.User.CacheEntry.TABLENAME, null, null);
192    }
193
194    @Test
195    public void getConversationsCorrectlyReadsMucUsers() throws Exception {
196        Assert.assertTrue(
197            "Occupant cache should be empty when no occupants are written",
198            NO_CACHED_MUC_USERS
199                .extractAndConfigure(db)
200                    .getMucOccupantCache()
201                    .isEmpty()
202        );
203
204        Assert.assertEquals(
205            "Cached entries should match fixture",
206            CONFORMING
207                .extractAndConfigure(db)
208                .getMucOccupantCache(),
209            CONFORMING.occupantCache()
210        );
211    }
212
213    @Test
214    public void getConversationsTruncatesTooBigRow() throws Exception {
215        final var conversation = ROW_TOO_BIG.extractAndConfigure(db);
216        java.lang.reflect.Field attributesField =
217            conversation.getClass().getDeclaredField("attributes");
218
219        attributesField.setAccessible(true);
220        org.json.JSONObject attributes =
221            (org.json.JSONObject) attributesField.get(conversation);
222
223        final var expected = new JSONObject();
224        expected.put("members_only", "false");
225        expected.put("moderated", "false");
226        expected.put("non_anonymous", "false");
227
228        Assert.assertEquals(
229            "Attributes should not contain occupant cache after truncation:\n" + attributes.toString(4),
230            expected.toString(),
231            attributes.toString()
232        );
233    }
234
235    @Test
236    public void updateConversationWritesMucOccupantsCache() throws Exception {
237        final var conversation = NO_CACHED_MUC_USERS.extractAndConfigure(db);
238        conversation.putAllInMucOccupantCache(CONFORMING.occupantCache());
239        db.updateConversation(conversation);
240
241        final var readBackCache = db.getMucUsersForConversation(conversation);
242        Assert.assertEquals(
243            "Cache should match after updateConversation",
244            CONFORMING.occupantCache(),
245            readBackCache
246        );
247    }
248}