package be.simplenotes.persistance.notes import be.simplenotes.domain.model.* import be.simplenotes.domain.usecases.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.UserRepository import be.simplenotes.persistance.DbMigrations import be.simplenotes.persistance.persistanceModule import be.simplenotes.shared.config.DataSourceConfig import me.liuwj.ktorm.database.* import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.entity.* import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.flywaydb.core.Flyway import org.junit.jupiter.api.* import org.junit.jupiter.api.parallel.ResourceLock import org.koin.dsl.koinApplication import org.koin.dsl.module import java.sql.SQLIntegrityConstraintViolationException import java.util.* import javax.sql.DataSource @ResourceLock("h2") internal class NoteRepositoryImplTest { private val testModule = module { single { dataSourceConfig() } } private val koinApp = koinApplication { modules(persistanceModule, testModule) } private fun dataSourceConfig() = DataSourceConfig( jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", driverClassName = "org.h2.Driver", username = "h2", password = "", maximumPoolSize = 2, connectionTimeout = 3000 ) private val koin = koinApp.koin @AfterAll fun afterAll() = koinApp.close() private val migration = koin.get() private val dataSource = koin.get() private val noteRepo = koin.get() private val userRepo = koin.get() private val db = koin.get() private lateinit var user1: PersistedUser private lateinit var user2: PersistedUser @BeforeEach fun beforeEach() { Flyway.configure() .dataSource(dataSource) .load() .clean() migration.migrate() user1 = userRepo.create(User("1", "1"))!! user2 = userRepo.create(User("2", "2"))!! } private fun createNote( userId: Int, title: String, tags: List = emptyList(), md: String = "md", html: String = "html", ): PersistedNote = noteRepo.create(userId, Note(NoteMetadata(title, tags), md, html)) private fun PersistedNote.toPersistedMeta() = PersistedNoteMetadata(meta.title, meta.tags, updatedAt, uuid) @Nested @DisplayName("create()") inner class Create { @Test fun `create note for non existing user`() { val note = Note(NoteMetadata("title", emptyList()), "md", "html") assertThatThrownBy { noteRepo.create(1000, note) }.isInstanceOf(SQLIntegrityConstraintViolationException::class.java) } @Test fun `create note for existing user`() { val note = Note(NoteMetadata("title", emptyList()), "md", "html") assertThat(noteRepo.create(user1.id, note)) .isEqualToIgnoringGivenFields(note, "uuid", "updatedAt") .hasNoNullFieldsOrProperties() assertThat(db.notes.toList()) .hasSize(1) .first() .isEqualToIgnoringGivenFields(note, "uuid", "updatedAt") } } @Nested @DisplayName("findAll()") inner class FindAll { @Test fun `find all notes`() { val notes1 = listOf( createNote(user1.id, "1", listOf("a", "b")), createNote(user1.id, "2"), createNote(user1.id, "3", listOf("c")) ) val notes2 = listOf( createNote(user2.id, "4") ) assertThat(noteRepo.findAll(user1.id)) .hasSize(3) .usingElementComparatorIgnoringFields("updatedAt") .containsExactlyInAnyOrderElementsOf( notes1.map { it.toPersistedMeta() } ) assertThat(noteRepo.findAll(user2.id)) .hasSize(1) .usingElementComparatorIgnoringFields("updatedAt") .containsExactlyInAnyOrderElementsOf( notes2.map { it.toPersistedMeta() } ) assertThat(noteRepo.findAll(1000)).isEmpty() } @Test fun pagination() { (50 downTo 1).forEach { createNote(user1.id, "$it") } assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 0)) .hasSize(20) .allMatch { it.title.toInt() in 1..20 } assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 20)) .hasSize(20) .allMatch { it.title.toInt() in 21..40 } assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 40)) .hasSize(10) .allMatch { it.title.toInt() in 41..50 } } @Test fun `find all notes with tag`() { createNote(user1.id, "1", listOf("a", "b")) createNote(user1.id, "2") createNote(user1.id, "3", listOf("c")) createNote(user1.id, "4", listOf("c")) createNote(user2.id, "5", listOf("c")) assertThat(noteRepo.findAll(user1.id, tag = "a")) .hasSize(1) .first() .hasFieldOrPropertyWithValue("title", "1") assertThat(noteRepo.findAll(user1.id, tag = "c")) .hasSize(2) assertThat(noteRepo.findAll(user2.id, tag = "c")) .hasSize(1) } } @Nested @DisplayName("find() | exists()") inner class FindExists { @Test @Suppress("UNCHECKED_CAST") fun `find an existing note`() { createNote(user1.id, "1", listOf("a", "b")) val note = db.notes.find { it.title eq "1" }!! .let { entity -> val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List entity.toPersistedNote(tags) } assertThat(noteRepo.find(user1.id, note.uuid)) .isEqualTo(note) assertThat(noteRepo.exists(user1.id, note.uuid)) .isTrue } @Test fun `find an existing note from the wrong user`() { val note = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.find(user2.id, note.uuid)).isNull() assertThat(noteRepo.exists(user2.id, note.uuid)).isFalse } @Test fun `find a non existing note`() { createNote(user1.id, "1", listOf("a", "b")) val uuid = UUID.randomUUID() assertThat(noteRepo.find(user1.id, uuid)).isNull() assertThat(noteRepo.exists(user2.id, uuid)).isFalse } } @Nested @DisplayName("delete()") inner class Delete { @Test fun `delete an existing note for a user should succeed and then fail`() { val note = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.delete(user1.id, note.uuid)) .isTrue } @Test fun `delete an existing note for the wrong user`() { val note = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.delete(1000, note.uuid)) .isFalse } } @Nested @DisplayName("getTags()") inner class Tags { @Test fun getTags() { val notes1 = listOf( createNote(user1.id, "1", listOf("a", "b")), createNote(user1.id, "2"), createNote(user1.id, "3", listOf("c", "a")) ) val notes2 = listOf( createNote(user2.id, "4", listOf("a")) ) val user1Tags = notes1.flatMap { it.meta.tags }.toSet() assertThat(noteRepo.getTags(user1.id)) .containsExactlyInAnyOrderElementsOf(user1Tags) val user2Tags = notes2.flatMap { it.meta.tags }.toSet() assertThat(noteRepo.getTags(user2.id)) .containsExactlyInAnyOrderElementsOf(user2Tags) assertThat(noteRepo.getTags(1000)) .isEmpty() } } @Nested @DisplayName("update()") inner class Update { @Test fun getTags() { val note1 = createNote(user1.id, "1", listOf("a", "b")) val newNote1 = Note(meta = note1.meta, markdown = "new", "new") assertThat(noteRepo.update(user1.id, note1.uuid, newNote1)) .isNotNull assertThat(noteRepo.find(user1.id, note1.uuid)) .isEqualToComparingOnlyGivenFields(newNote1, "meta", "markdown", "html") val note2 = createNote(user1.id, "2") val newNote2 = Note(meta = note1.meta.copy(tags = listOf("a")), markdown = "new", "new") assertThat(noteRepo.update(user1.id, note2.uuid, newNote2)) .isNotNull assertThat(noteRepo.find(user1.id, note2.uuid)) .isEqualToComparingOnlyGivenFields(newNote2, "meta", "markdown", "html") } } @Nested inner class Trash { @Test fun `trashed noted should be restored`() { val note1 = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = false)) .isTrue val isDeleted = db.notes .find { it.uuid eq note1.uuid } ?.deleted assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue val isDeleted2 = db.notes .find { it.uuid eq note1.uuid } ?.deleted assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse } @Test fun `permanent delete`() { val note1 = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = true)) .isTrue assertThat(noteRepo.restore(user1.id, note1.uuid)).isFalse } } }