Skip to content

Commit 035b1eb

Browse files
authored
Merge pull request #7594 from vector-im/feature/bca/better_edit_validation
Better edit (replace handling)
2 parents ae996ae + bec8b5f commit 035b1eb

File tree

43 files changed

+1533
-228
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1533
-228
lines changed

changelog.d/7594.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Better validation of edits
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.database
18+
19+
import android.content.Context
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import androidx.test.platform.app.InstrumentationRegistry
22+
import io.realm.Realm
23+
import org.amshove.kluent.fail
24+
import org.amshove.kluent.shouldBe
25+
import org.amshove.kluent.shouldBeEqualTo
26+
import org.amshove.kluent.shouldNotBe
27+
import org.junit.After
28+
import org.junit.Before
29+
import org.junit.Rule
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
import org.matrix.android.sdk.api.session.events.model.toModel
33+
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
34+
import org.matrix.android.sdk.internal.database.mapper.EventMapper
35+
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
36+
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
37+
import org.matrix.android.sdk.internal.database.query.where
38+
import org.matrix.android.sdk.internal.util.Normalizer
39+
40+
@RunWith(AndroidJUnit4::class)
41+
class RealmSessionStoreMigration43Test {
42+
43+
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
44+
45+
lateinit var context: Context
46+
var realm: Realm? = null
47+
48+
@Before
49+
fun setUp() {
50+
context = InstrumentationRegistry.getInstrumentation().context
51+
}
52+
53+
@After
54+
fun tearDown() {
55+
realm?.close()
56+
}
57+
58+
@Test
59+
fun migrationShouldBeNeeed() {
60+
val realmName = "session_42.realm"
61+
val realmConfiguration = configurationFactory.createConfiguration(
62+
realmName,
63+
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
64+
SessionRealmModule(),
65+
43,
66+
null
67+
)
68+
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
69+
70+
try {
71+
realm = Realm.getInstance(realmConfiguration)
72+
fail("Should need a migration")
73+
} catch (failure: Throwable) {
74+
// nop
75+
}
76+
}
77+
78+
// Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0
79+
// $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI
80+
// $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo
81+
@Test
82+
fun testMigration43() {
83+
val realmName = "session_42.realm"
84+
val migration = RealmSessionStoreMigration(Normalizer())
85+
val realmConfiguration = configurationFactory.createConfiguration(
86+
realmName,
87+
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
88+
SessionRealmModule(),
89+
43,
90+
migration
91+
)
92+
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
93+
94+
realm = Realm.getInstance(realmConfiguration)
95+
96+
// assert that the edit from 42 are migrated
97+
val editions = EventAnnotationsSummaryEntity
98+
.where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI")
99+
.findFirst()
100+
?.editSummary
101+
?.editions
102+
103+
editions shouldNotBe null
104+
editions!!.size shouldBe 1
105+
val firstEdition = editions.first()
106+
firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28"
107+
firstEdition?.isLocalEcho shouldBeEqualTo false
108+
109+
val editEvent = EventMapper.map(firstEdition!!.event!!)
110+
val body = editEvent.content.toModel<MessageContent>()?.body
111+
body shouldBeEqualTo "* Message 2 with edit"
112+
113+
// assert that the edit from 42 are migrated
114+
val editionsOfE2E = EventAnnotationsSummaryEntity
115+
.where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo")
116+
.findFirst()
117+
?.editSummary
118+
?.editions
119+
120+
editionsOfE2E shouldNotBe null
121+
editionsOfE2E!!.size shouldBe 1
122+
val firstEditionE2E = editionsOfE2E.first()
123+
firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts"
124+
firstEditionE2E?.isLocalEcho shouldBeEqualTo false
125+
126+
val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!)
127+
val body2 = editEventE2E.getClearContent().toModel<MessageContent>()?.body
128+
body2 shouldBeEqualTo "* Message 2, e2e edit"
129+
}
130+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2020 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.database
18+
19+
import android.content.Context
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import androidx.test.platform.app.InstrumentationRegistry
22+
import io.realm.Realm
23+
import org.junit.After
24+
import org.junit.Before
25+
import org.junit.Rule
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
29+
import org.matrix.android.sdk.internal.util.Normalizer
30+
31+
@RunWith(AndroidJUnit4::class)
32+
class SessionSanityMigrationTest {
33+
34+
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
35+
36+
lateinit var context: Context
37+
var realm: Realm? = null
38+
39+
@Before
40+
fun setUp() {
41+
context = InstrumentationRegistry.getInstrumentation().context
42+
}
43+
44+
@After
45+
fun tearDown() {
46+
realm?.close()
47+
}
48+
49+
@Test
50+
fun sessionDatabaseShouldMigrateGracefully() {
51+
val realmName = "session_42.realm"
52+
val migration = RealmSessionStoreMigration(Normalizer())
53+
val realmConfiguration = configurationFactory.createConfiguration(
54+
realmName,
55+
"efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
56+
SessionRealmModule(),
57+
migration.schemaVersion,
58+
migration
59+
)
60+
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
61+
62+
realm = Realm.getInstance(realmConfiguration)
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.matrix.android.sdk.internal.database
17+
18+
import android.content.Context
19+
import androidx.test.platform.app.InstrumentationRegistry
20+
import io.realm.Realm
21+
import io.realm.RealmConfiguration
22+
import io.realm.RealmMigration
23+
import org.junit.rules.TemporaryFolder
24+
import org.junit.runner.Description
25+
import org.junit.runners.model.Statement
26+
import java.io.File
27+
import java.io.FileOutputStream
28+
import java.io.IOException
29+
import java.io.InputStream
30+
import java.lang.IllegalStateException
31+
import java.util.Collections
32+
import java.util.Locale
33+
import java.util.concurrent.ConcurrentHashMap
34+
import kotlin.Throws
35+
36+
/**
37+
* Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java
38+
*/
39+
class TestRealmConfigurationFactory : TemporaryFolder() {
40+
private val map: Map<RealmConfiguration, Boolean> = ConcurrentHashMap()
41+
private val configurations = Collections.newSetFromMap(map)
42+
@get:Synchronized private var isUnitTestFailed = false
43+
private var testName = ""
44+
private var tempFolder: File? = null
45+
46+
override fun apply(base: Statement, description: Description): Statement {
47+
return object : Statement() {
48+
@Throws(Throwable::class)
49+
override fun evaluate() {
50+
setTestName(description)
51+
before()
52+
try {
53+
base.evaluate()
54+
} catch (throwable: Throwable) {
55+
setUnitTestFailed()
56+
throw throwable
57+
} finally {
58+
after()
59+
}
60+
}
61+
}
62+
}
63+
64+
@Throws(Throwable::class)
65+
override fun before() {
66+
Realm.init(InstrumentationRegistry.getInstrumentation().targetContext)
67+
super.before()
68+
}
69+
70+
override fun after() {
71+
try {
72+
for (configuration in configurations) {
73+
Realm.deleteRealm(configuration)
74+
}
75+
} catch (e: IllegalStateException) {
76+
// Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw.
77+
if (!isUnitTestFailed) {
78+
throw e
79+
}
80+
} finally {
81+
// This will delete the temp directory.
82+
super.after()
83+
}
84+
}
85+
86+
@Throws(IOException::class)
87+
override fun create() {
88+
super.create()
89+
tempFolder = File(super.getRoot(), testName)
90+
check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath }
91+
check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath }
92+
}
93+
94+
override fun getRoot(): File {
95+
checkNotNull(tempFolder) { "the temporary folder has not yet been created" }
96+
return tempFolder!!
97+
}
98+
99+
/**
100+
* To be called in the [.apply].
101+
*/
102+
protected fun setTestName(description: Description) {
103+
testName = description.displayName
104+
}
105+
106+
@Synchronized
107+
fun setUnitTestFailed() {
108+
isUnitTestFailed = true
109+
}
110+
111+
// This builder creates a configuration that is *NOT* managed.
112+
// You have to delete it yourself.
113+
private fun createConfigurationBuilder(): RealmConfiguration.Builder {
114+
return RealmConfiguration.Builder().directory(root)
115+
}
116+
117+
fun String.decodeHex(): ByteArray {
118+
check(length % 2 == 0) { "Must have an even length" }
119+
return chunked(2)
120+
.map { it.toInt(16).toByte() }
121+
.toByteArray()
122+
}
123+
124+
fun createConfiguration(
125+
name: String,
126+
key: String?,
127+
module: Any,
128+
schemaVersion: Long,
129+
migration: RealmMigration?
130+
): RealmConfiguration {
131+
val builder = createConfigurationBuilder()
132+
builder
133+
.directory(root)
134+
.name(name)
135+
.apply {
136+
if (key != null) {
137+
encryptionKey(key.decodeHex())
138+
}
139+
}
140+
.modules(module)
141+
// Allow writes on UI
142+
.allowWritesOnUiThread(true)
143+
.schemaVersion(schemaVersion)
144+
.apply {
145+
migration?.let { migration(it) }
146+
}
147+
val configuration = builder.build()
148+
configurations.add(configuration)
149+
return configuration
150+
}
151+
152+
// Copies a Realm file from assets to temp dir
153+
@Throws(IOException::class)
154+
fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) {
155+
val config = RealmConfiguration.Builder()
156+
.directory(root)
157+
.name(newName)
158+
.build()
159+
copyRealmFromAssets(context, realmPath, config)
160+
}
161+
162+
@Throws(IOException::class)
163+
fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) {
164+
check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) }
165+
val outFile = File(config.realmDirectory, config.realmFileName)
166+
copyFileFromAssets(context, realmPath, outFile)
167+
}
168+
169+
@Throws(IOException::class)
170+
fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) {
171+
var stream: InputStream? = null
172+
var os: FileOutputStream? = null
173+
try {
174+
stream = context.assets.open(assetPath!!)
175+
os = FileOutputStream(outFile)
176+
val buf = ByteArray(1024)
177+
var bytesRead: Int
178+
while (stream.read(buf).also { bytesRead = it } > -1) {
179+
os.write(buf, 0, bytesRead)
180+
}
181+
} finally {
182+
if (stream != null) {
183+
try {
184+
stream.close()
185+
} catch (ignore: IOException) {
186+
}
187+
}
188+
if (os != null) {
189+
try {
190+
os.close()
191+
} catch (ignore: IOException) {
192+
}
193+
}
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)