2
0
Karlatemp 3 жил өмнө
parent
commit
1261ebfd89
30 өөрчлөгдсөн 1157 нэмэгдсэн , 558 устгасан
  1. 3 0
      mirai-core-mock/build.gradle.kts
  2. 3 3
      mirai-core-mock/src/MockBot.kt
  3. 1 1
      mirai-core-mock/src/MockBotFactory.kt
  4. 1 1
      mirai-core-mock/src/contact/MockContact.kt
  5. 1 1
      mirai-core-mock/src/contact/MockFriend.kt
  6. 1 1
      mirai-core-mock/src/contact/MockGroup.kt
  7. 1 1
      mirai-core-mock/src/contact/MockMember.kt
  8. 1 1
      mirai-core-mock/src/contact/MockNormalMember.kt
  9. 1 1
      mirai-core-mock/src/contact/MockStranger.kt
  10. 1 1
      mirai-core-mock/src/contact/MockUser.kt
  11. 1 1
      mirai-core-mock/src/contact/MockUserOrBot.kt
  12. 1 1
      mirai-core-mock/src/internal/MockBotFactoryImpl.kt
  13. 1 1
      mirai-core-mock/src/internal/MockBotImpl.kt
  14. 1 1
      mirai-core-mock/src/internal/contact/AbstractMockContact.kt
  15. 2 2
      mirai-core-mock/src/internal/contact/MockGroupImpl.kt
  16. 21 7
      mirai-core-mock/src/internal/remotefile/FsServerImpl.kt
  17. 331 0
      mirai-core-mock/src/internal/remotefile/v1/MockRemoteFile.kt
  18. 124 0
      mirai-core-mock/src/internal/remotefile/v2/MockRemoteFiles.kt
  19. 13 0
      mirai-core-mock/src/internal/txfs/TxFileDiskImpl.kt
  20. 7 3
      mirai-core-mock/src/txfs/TmpFsServer.kt
  21. 4 0
      mirai-core-mock/src/txfs/TxRemoteFile.kt
  22. 1 1
      mirai-core-mock/src/userprofile/UserProfileService.kt
  23. 1 1
      mirai-core-mock/src/utils/mockdsl.kt
  24. 9 1
      mirai-core-mock/test/FsServerTest.kt
  25. 0 528
      mirai-core-mock/test/MockBotTest.kt
  26. 88 0
      mirai-core-mock/test/MockBotTestBase.kt
  27. 212 0
      mirai-core-mock/test/mock/MessagingTest.kt
  28. 117 0
      mirai-core-mock/test/mock/MockBotBaseTest.kt
  29. 62 0
      mirai-core-mock/test/mock/MockFriendTest.kt
  30. 147 0
      mirai-core-mock/test/mock/MockGroupTest.kt

+ 3 - 0
mirai-core-mock/build.gradle.kts

@@ -21,6 +21,9 @@ plugins {
 version = Versions.project
 description = "Mirai core mock testing framework"
 
+kotlin {
+    explicitApiWarning()
+}
 
 dependencies {
     api(project(":mirai-core"))

+ 3 - 3
mirai-core-mock/src/MockBot.kt

@@ -11,7 +11,7 @@
 
 package net.mamoe.mirai.mock
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.ContactList
 import net.mamoe.mirai.event.broadcast
@@ -21,7 +21,7 @@ import net.mamoe.mirai.event.events.NewFriendRequestEvent
 import net.mamoe.mirai.message.data.OnlineAudio
 import net.mamoe.mirai.mock.contact.*
 import net.mamoe.mirai.mock.database.MessageDatabase
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TmpFsServer
 import net.mamoe.mirai.mock.userprofile.UserProfileService
 import net.mamoe.mirai.mock.utils.NameGenerator
 import net.mamoe.mirai.utils.ExternalResource
@@ -43,7 +43,7 @@ public interface MockBot : Bot, MockContactOrBot, MockUserOrBot {
      * 修改此字段时不会广播事件
      */
     @MockBotDSL
-    var nickNoEvent: String
+    public var nickNoEvent: String
 
     /**
      * bot 昵称

+ 1 - 1
mirai-core-mock/src/MockBotFactory.kt

@@ -12,7 +12,7 @@ package net.mamoe.mirai.mock
 import net.mamoe.mirai.BotFactory
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.mock.database.MessageDatabase
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TmpFsServer
 import net.mamoe.mirai.mock.internal.MockBotFactoryImpl
 import net.mamoe.mirai.mock.internal.MockMiraiImpl
 import net.mamoe.mirai.mock.userprofile.UserProfileService

+ 1 - 1
mirai-core-mock/src/contact/MockContact.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Contact
 
 @JvmBlockingBridge

+ 1 - 1
mirai-core-mock/src/contact/MockFriend.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Friend
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent

+ 1 - 1
mirai-core-mock/src/contact/MockGroup.kt

@@ -11,7 +11,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.ContactList
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.NormalMember

+ 1 - 1
mirai-core-mock/src/contact/MockMember.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.mock.MockBotDSL

+ 1 - 1
mirai-core-mock/src/contact/MockNormalMember.kt

@@ -10,7 +10,7 @@
 package net.mamoe.mirai.mock.contact
 
 import kotlinx.coroutines.cancel
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.NormalMember
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.BotLeaveEvent

+ 1 - 1
mirai-core-mock/src/contact/MockStranger.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Stranger
 import net.mamoe.mirai.mock.MockBotDSL
 

+ 1 - 1
mirai-core-mock/src/contact/MockUser.kt

@@ -11,7 +11,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.User
 import net.mamoe.mirai.event.events.GroupMessageEvent
 import net.mamoe.mirai.message.MessageReceipt

+ 1 - 1
mirai-core-mock/src/contact/MockUserOrBot.kt

@@ -11,7 +11,7 @@
 
 package net.mamoe.mirai.mock.contact
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.UserOrBot
 import net.mamoe.mirai.event.events.GroupMessageEvent
 import net.mamoe.mirai.message.MessageReceipt

+ 1 - 1
mirai-core-mock/src/internal/MockBotFactoryImpl.kt

@@ -13,7 +13,7 @@ import net.mamoe.mirai.Bot
 import net.mamoe.mirai.mock.MockBot
 import net.mamoe.mirai.mock.MockBotFactory
 import net.mamoe.mirai.mock.database.MessageDatabase
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TmpFsServer
 import net.mamoe.mirai.mock.userprofile.UserProfileService
 import net.mamoe.mirai.mock.utils.NameGenerator
 import net.mamoe.mirai.utils.BotConfiguration

+ 1 - 1
mirai-core-mock/src/internal/MockBotImpl.kt

@@ -32,7 +32,7 @@ import net.mamoe.mirai.mock.contact.MockGroup
 import net.mamoe.mirai.mock.contact.MockOtherClient
 import net.mamoe.mirai.mock.contact.MockStranger
 import net.mamoe.mirai.mock.database.MessageDatabase
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TmpFsServer
 import net.mamoe.mirai.mock.internal.components.MockEventDispatcherImpl
 import net.mamoe.mirai.mock.internal.contact.MockFriendImpl
 import net.mamoe.mirai.mock.internal.contact.MockGroupImpl

+ 1 - 1
mirai-core-mock/src/internal/contact/AbstractMockContact.kt

@@ -43,7 +43,7 @@ internal abstract class AbstractMockContact(
     protected abstract fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing
 
     override suspend fun sendMessage(message: Message): MessageReceipt<Contact> {
-        val msg = broadcastMessagePreSendEvent(message) { _, _ -> newMessagePreSend(message) }
+        val msg = broadcastMessagePreSendEvent(message, false) { _, _ -> newMessagePreSend(message) }
 
         val source = newMessageSource(msg)
         val response = source withMessage msg

+ 2 - 2
mirai-core-mock/src/internal/contact/MockGroupImpl.kt

@@ -29,7 +29,6 @@ import net.mamoe.mirai.mock.contact.MockGroupControlPane
 import net.mamoe.mirai.mock.contact.MockNormalMember
 import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToGroup
 import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
-import net.mamoe.mirai.mock.internal.remotefile.MockRemoteFileRoot
 import net.mamoe.mirai.mock.utils.broadcastBlocking
 import net.mamoe.mirai.mock.utils.mock
 import net.mamoe.mirai.utils.ExternalResource
@@ -306,7 +305,8 @@ internal class MockGroupImpl(
 
     @Suppress("OverridingDeprecatedMember", "DEPRECATION")
     override val filesRoot: RemoteFile by lazy {
-        MockRemoteFileRoot(this)
+        net.mamoe.mirai.mock.internal.remotefile.v1.MockRemoteFileRoot(this)
+        //MockRemoteFileRoot(this)
     }
 
     override val files: RemoteFiles

+ 21 - 7
mirai-core-mock/src/internal/remotefile/FsServerImpl.kt

@@ -14,24 +14,27 @@ import io.ktor.features.*
 import io.ktor.response.*
 import io.ktor.server.engine.*
 import io.ktor.server.netty.*
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.internal.txfs.TxFileDiskImpl
+import net.mamoe.mirai.mock.txfs.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TxFileDisk
 import net.mamoe.mirai.utils.*
 import java.io.IOException
 import java.net.ServerSocket
 import java.net.URI
 import java.nio.file.FileSystem
 import java.nio.file.Files
+import java.nio.file.Path
 import java.util.*
-import kotlin.io.path.createFile
-import kotlin.io.path.exists
-import kotlin.io.path.inputStream
-import kotlin.io.path.outputStream
+import kotlin.io.path.*
 
 internal class FsServerImpl(
     override val fsSystem: FileSystem,
     val httpPort: Int,
 ) : TmpFsServer {
     var logger by lateinitMutableProperty { MiraiLogger.Factory.create(TmpFsServer::class.java, "TmpFsServer") }
+    override val fsDisk: TxFileDisk by lazy {
+        TxFileDiskImpl(fsSystem.getPath("tx-fs-disk"))
+    }
 
     override lateinit var httpRoot: String
     lateinit var server: NettyApplicationEngine
@@ -86,8 +89,12 @@ internal class FsServerImpl(
             module {
                 intercept(ApplicationCallPipeline.Call) {
                     val request = URI.create(call.request.origin.uri).path.removePrefix("/")
-                    val path = fsSystem.getPath(request)
-                    logger.verbose { "New http request: $request" }
+                    val path = if (request.startsWith("abs-access")) {
+                        fsSystem.getPath(request.removePrefix("abs-access"))
+                    } else {
+                        fsSystem.getPath(request)
+                    }
+                    logger.verbose { "New http request: $request -> $path" }
                     if (path.exists()) {
                         call.respondOutputStream {
                             runBIO {
@@ -121,4 +128,11 @@ internal class FsServerImpl(
         }
         fsSystem.close()
     }
+
+    override fun resolveHttpUrl(path: Path): String {
+        require(path.fileSystem === fsSystem) {
+            error("Cross FileSystem access: $path")
+        }
+        return "${httpRoot}abs-access${path.absolutePathString()}"
+    }
 }

+ 331 - 0
mirai-core-mock/src/internal/remotefile/v1/MockRemoteFile.kt

@@ -0,0 +1,331 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+@file:Suppress("DEPRECATION")
+
+package net.mamoe.mirai.mock.internal.remotefile.v1
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.FileSupported
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.isOperator
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.FileMessage
+import net.mamoe.mirai.mock.txfs.TxFileSystem
+import net.mamoe.mirai.mock.txfs.TxRemoteFile
+import net.mamoe.mirai.mock.utils.mock
+import net.mamoe.mirai.utils.*
+import kotlin.io.path.inputStream
+
+internal class MockRemoteFileRoot(
+    override val contact: FileSupported,
+    val fileSystem: TxFileSystem,
+) : RemoteFile {
+    constructor(contact: FileSupported) : this(
+        contact,
+        contact.bot.mock().tmpFsServer.fsDisk.newFsSystem()
+    )
+
+    override val name: String get() = ""
+    override val id: String? get() = null
+    override val path: String get() = RemoteFile.ROOT_PATH
+    override val parent: RemoteFile? get() = null
+
+    override fun toString(): String = path
+
+    override suspend fun isFile(): Boolean = false
+    override suspend fun length(): Long = 0
+    override suspend fun toMessage(): FileMessage? = null
+    override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? = null
+    override suspend fun exists(): Boolean = true
+    override suspend fun isDirectory(): Boolean = true
+    override suspend fun delete(): Boolean = false
+    override suspend fun renameTo(name: String): Boolean = false
+    override suspend fun moveTo(target: RemoteFile): Boolean = false
+    override suspend fun mkdir(): Boolean = false
+
+    override suspend fun getInfo(): RemoteFile.FileInfo {
+        return RemoteFile.FileInfo(
+            name = "",
+            id = "/",
+            path = "/",
+            length = 0,
+            downloadTimes = 0,
+            uploaderId = 0,
+            uploadTime = 0,
+            lastModifyTime = fileSystem.root.fileInfo.lastUpdateTime,
+            sha1 = byteArrayOf(),
+            md5 = byteArrayOf(),
+        )
+    }
+
+
+    override fun resolve(relative: String): RemoteFile {
+        return MockRemoteFileSub(
+            this,
+            relative.substringAfter('/'),
+            null,
+            relative.removePrefix("/"),
+            this
+        )
+    }
+
+    override fun resolve(relative: RemoteFile): RemoteFile {
+        return resolve(relative.path)
+    }
+
+    override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
+        val native = fileSystem.resolveById(id) ?: return null
+        if (!deep && native !== fileSystem.root) return null
+        return toSubRF(native)
+    }
+
+    override fun resolveSibling(relative: String): RemoteFile = error("Root path does not have sibling paths.")
+    override fun resolveSibling(relative: RemoteFile): RemoteFile = error("Root path does not have sibling paths.")
+
+    fun toSubRF(native: TxRemoteFile): MockRemoteFileSub {
+        val pt = native.parent
+        return MockRemoteFileSub(
+            root = this,
+            name = native.name,
+            id = id,
+            path = native.path,
+            parent = when {
+                pt === fileSystem.root -> this
+                else -> toSubRF(pt)
+            }
+        )
+    }
+
+    private fun listFilesSeq(): Sequence<RemoteFile> {
+        return fileSystem.root.listFiles()!!
+            .map { toSubRF(it) }
+    }
+
+    override suspend fun listFiles(): Flow<RemoteFile> {
+        return listFilesSeq().asFlow()
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
+        return listFilesSeq().iterator()
+    }
+
+
+    override suspend fun upload(resource: ExternalResource, callback: RemoteFile.ProgressionCallback?): FileMessage {
+        error("Cannot upload a file as a directory")
+    }
+
+    @MiraiExperimentalApi
+    override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
+        error("Cannot upload a file as a directory")
+    }
+
+}
+
+internal class MockRemoteFileSub(
+    val root: MockRemoteFileRoot,
+    override val name: String,
+    override val id: String?,
+    override val path: String,
+    override val parent: RemoteFile,
+) : RemoteFile {
+    override val contact: FileSupported get() = root.contact
+    private fun resolveNative(): TxRemoteFile? {
+
+        id?.let { root.fileSystem.resolveById(it) }?.let { return it }
+
+        return root.fileSystem.findByPath(path).firstOrNull()
+    }
+
+    override suspend fun isFile(): Boolean {
+        return resolveNative()?.isFile ?: false
+    }
+
+    override suspend fun isDirectory(): Boolean {
+        return resolveNative()?.isDirectory ?: false
+    }
+
+    override suspend fun length(): Long {
+        return resolveNative()?.size ?: 0
+    }
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    override suspend fun getInfo(): RemoteFile.FileInfo? {
+        val native = resolveNative() ?: return null
+        return runBIO {
+            RemoteFile.FileInfo(
+                name = native.name,
+                id = native.id,
+                path = native.path,
+                length = native.size,
+                downloadTimes = 0,
+                uploadTime = native.fileInfo.createTime,
+                uploaderId = native.fileInfo.creator,
+                lastModifyTime = native.fileInfo.lastUpdateTime,
+                sha1 = native.resolveNativePath().inputStream().sha1(),
+                md5 = native.resolveNativePath().inputStream().md5()
+            )
+        }
+    }
+
+    override suspend fun exists(): Boolean {
+        return resolveNative() != null
+    }
+
+    override fun toString(): String = path
+
+    override fun resolve(relative: String): RemoteFile {
+        if (relative.startsWith('/')) {
+            return root.resolve(relative)
+        }
+        return root.resolve("$path/$relative")
+    }
+
+    override fun resolve(relative: RemoteFile): RemoteFile {
+        return resolve(relative.path)
+    }
+
+    override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
+        val result = root.fileSystem.resolveById(id) ?: return null
+        return root.toSubRF(result)
+    }
+
+    override fun resolveSibling(relative: String): RemoteFile {
+        return parent.resolve(relative)
+    }
+
+    override fun resolveSibling(relative: RemoteFile): RemoteFile {
+        return parent.resolve(relative)
+    }
+
+    private fun TxRemoteFile.hasPerm(): Boolean {
+        if (fileInfo.creator == contact.bot.id) return true
+        when (val contact = contact) {
+            is Group -> {
+                if (contact.botPermission.isOperator()) return true
+            }
+        }
+        return false
+    }
+
+    override suspend fun delete(): Boolean {
+        // TODO: Perm check
+        val native = resolveNative() ?: return false
+        if (!native.hasPerm()) return false
+        return native.delete()
+    }
+
+    override suspend fun renameTo(name: String): Boolean {
+        val native = resolveNative() ?: return false
+        if (!native.hasPerm()) return false
+        return native.rename(name)
+    }
+
+    override suspend fun moveTo(target: RemoteFile): Boolean {
+        if (target.contact !== contact) {
+            error("Crossing file moving not support")
+        }
+        if (target === root) return false
+        if (target === this) return true
+        val f = resolveNative() ?: return false
+        if (!f.hasPerm()) return false
+        val t = when (val p = target.parent) {
+            is MockRemoteFileRoot -> root.fileSystem.root
+            else -> p.cast<MockRemoteFileSub>().resolveNative()
+        } ?: return false
+        f.moveTo(t)
+        f.rename(target.name)
+        return true
+    }
+
+    override suspend fun mkdir(): Boolean {
+        if (parent !== root) return false
+        when (val contact = contact) {
+            is Group -> {
+                if (!contact.botPermission.isOperator()) return false
+            }
+        }
+        root.fileSystem.root.mksubdir(name, contact.bot.id)
+        return true
+    }
+
+    private fun listFilesSeq(): Sequence<RemoteFile> {
+        return resolveNative()?.listFiles().orEmpty()
+            .map { root.toSubRF(it) }
+    }
+
+    override suspend fun listFiles(): Flow<RemoteFile> {
+        return listFilesSeq().asFlow()
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
+        return listFilesSeq().iterator()
+    }
+
+    override suspend fun toMessage(): FileMessage? {
+        val nt = resolveNative() ?: return null
+        if (!nt.isFile) return null
+        return FileMessage(
+            id = nt.id,
+            internalId = 0,
+            name = nt.name,
+            size = nt.size,
+        )
+    }
+
+    override suspend fun upload(resource: ExternalResource, callback: RemoteFile.ProgressionCallback?): FileMessage {
+
+        kotlin.run perm@{
+            when (val contact = contact) {
+                is Group -> {
+                    if (contact.botPermission.isOperator()) return@perm
+                    if (contact.mock().controlPane.isAllowMemberFileUploading) return@perm
+
+                    error("Group $contact not allow uploading files.")
+                }
+            }
+        }
+
+        val dir = when {
+            parent === root -> root.fileSystem.root
+            else -> parent.cast<MockRemoteFileSub>().resolveNative() ?: error("Parent $parent not found")
+        }
+        val file = dir.uploadFile(name, resource, contact.bot.id)
+        return FileMessage(
+            id = file.id,
+            internalId = 0,
+            name = file.name,
+            size = file.size,
+        )
+    }
+
+    @MiraiExperimentalApi
+    override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
+        return contact.sendMessage(upload(resource))
+    }
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
+        val native = resolveNative() ?: return null
+        return runBIO {
+            RemoteFile.DownloadInfo(
+                filename = native.name,
+                id = native.id,
+                path = native.path,
+                url = contact.bot.mock().tmpFsServer.resolveHttpUrl(native.resolveNativePath()),
+                sha1 = native.resolveNativePath().inputStream().sha1(),
+                md5 = native.resolveNativePath().inputStream().md5()
+            )
+        }
+    }
+}

+ 124 - 0
mirai-core-mock/src/internal/remotefile/v2/MockRemoteFiles.kt

@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+@file:Suppress("ClassName")
+
+package net.mamoe.mirai.mock.internal.remotefile.v2
+
+import kotlinx.coroutines.flow.Flow
+import net.mamoe.mirai.contact.FileSupported
+import net.mamoe.mirai.contact.file.AbsoluteFile
+import net.mamoe.mirai.contact.file.AbsoluteFileFolder
+import net.mamoe.mirai.contact.file.AbsoluteFolder
+import net.mamoe.mirai.contact.file.RemoteFiles
+import net.mamoe.mirai.mock.txfs.TxFileSystem
+import net.mamoe.mirai.utils.ExternalResource
+import net.mamoe.mirai.utils.JavaFriendlyAPI
+import net.mamoe.mirai.utils.ProgressionCallback
+import java.util.stream.Stream
+
+internal class MockRemoteFiles(
+    override val contact: FileSupported,
+    val fileSystem: TxFileSystem,
+) : RemoteFiles {
+    override val root: AbsoluteFolder = MRF_AbsoluteFolderRoot(this)
+}
+
+internal class MRF_AbsoluteFolderRoot(
+    val files: MockRemoteFiles,
+) : AbsoluteFolder {
+    override val contentsCount: Int get() = 0
+    override suspend fun refreshed(): AbsoluteFolder = this
+
+    override val contact: FileSupported get() = files.contact
+    override val parent: AbsoluteFolder? get() = null
+    override val id: String get() = "/"
+    override val name: String get() = "/"
+    override val absolutePath: String get() = "/"
+    override val isFile: Boolean get() = false
+    override val isFolder: Boolean get() = true
+    override val uploadTime: Long get() = 0
+    override val lastModifiedTime: Long get() = 0
+    override val uploaderId: Long get() = 0
+    override suspend fun exists(): Boolean = true
+    override suspend fun renameTo(newName: String): Boolean = false
+    override suspend fun delete(): Boolean = false
+    override suspend fun refresh(): Boolean = true
+    override fun toString(): String = absolutePath
+
+
+    override suspend fun folders(): Flow<AbsoluteFolder> {
+        TODO("Not yet implemented")
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun foldersStream(): Stream<AbsoluteFolder> {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun files(): Flow<AbsoluteFile> {
+        TODO("Not yet implemented")
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun filesStream(): Stream<AbsoluteFile> {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun children(): Flow<AbsoluteFileFolder> {
+        TODO("Not yet implemented")
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun childrenStream(): Stream<AbsoluteFileFolder> {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun createFolder(name: String): AbsoluteFolder {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun resolveFolder(name: String): AbsoluteFolder? {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun resolveFolderById(id: String): AbsoluteFolder? {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun resolveFileById(id: String, deep: Boolean): AbsoluteFile? {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun resolveFiles(path: String): Flow<AbsoluteFile> {
+        TODO("Not yet implemented")
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun resolveFilesStream(path: String): Stream<AbsoluteFile> {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun resolveAll(path: String): Flow<AbsoluteFileFolder> {
+        TODO("Not yet implemented")
+    }
+
+    @JavaFriendlyAPI
+    override suspend fun resolveAllStream(path: String): Stream<AbsoluteFileFolder> {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun uploadNewFile(
+        filepath: String,
+        content: ExternalResource,
+        callback: ProgressionCallback<AbsoluteFile, Long>?
+    ): AbsoluteFile {
+        TODO("Not yet implemented")
+    }
+}

+ 13 - 0
mirai-core-mock/src/internal/txfs/TxFileDiskImpl.kt

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai.mock.internal.txfs
 
+import net.mamoe.mirai.mock.internal.remotefile.length
 import net.mamoe.mirai.mock.txfs.TxFileDisk
 import net.mamoe.mirai.mock.txfs.TxFileSystem
 import net.mamoe.mirai.mock.txfs.TxRemoteFile
@@ -210,6 +211,12 @@ internal class TxFileImpl(
     override val name: String get() = system.resolveName(id)
     override val path: String get() = system.resolveAbsPath(id)
     override val parent: TxFileImpl get() = system.resolveParent(id)
+    override val size: Long
+        get() {
+            val pt = toPath
+            if (pt.isFile) return pt.length()
+            return 0
+        }
 
     override fun listFiles(): Sequence<TxRemoteFile>? {
         val pt = toPath
@@ -257,6 +264,12 @@ internal class TxFileImpl(
         path.toPath.resolve(id.substring(1)).createFile()
     }
 
+    override fun resolveNativePath(): Path {
+        val pt = toPath
+        if (!pt.isFile) error("file not exists: $this <$pt>")
+        return pt
+    }
+
     override fun asExternalResource(): ExternalResource {
         val pt = toPath
         if (!pt.isFile) error("file not exists: $pt")

+ 7 - 3
mirai-core-mock/src/fsserver/TmpFsServer.kt → mirai-core-mock/src/txfs/TmpFsServer.kt

@@ -7,16 +7,17 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-package net.mamoe.mirai.mock.fsserver
+package net.mamoe.mirai.mock.txfs
 
 import com.google.common.jimfs.Configuration
 import com.google.common.jimfs.Jimfs
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.mock.internal.remotefile.FsServerImpl
 import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.plusHttpSubpath
 import java.io.Closeable
 import java.nio.file.FileSystem
+import java.nio.file.Path
 
 /**
  * 临时 HTTP 文件中转服务器
@@ -35,6 +36,7 @@ import java.nio.file.FileSystem
 public interface TmpFsServer : Closeable {
     public val httpRoot: String
     public val fsSystem: FileSystem
+    public val fsDisk: TxFileDisk
 
 
     /**
@@ -70,6 +72,8 @@ public interface TmpFsServer : Closeable {
         return httpRoot.plusHttpSubpath(id)
     }
 
+    public fun resolveHttpUrl(path: Path): String
+
     public companion object {
         @JvmStatic
         public fun ofFsSystem(fs: FileSystem, port: Int = 0): TmpFsServer {
@@ -87,4 +91,4 @@ public interface TmpFsServer : Closeable {
             return FsServerImpl(fs, port)
         }
     }
-}
+}

+ 4 - 0
mirai-core-mock/src/txfs/TxRemoteFile.kt

@@ -10,6 +10,7 @@
 package net.mamoe.mirai.mock.txfs
 
 import net.mamoe.mirai.utils.ExternalResource
+import java.nio.file.Path
 
 public interface TxRemoteFile {
     public val system: TxFileSystem
@@ -21,6 +22,7 @@ public interface TxRemoteFile {
     public val id: String
     public val exists: Boolean
     public val parent: TxRemoteFile
+    public val size: Long
 
     public fun listFiles(): Sequence<TxRemoteFile>?
     public fun delete(): Boolean
@@ -28,6 +30,8 @@ public interface TxRemoteFile {
     public fun moveTo(path: TxRemoteFile)
 
     public fun asExternalResource(): ExternalResource
+    public fun resolveNativePath(): Path
+
     public fun uploadFile(name: String, content: ExternalResource, uploader: Long): TxRemoteFile
 
     public fun mksubdir(name: String, creator: Long): TxRemoteFile

+ 1 - 1
mirai-core-mock/src/userprofile/UserProfileService.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.mock.userprofile
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.data.UserProfile
 import net.mamoe.mirai.mock.userprofile.MockUserProfileBuilder.Companion.invoke

+ 1 - 1
mirai-core-mock/src/utils/mockdsl.kt

@@ -11,7 +11,7 @@
 
 package net.mamoe.mirai.mock.utils
 
-import net.mamoe.kjbb.JvmBlockingBridge
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.event.broadcast

+ 9 - 1
mirai-core-mock/test/FsServerTest.kt

@@ -10,10 +10,12 @@
 package net.mamoe.mirai.mock.test
 
 import kotlinx.coroutines.runBlocking
-import net.mamoe.mirai.mock.fsserver.TmpFsServer
+import net.mamoe.mirai.mock.txfs.TmpFsServer
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.mkParentDirs
 import org.junit.jupiter.api.Test
 import java.net.URL
+import kotlin.io.path.writeText
 import kotlin.test.assertEquals
 
 @Suppress("RemoveExplicitTypeArguments")
@@ -29,6 +31,12 @@ internal class FsServerTest {
         val response = URL(fsServer.httpRoot + pt).readText()
         assertEquals("Test", response)
 
+        val pt0 = fsServer.fsSystem.getPath("/rand/etc/randrand/somedata")
+        pt0.mkParentDirs()
+        pt0.writeText("Test")
+
+        assertEquals("Test", URL(fsServer.resolveHttpUrl(pt0)).readText())
+
         fsServer.close()
     }
 }

+ 0 - 528
mirai-core-mock/test/MockBotTest.kt

@@ -1,528 +0,0 @@
-/*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "NOTHING_TO_INLINE")
-
-package net.mamoe.mirai.mock.test
-
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.runBlocking
-import net.mamoe.mirai.Mirai
-import net.mamoe.mirai.contact.MemberPermission
-import net.mamoe.mirai.contact.announcement.AnnouncementParameters
-import net.mamoe.mirai.event.Event
-import net.mamoe.mirai.event.GlobalEventChannel
-import net.mamoe.mirai.event.events.*
-import net.mamoe.mirai.message.data.MessageSource.Key.recall
-import net.mamoe.mirai.message.data.OnlineMessageSource
-import net.mamoe.mirai.message.data.PlainText
-import net.mamoe.mirai.message.data.messageChainOf
-import net.mamoe.mirai.message.data.source
-import net.mamoe.mirai.mock.MockBotFactory
-import net.mamoe.mirai.mock.contact.MockNormalMember
-import net.mamoe.mirai.mock.contact.announcement.MockOnlineAnnouncement
-import net.mamoe.mirai.mock.internal.MockBotImpl
-import net.mamoe.mirai.mock.userprofile.buildUserProfile
-import net.mamoe.mirai.mock.utils.*
-import net.mamoe.mirai.mock.utils.MockActions.mockFireRecalled
-import net.mamoe.mirai.mock.utils.MockActions.nudged
-import net.mamoe.mirai.mock.utils.MockActions.nudgedBy
-import net.mamoe.mirai.mock.utils.MockActions.says
-import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.TestInstance
-import org.junit.jupiter.api.fail
-import java.net.URL
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-import kotlin.reflect.jvm.jvmName
-import kotlin.test.*
-
-@Suppress("RemoveExplicitTypeArguments")
-@TestInstance(TestInstance.Lifecycle.PER_METHOD)
-internal class MockBotTest {
-    internal val bot = MockBotFactory.newMockBotBuilder()
-        .id(984651187)
-        .nick("Sakura")
-        .create()
-
-    @Test
-    internal fun testMockBotMocking() = runBlocking<Unit> {
-        repeat(50) { i ->
-            bot.addFriend(20000L + i, "usr$i")
-            bot.addStranger(10000L + i, "stranger$i")
-            bot.addGroup(798100000L + i, "group$i")
-        }
-        assertEquals(50, bot.friends.size)
-        assertEquals(50, bot.strangers.size)
-        assertEquals(50, bot.groups.size)
-
-        repeat(50) { i ->
-            assertEquals("usr$i", bot.getFriendOrFail(20000L + i).nick)
-            assertEquals("stranger$i", bot.getStrangerOrFail(10000L + i).nick)
-
-            val group = bot.getGroupOrFail(798100000L + i)
-            assertEquals("group$i", group.name)
-            assertSame(group.botAsMember, group.owner)
-            assertSame(MemberPermission.OWNER, group.botPermission)
-            assertEquals(0, group.members.size)
-        }
-
-        val mockGroup = bot.group(798100000L)
-        repeat(50) { i ->
-            mockGroup.addMember(simpleMemberInfo(3700000L + i, "member$i", permission = MemberPermission.MEMBER))
-        }
-        repeat(50) { i ->
-            val member = mockGroup.member(3700000L + i)
-            assertEquals(MemberPermission.MEMBER, member.permission)
-            assertEquals("member$i", member.nick)
-            assertTrue(member.nameCard.isEmpty())
-            assertEquals(MemberPermission.OWNER, mockGroup.botPermission)
-        }
-
-        val newOwner: MockNormalMember
-        runAndReceiveEventBroadcast {
-            newOwner = mockGroup.addMember0(simpleMemberInfo(84485417, "root", permission = MemberPermission.OWNER))
-        }.let { events ->
-            assertEquals(0, events.size)
-        }
-        assertEquals(MemberPermission.OWNER, newOwner.permission)
-        assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
-        assertSame(newOwner, mockGroup.owner)
-
-        val newNewOwner = mockGroup.member(3700000L)
-        runAndReceiveEventBroadcast {
-            mockGroup.changeOwner(newNewOwner)
-        }.let { events ->
-            assertEquals(2, events.size)
-            assertIsInstance<MemberPermissionChangeEvent>(events[0]) {
-                assertSame(newNewOwner, member)
-                assertSame(MemberPermission.OWNER, new)
-                assertSame(MemberPermission.MEMBER, origin)
-            }
-            assertIsInstance<MemberPermissionChangeEvent>(events[1]) {
-                assertSame(newOwner, member)
-                assertSame(MemberPermission.OWNER, origin)
-                assertSame(MemberPermission.MEMBER, new)
-            }
-        }
-        assertEquals(MemberPermission.OWNER, newNewOwner.permission)
-        assertEquals(MemberPermission.MEMBER, newOwner.permission)
-        assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
-        assertSame(newNewOwner, mockGroup.owner)
-    }
-
-    @Test
-    internal fun testMockAvatarChange() = runBlocking<Unit> {
-        assertEquals("http://q1.qlogo.cn/g?b=qq&nk=${bot.id}&s=640", bot.avatarUrl)
-        runAndReceiveEventBroadcast {
-            bot.avatarUrl = "http://localhost/test.png"
-            assertEquals("http://localhost/test.png", bot.avatarUrl)
-        }.let { events ->
-            assertEquals(1, events.size)
-            assertIsInstance<BotAvatarChangedEvent>(events[0])
-        }
-    }
-
-    @Test
-    internal fun testNewFriendRequest() = runBlocking<Unit> {
-        runAndReceiveEventBroadcast {
-            bot.broadcastNewFriendRequestEvent(
-                1, "Hi", 0, "Hello!"
-            ).reject()
-        }.let { events ->
-            assertEquals(1, events.size)
-            assertIsInstance<NewFriendRequestEvent>(events[0]) {
-                assertEquals(1, fromId)
-                assertEquals("Hi", fromNick)
-                assertEquals(0, fromGroupId)
-                assertEquals("Hello!", message)
-            }
-            assertEquals(bot.friends.size, 0)
-        }
-
-        runAndReceiveEventBroadcast {
-            bot.broadcastNewFriendRequestEvent(
-                1, "Hi", 0, "Hello!"
-            ).accept()
-        }.let { events ->
-            assertEquals(2, events.size, events.toString())
-            assertIsInstance<NewFriendRequestEvent>(events[0]) {
-                assertEquals(1, fromId)
-                assertEquals("Hi", fromNick)
-                assertEquals(0, fromGroupId)
-                assertEquals("Hello!", message)
-            }
-
-            assertIsInstance<FriendAddEvent>(events[1]) {
-                assertEquals(1, friend.id)
-                assertEquals("Hi", friend.nick)
-                assertSame(friend, bot.getFriend(friend.id))
-            }
-
-            assertEquals(1, bot.friends.size)
-        }
-
-    }
-
-    @Test
-    internal fun testMessageEventBroadcast() = runBlocking<Unit> {
-        runAndReceiveEventBroadcast {
-            bot.addGroup(5597122, "testing!")
-                .addMember0(simpleMemberInfo(5971, "test", permission = MemberPermission.OWNER))
-                .says("Hello World")
-
-            bot.addFriend(9815, "tester").says("Msg By TestFriend")
-
-            bot.addStranger(987166, "sudo").says("How are you")
-
-            bot.group(5597122).sendMessage("Testing message")
-            bot.friend(9815).sendMessage("Hi my friend")
-            bot.stranger(987166).sendMessage("How are you")
-        }.let { events ->
-            assertEquals(9, events.size)
-
-            assertIsInstance<GroupMessageEvent>(events[0]) {
-                assertEquals("Hello World", message.contentToString())
-                assertEquals("test", senderName)
-                assertEquals(5971, sender.id)
-                assertEquals(5597122, group.id)
-                assertIsInstance<OnlineMessageSource.Incoming.FromGroup>(message.source)
-            }
-            assertIsInstance<FriendMessageEvent>(events[1]) {
-                assertEquals("Msg By TestFriend", message.contentToString())
-                assertEquals("tester", senderName)
-                assertEquals(9815, sender.id)
-                assertIsInstance<OnlineMessageSource.Incoming.FromFriend>(message.source)
-
-            }
-            assertIsInstance<StrangerMessageEvent>(events[2]) {
-                assertEquals("How are you", message.contentToString())
-                assertEquals("sudo", senderName)
-                assertEquals(987166, sender.id)
-                assertIsInstance<OnlineMessageSource.Incoming.FromStranger>(message.source)
-            }
-
-            assertIsInstance<GroupMessagePreSendEvent>(events[3])
-            assertIsInstance<GroupMessagePostSendEvent>(events[4]) {
-                assertIsInstance<OnlineMessageSource.Outgoing.ToGroup>(receipt!!.source)
-            }
-            assertIsInstance<FriendMessagePreSendEvent>(events[5])
-            assertIsInstance<FriendMessagePostSendEvent>(events[6]) {
-                assertIsInstance<OnlineMessageSource.Outgoing.ToFriend>(receipt!!.source)
-            }
-            assertIsInstance<StrangerMessagePreSendEvent>(events[7])
-            assertIsInstance<StrangerMessagePostSendEvent>(events[8]) {
-                assertIsInstance<OnlineMessageSource.Outgoing.ToStranger>(receipt!!.source)
-            }
-        }
-    }
-
-    @Test
-    internal fun testMockGroupJoinRequest() = runBlocking<Unit> {
-        val group = bot.addGroup(9875555515, "test")
-
-        runAndReceiveEventBroadcast {
-            group.broadcastNewMemberJoinRequestEvent(
-                100000000, "demo", "msg"
-            ).accept()
-        }.let { events ->
-            assertEquals(2, events.size)
-            assertIsInstance<MemberJoinRequestEvent>(events[0]) {
-                assertEquals(100000000, fromId)
-                assertEquals("demo", fromNick)
-                assertEquals("msg", message)
-            }
-            assertIsInstance<MemberJoinEvent>(events[1]) {
-                assertEquals(100000000, member.id)
-                assertEquals("demo", member.nick)
-            }
-        }
-
-        val member = group.member(100000000)
-        assertEquals(MemberPermission.MEMBER, member.permission)
-    }
-
-    @Test
-    internal fun testMockBotJoinGroupRequest() = runBlocking<Unit> {
-        val invitor = bot.addFriend(5710, "demo")
-        runAndReceiveEventBroadcast {
-            invitor.broadcastInviteBotJoinGroupRequestEvent(999999999, "test")
-                .accept()
-        }.let { events ->
-            assertEquals(2, events.size)
-            assertIsInstance<BotInvitedJoinGroupRequestEvent>(events[0]) {
-                assertEquals(5710, invitorId)
-                assertEquals("demo", invitorNick)
-                assertEquals(999999999, groupId)
-                assertEquals("test", groupName)
-            }
-            assertIsInstance<BotJoinGroupEvent>(events[1]) {
-                assertNotSame(group.botAsMember, group.owner)
-                assertEquals(MemberPermission.MEMBER, group.botPermission)
-                assertEquals(999999999, group.id)
-                assertEquals(MemberPermission.OWNER, group.owner.permission)
-            }
-        }
-    }
-
-    @Test
-    internal fun testMessageRecallEventBroadcast() = runBlocking<Unit> {
-        val group = bot.addGroup(8484846, "g")
-        val admin = group.addMember0(simpleMemberInfo(945474, "admin", permission = MemberPermission.ADMINISTRATOR))
-        val sender = group.addMember0(simpleMemberInfo(178711, "usr", permission = MemberPermission.MEMBER))
-
-        runAndReceiveEventBroadcast {
-            sender.says("Test").mockFireRecalled()
-            sender.says("Admin recall").mockFireRecalled(admin)
-            group.sendMessage("Hello world").mockFireRecalled(admin)
-            sender.says("Hi").recall()
-            admin.says("I'm admin").let { resp ->
-                resp.recall()
-                assertFails { resp.recall() }.let(::println)
-            }
-        }.dropMsgChat().let { events ->
-            assertEquals(5, events.size)
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
-                assertNull(operator)
-                assertSame(sender, author)
-            }
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[1]) {
-                assertSame(admin, operator)
-                assertSame(sender, author)
-            }
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[2]) {
-                assertSame(admin, operator)
-                assertSame(group.botAsMember, author)
-            }
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[3]) {
-                assertSame(null, operator)
-                assertSame(sender, author)
-            }
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[4]) {
-                assertSame(null, operator)
-                assertSame(admin, author)
-            }
-        }
-
-        val root = group.addMember0(simpleMemberInfo(54986565, "root", permission = MemberPermission.OWNER))
-
-        runAndReceiveEventBroadcast {
-            sender.says("0").runAndAssertFails { recall() }
-            admin.says("0").runAndAssertFails { recall() }
-            root.says("0").runAndAssertFails { recall() }
-            group.sendMessage("Hi").recall()
-        }.dropMsgChat().let { events ->
-            assertEquals(1, events.size)
-            assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
-                assertEquals(group.botAsMember, author)
-                assertEquals(null, operator)
-            }
-        }
-    }
-
-    @Test
-    internal fun testGroupAnnouncements() = runBlocking<Unit> {
-        val group = bot.addGroup(8484541, "87")
-        group.announcements.publish(
-            MockOnlineAnnouncement(
-                content = "Hello World",
-                parameters = AnnouncementParameters.DEFAULT,
-                senderId = 971121,
-                allConfirmed = false,
-                confirmedMembersCount = 0,
-                publicationTime = 0
-            )
-        )
-        val anc = group.announcements.asFlow().toList()
-        assertEquals(1, anc.size)
-        assertEquals("Hello World", anc[0].content)
-        assertFalse(anc[0].fid.isEmpty())
-        assertEquals(anc[0], group.announcements.get(anc[0].fid))
-    }
-
-    @Test
-    internal fun testLeave() = runBlocking<Unit> {
-        runAndReceiveEventBroadcast {
-            bot.addGroup(1, "1").quit()
-            bot.addFriend(2, "2").delete()
-            bot.addStranger(3, "3").delete()
-            bot.addGroup(4, "4")
-                .addMember0(simpleMemberInfo(5, "5", permission = MemberPermission.MEMBER))
-                .broadcastMemberLeave()
-            bot.addGroup(6, "6")
-                .addMember0(simpleMemberInfo(7, "7", permission = MemberPermission.OWNER))
-                .broadcastKickBot()
-        }.let { events ->
-            assertEquals(5, events.size)
-            assertIsInstance<BotLeaveEvent.Active>(events[0]) {
-                assertEquals(1, group.id)
-            }
-            assertIsInstance<FriendDeleteEvent>(events[1]) {
-                assertEquals(2, friend.id)
-            }
-            assertIsInstance<StrangerRelationChangeEvent.Deleted>(events[2]) {
-                assertEquals(3, stranger.id)
-            }
-            assertIsInstance<MemberLeaveEvent>(events[3]) {
-                assertEquals(4, group.id)
-                assertEquals(5, member.id)
-            }
-            assertIsInstance<BotLeaveEvent.Kick>(events[4]) {
-                assertEquals(6, group.id)
-                assertEquals(7, operator.id)
-            }
-        }
-    }
-
-    @Test
-    internal fun testNudge() = runBlocking<Unit> {
-        val group = bot.addGroup(1, "1")
-        val nudgeSender = group.addMember0(simpleMemberInfo(3, "3", permission = MemberPermission.MEMBER))
-        val nudged = group.addMember0(simpleMemberInfo(4, "4", permission = MemberPermission.MEMBER))
-
-        val myFriend = bot.addFriend(1, "514")
-        val myStranger = bot.addStranger(2, "awef")
-
-        runAndReceiveEventBroadcast {
-            nudged.nudgedBy(nudgeSender)
-            nudged.nudge().sendTo(group)
-            myFriend.nudged(bot)
-            myStranger.nudged(bot)
-            myFriend.nudgedBy(bot)
-            myStranger.nudgedBy(bot)
-        }.let { events ->
-            assertEquals(6, events.size)
-            assertIsInstance<NudgeEvent>(events[0]) {
-                assertSame(nudgeSender, this.from)
-                assertSame(nudged, this.target)
-                assertSame(group, this.subject)
-            }
-            assertIsInstance<NudgeEvent>(events[1]) {
-                assertSame(bot, this.from)
-                assertSame(nudged, this.target)
-                assertSame(group, this.subject)
-            }
-            assertIsInstance<NudgeEvent>(events[2]) {
-                assertSame(myFriend, this.from)
-                assertSame(bot, this.target)
-                assertSame(myFriend, this.subject)
-            }
-            assertIsInstance<NudgeEvent>(events[3]) {
-                assertSame(myStranger, this.from)
-                assertSame(bot, this.target)
-                assertSame(myStranger, this.subject)
-            }
-            assertIsInstance<NudgeEvent>(events[4]) {
-                assertSame(bot, this.from)
-                assertSame(myFriend, this.target)
-                assertSame(myFriend, this.subject)
-            }
-            assertIsInstance<NudgeEvent>(events[5]) {
-                assertSame(bot, this.from)
-                assertSame(myStranger, this.target)
-                assertSame(myStranger, this.subject)
-            }
-        }
-    }
-
-    @Test
-    internal fun testGroupFileV1() = runBlocking<Unit> {
-        val fsroot = bot.addGroup(5417, "58aw").filesRoot
-        fsroot.resolve("helloworld.txt").uploadAndSend(
-            "HelloWorld".toByteArray().toExternalResource().toAutoCloseable()
-        )
-        assertEquals(1, fsroot.listFilesCollection().size)
-        assertEquals(
-            "HelloWorld",
-            fsroot.resolve("helloworld.txt")
-                .getDownloadInfo()!!
-                .url.toUrl()
-                .also { println("Mock file url: $it") }
-                .readText()
-        )
-        fsroot.resolve("helloworld.txt").delete()
-        assertEquals(0, fsroot.listFilesCollection().size)
-    }
-
-    @Test
-    internal fun testQueryProfile() = runBlocking<Unit> {
-        val service = bot.userProfileService
-        val profile = buildUserProfile {
-            nickname("Test0")
-        }
-        service.putUserProfile(1, profile)
-        assertSame(profile, Mirai.queryProfile(bot, 1))
-    }
-
-    @Test
-    internal fun testRoamingMessages() = runBlocking<Unit> {
-        val mockFriend = bot.addFriend(1, "1")
-        mockFriend says { append("Testing!") }
-        mockFriend says { append("Test2!") }
-        mockFriend.sendMessage("Pong!")
-
-        mockFriend.roamingMessages.getAllMessages().toList().let { messages ->
-            assertEquals(3, messages.size)
-            assertEquals(messageChainOf(PlainText("Testing!")), messages[0])
-            assertEquals(messageChainOf(PlainText("Test2!")), messages[1])
-            assertEquals(messageChainOf(PlainText("Pong!")), messages[2])
-        }
-    }
-
-    //<editor-fold defaultstate="collapsed" desc="Utils">
-    @AfterEach
-    internal fun destroy() {
-        bot.close()
-    }
-
-    internal inline fun <reified T> assertIsInstance(value: Any?, block: T.() -> Unit = {}) {
-        if (value !is T) {
-            fail { "Actual value $value (${value?.javaClass}) is not instanceof ${T::class.jvmName}" }
-        }
-        block(value)
-    }
-
-    internal suspend inline fun runAndReceiveEventBroadcast(
-        action: () -> Unit
-    ): List<Event> {
-
-        contract {
-            callsInPlace(action, InvocationKind.EXACTLY_ONCE)
-        }
-
-        val result = mutableListOf<Event>()
-        val listener = GlobalEventChannel.subscribeAlways<Event> {
-            result.add(this)
-        }
-
-        action()
-
-        (bot as MockBotImpl).joinEventBroadcast()
-
-        listener.cancel()
-        return result
-    }
-
-    internal fun List<Event>.dropMessagePrePost() = filterNot {
-        it is MessagePreSendEvent || it is MessagePostSendEvent<*>
-    }
-
-    internal fun List<Event>.dropMsgChat() = filterNot {
-        it is MessageEvent || it is MessagePreSendEvent || it is MessagePostSendEvent<*>
-    }
-
-    internal inline fun String.toUrl(): URL = URL(this)
-
-    internal inline fun <T> T.runAndAssertFails(block: T.() -> Unit) {
-        assertFails { block() }
-    }
-
-    //</editor-fold>
-}

+ 88 - 0
mirai-core-mock/test/MockBotTestBase.kt

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.test
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.event.Event
+import net.mamoe.mirai.event.GlobalEventChannel
+import net.mamoe.mirai.event.events.MessageEvent
+import net.mamoe.mirai.event.events.MessagePostSendEvent
+import net.mamoe.mirai.event.events.MessagePreSendEvent
+import net.mamoe.mirai.mock.MockBotFactory
+import net.mamoe.mirai.mock.internal.MockBotImpl
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.fail
+import java.net.URL
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+import kotlin.reflect.jvm.jvmName
+import kotlin.test.assertFails
+
+@TestInstance(TestInstance.Lifecycle.PER_METHOD)
+internal open class MockBotTestBase {
+    internal val bot = MockBotFactory.newMockBotBuilder()
+        .id(3259866114)
+        .nick("Kafusumi")
+        .create()
+
+    @AfterEach
+    internal fun `$$bot_release`() {
+        bot.close()
+    }
+
+    internal inline fun <reified T> assertIsInstance(value: Any?, block: T.() -> Unit = {}) {
+        if (value !is T) {
+            fail { "Actual value $value (${value?.javaClass}) is not instanceof ${T::class.jvmName}" }
+        }
+        block(value)
+    }
+
+    internal fun runTest(action: suspend CoroutineScope.() -> Unit) {
+        runBlocking(block = action)
+    }
+
+    internal suspend fun runAndReceiveEventBroadcast(
+        action: suspend () -> Unit
+    ): List<Event> {
+
+        contract {
+            callsInPlace(action, InvocationKind.EXACTLY_ONCE)
+        }
+
+        val result = mutableListOf<Event>()
+        val listener = GlobalEventChannel.subscribeAlways<Event> {
+            result.add(this)
+        }
+
+        action()
+
+        (bot as MockBotImpl).joinEventBroadcast()
+
+        listener.cancel()
+        return result
+    }
+
+    internal fun List<Event>.dropMessagePrePost() = filterNot {
+        it is MessagePreSendEvent || it is MessagePostSendEvent<*>
+    }
+
+    internal fun List<Event>.dropMsgChat() = filterNot {
+        it is MessageEvent || it is MessagePreSendEvent || it is MessagePostSendEvent<*>
+    }
+
+    internal fun String.toUrl(): URL = URL(this)
+
+    internal inline fun <T> T.runAndAssertFails(block: T.() -> Unit) {
+        assertFails { block() }
+    }
+
+}

+ 212 - 0
mirai-core-mock/test/mock/MessagingTest.kt

@@ -0,0 +1,212 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.test.mock
+
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.message.data.MessageSource.Key.recall
+import net.mamoe.mirai.message.data.OnlineMessageSource
+import net.mamoe.mirai.message.data.PlainText
+import net.mamoe.mirai.message.data.messageChainOf
+import net.mamoe.mirai.message.data.source
+import net.mamoe.mirai.mock.test.MockBotTestBase
+import net.mamoe.mirai.mock.utils.MockActions.mockFireRecalled
+import net.mamoe.mirai.mock.utils.MockActions.nudged
+import net.mamoe.mirai.mock.utils.MockActions.nudgedBy
+import net.mamoe.mirai.mock.utils.MockActions.says
+import net.mamoe.mirai.mock.utils.friend
+import net.mamoe.mirai.mock.utils.group
+import net.mamoe.mirai.mock.utils.simpleMemberInfo
+import net.mamoe.mirai.mock.utils.stranger
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+
+internal class MessagingTest: MockBotTestBase() {
+
+    @Test
+    internal fun testMessageEventBroadcast() = runTest {
+        runAndReceiveEventBroadcast {
+            bot.addGroup(5597122, "testing!")
+                .addMember0(simpleMemberInfo(5971, "test", permission = MemberPermission.OWNER))
+                .says("Hello World")
+
+            bot.addFriend(9815, "tester").says("Msg By TestFriend")
+
+            bot.addStranger(987166, "sudo").says("How are you")
+
+            bot.group(5597122).sendMessage("Testing message")
+            bot.friend(9815).sendMessage("Hi my friend")
+            bot.stranger(987166).sendMessage("How are you")
+        }.let { events ->
+            assertEquals(9, events.size)
+
+            assertIsInstance<GroupMessageEvent>(events[0]) {
+                assertEquals("Hello World", message.contentToString())
+                assertEquals("test", senderName)
+                assertEquals(5971, sender.id)
+                assertEquals(5597122, group.id)
+                assertIsInstance<OnlineMessageSource.Incoming.FromGroup>(message.source)
+            }
+            assertIsInstance<FriendMessageEvent>(events[1]) {
+                assertEquals("Msg By TestFriend", message.contentToString())
+                assertEquals("tester", senderName)
+                assertEquals(9815, sender.id)
+                assertIsInstance<OnlineMessageSource.Incoming.FromFriend>(message.source)
+
+            }
+            assertIsInstance<StrangerMessageEvent>(events[2]) {
+                assertEquals("How are you", message.contentToString())
+                assertEquals("sudo", senderName)
+                assertEquals(987166, sender.id)
+                assertIsInstance<OnlineMessageSource.Incoming.FromStranger>(message.source)
+            }
+
+            assertIsInstance<GroupMessagePreSendEvent>(events[3])
+            assertIsInstance<GroupMessagePostSendEvent>(events[4]) {
+                assertIsInstance<OnlineMessageSource.Outgoing.ToGroup>(receipt!!.source)
+            }
+            assertIsInstance<FriendMessagePreSendEvent>(events[5])
+            assertIsInstance<FriendMessagePostSendEvent>(events[6]) {
+                assertIsInstance<OnlineMessageSource.Outgoing.ToFriend>(receipt!!.source)
+            }
+            assertIsInstance<StrangerMessagePreSendEvent>(events[7])
+            assertIsInstance<StrangerMessagePostSendEvent>(events[8]) {
+                assertIsInstance<OnlineMessageSource.Outgoing.ToStranger>(receipt!!.source)
+            }
+        }
+    }
+
+    @Test
+    internal fun testNudge() = runTest {
+        val group = bot.addGroup(1, "1")
+        val nudgeSender = group.addMember0(simpleMemberInfo(3, "3", permission = MemberPermission.MEMBER))
+        val nudged = group.addMember0(simpleMemberInfo(4, "4", permission = MemberPermission.MEMBER))
+
+        val myFriend = bot.addFriend(1, "514")
+        val myStranger = bot.addStranger(2, "awef")
+
+        runAndReceiveEventBroadcast {
+            nudged.nudgedBy(nudgeSender)
+            nudged.nudge().sendTo(group)
+            myFriend.nudged(bot)
+            myStranger.nudged(bot)
+            myFriend.nudgedBy(bot)
+            myStranger.nudgedBy(bot)
+        }.let { events ->
+            assertEquals(6, events.size)
+            assertIsInstance<NudgeEvent>(events[0]) {
+                assertSame(nudgeSender, this.from)
+                assertSame(nudged, this.target)
+                assertSame(group, this.subject)
+            }
+            assertIsInstance<NudgeEvent>(events[1]) {
+                assertSame(bot, this.from)
+                assertSame(nudged, this.target)
+                assertSame(group, this.subject)
+            }
+            assertIsInstance<NudgeEvent>(events[2]) {
+                assertSame(myFriend, this.from)
+                assertSame(bot, this.target)
+                assertSame(myFriend, this.subject)
+            }
+            assertIsInstance<NudgeEvent>(events[3]) {
+                assertSame(myStranger, this.from)
+                assertSame(bot, this.target)
+                assertSame(myStranger, this.subject)
+            }
+            assertIsInstance<NudgeEvent>(events[4]) {
+                assertSame(bot, this.from)
+                assertSame(myFriend, this.target)
+                assertSame(myFriend, this.subject)
+            }
+            assertIsInstance<NudgeEvent>(events[5]) {
+                assertSame(bot, this.from)
+                assertSame(myStranger, this.target)
+                assertSame(myStranger, this.subject)
+            }
+        }
+    }
+
+    @Test
+    internal fun testRoamingMessages() = runTest {
+        val mockFriend = bot.addFriend(1, "1")
+        mockFriend says { append("Testing!") }
+        mockFriend says { append("Test2!") }
+        mockFriend.sendMessage("Pong!")
+
+        mockFriend.roamingMessages.getAllMessages().toList().let { messages ->
+            assertEquals(3, messages.size)
+            assertEquals(messageChainOf(PlainText("Testing!")), messages[0])
+            assertEquals(messageChainOf(PlainText("Test2!")), messages[1])
+            assertEquals(messageChainOf(PlainText("Pong!")), messages[2])
+        }
+    }
+
+    @Test
+    internal fun testMessageRecallEventBroadcast() = runTest {
+        val group = bot.addGroup(8484846, "g")
+        val admin = group.addMember0(simpleMemberInfo(945474, "admin", permission = MemberPermission.ADMINISTRATOR))
+        val sender = group.addMember0(simpleMemberInfo(178711, "usr", permission = MemberPermission.MEMBER))
+
+        runAndReceiveEventBroadcast {
+            sender.says("Test").mockFireRecalled()
+            sender.says("Admin recall").mockFireRecalled(admin)
+            group.sendMessage("Hello world").mockFireRecalled(admin)
+            sender.says("Hi").recall()
+            admin.says("I'm admin").let { resp ->
+                resp.recall()
+                assertFails { resp.recall() }.let(::println)
+            }
+        }.dropMsgChat().let { events ->
+            assertEquals(5, events.size)
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
+                assertNull(operator)
+                assertSame(sender, author)
+            }
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[1]) {
+                assertSame(admin, operator)
+                assertSame(sender, author)
+            }
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[2]) {
+                assertSame(admin, operator)
+                assertSame(group.botAsMember, author)
+            }
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[3]) {
+                assertSame(null, operator)
+                assertSame(sender, author)
+            }
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[4]) {
+                assertSame(null, operator)
+                assertSame(admin, author)
+            }
+        }
+
+        val root = group.addMember0(simpleMemberInfo(54986565, "root", permission = MemberPermission.OWNER))
+
+        runAndReceiveEventBroadcast {
+            sender.says("0").runAndAssertFails { recall() }
+            admin.says("0").runAndAssertFails { recall() }
+            root.says("0").runAndAssertFails { recall() }
+            group.sendMessage("Hi").recall()
+        }.dropMsgChat().let { events ->
+            assertEquals(1, events.size)
+            assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
+                assertEquals(group.botAsMember, author)
+                assertEquals(null, operator)
+            }
+        }
+    }
+
+}

+ 117 - 0
mirai-core-mock/test/mock/MockBotBaseTest.kt

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.test.mock
+
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.event.events.BotAvatarChangedEvent
+import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
+import net.mamoe.mirai.mock.contact.MockNormalMember
+import net.mamoe.mirai.mock.test.MockBotTestBase
+import net.mamoe.mirai.mock.userprofile.buildUserProfile
+import net.mamoe.mirai.mock.utils.group
+import net.mamoe.mirai.mock.utils.member
+import net.mamoe.mirai.mock.utils.simpleMemberInfo
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+internal class MockBotBaseTest : MockBotTestBase() {
+
+    @Test
+    internal fun testMockBotMocking() = runTest {
+        repeat(50) { i ->
+            bot.addFriend(20000L + i, "usr$i")
+            bot.addStranger(10000L + i, "stranger$i")
+            bot.addGroup(798100000L + i, "group$i")
+        }
+        assertEquals(50, bot.friends.size)
+        assertEquals(50, bot.strangers.size)
+        assertEquals(50, bot.groups.size)
+
+        repeat(50) { i ->
+            assertEquals("usr$i", bot.getFriendOrFail(20000L + i).nick)
+            assertEquals("stranger$i", bot.getStrangerOrFail(10000L + i).nick)
+
+            val group = bot.getGroupOrFail(798100000L + i)
+            assertEquals("group$i", group.name)
+            assertSame(group.botAsMember, group.owner)
+            assertSame(MemberPermission.OWNER, group.botPermission)
+            assertEquals(0, group.members.size)
+        }
+
+        val mockGroup = bot.group(798100000L)
+        repeat(50) { i ->
+            mockGroup.addMember(simpleMemberInfo(3700000L + i, "member$i", permission = MemberPermission.MEMBER))
+        }
+        repeat(50) { i ->
+            val member = mockGroup.member(3700000L + i)
+            assertEquals(MemberPermission.MEMBER, member.permission)
+            assertEquals("member$i", member.nick)
+            assertTrue(member.nameCard.isEmpty())
+            assertEquals(MemberPermission.OWNER, mockGroup.botPermission)
+        }
+
+        val newOwner: MockNormalMember
+        runAndReceiveEventBroadcast {
+            newOwner = mockGroup.addMember0(simpleMemberInfo(84485417, "root", permission = MemberPermission.OWNER))
+        }.let { events ->
+            assertEquals(0, events.size)
+        }
+        assertEquals(MemberPermission.OWNER, newOwner.permission)
+        assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
+        assertSame(newOwner, mockGroup.owner)
+
+        val newNewOwner = mockGroup.member(3700000L)
+        runAndReceiveEventBroadcast {
+            mockGroup.changeOwner(newNewOwner)
+        }.let { events ->
+            assertEquals(2, events.size)
+            assertIsInstance<MemberPermissionChangeEvent>(events[0]) {
+                assertSame(newNewOwner, member)
+                assertSame(MemberPermission.OWNER, new)
+                assertSame(MemberPermission.MEMBER, origin)
+            }
+            assertIsInstance<MemberPermissionChangeEvent>(events[1]) {
+                assertSame(newOwner, member)
+                assertSame(MemberPermission.OWNER, origin)
+                assertSame(MemberPermission.MEMBER, new)
+            }
+        }
+        assertEquals(MemberPermission.OWNER, newNewOwner.permission)
+        assertEquals(MemberPermission.MEMBER, newOwner.permission)
+        assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
+        assertSame(newNewOwner, mockGroup.owner)
+    }
+
+    @Test
+    internal fun testMockAvatarChange() = runTest {
+        assertEquals("http://q1.qlogo.cn/g?b=qq&nk=${bot.id}&s=640", bot.avatarUrl)
+        runAndReceiveEventBroadcast {
+            bot.avatarUrl = "http://localhost/test.png"
+            assertEquals("http://localhost/test.png", bot.avatarUrl)
+        }.let { events ->
+            assertEquals(1, events.size)
+            assertIsInstance<BotAvatarChangedEvent>(events[0])
+        }
+    }
+
+    @Test
+    internal fun testQueryProfile() = runTest {
+        val service = bot.userProfileService
+        val profile = buildUserProfile {
+            nickname("Test0")
+        }
+        service.putUserProfile(1, profile)
+        assertSame(profile, Mirai.queryProfile(bot, 1))
+    }
+
+}

+ 62 - 0
mirai-core-mock/test/mock/MockFriendTest.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.test.mock
+
+import net.mamoe.mirai.event.events.FriendAddEvent
+import net.mamoe.mirai.event.events.NewFriendRequestEvent
+import net.mamoe.mirai.mock.test.MockBotTestBase
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+
+internal class MockFriendTest: MockBotTestBase() {
+
+    @Test
+    internal fun testNewFriendRequest() = runTest {
+        runAndReceiveEventBroadcast {
+            bot.broadcastNewFriendRequestEvent(
+                1, "Hi", 0, "Hello!"
+            ).reject()
+        }.let { events ->
+            assertEquals(1, events.size)
+            assertIsInstance<NewFriendRequestEvent>(events[0]) {
+                assertEquals(1, fromId)
+                assertEquals("Hi", fromNick)
+                assertEquals(0, fromGroupId)
+                assertEquals("Hello!", message)
+            }
+            assertEquals(bot.friends.size, 0)
+        }
+
+        runAndReceiveEventBroadcast {
+            bot.broadcastNewFriendRequestEvent(
+                1, "Hi", 0, "Hello!"
+            ).accept()
+        }.let { events ->
+            assertEquals(2, events.size, events.toString())
+            assertIsInstance<NewFriendRequestEvent>(events[0]) {
+                assertEquals(1, fromId)
+                assertEquals("Hi", fromNick)
+                assertEquals(0, fromGroupId)
+                assertEquals("Hello!", message)
+            }
+
+            assertIsInstance<FriendAddEvent>(events[1]) {
+                assertEquals(1, friend.id)
+                assertEquals("Hi", friend.nick)
+                assertSame(friend, bot.getFriend(friend.id))
+            }
+
+            assertEquals(1, bot.friends.size)
+        }
+
+    }
+
+}

+ 147 - 0
mirai-core-mock/test/mock/MockGroupTest.kt

@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.test.mock
+
+import kotlinx.coroutines.flow.toList
+import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.contact.announcement.AnnouncementParameters
+import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.mock.contact.announcement.MockOnlineAnnouncement
+import net.mamoe.mirai.mock.test.MockBotTestBase
+import net.mamoe.mirai.mock.utils.member
+import net.mamoe.mirai.mock.utils.simpleMemberInfo
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotSame
+
+internal class MockGroupTest : MockBotTestBase() {
+    @Test
+    internal fun testMockGroupJoinRequest() = runTest {
+        val group = bot.addGroup(9875555515, "test")
+
+        runAndReceiveEventBroadcast {
+            group.broadcastNewMemberJoinRequestEvent(
+                100000000, "demo", "msg"
+            ).accept()
+        }.let { events ->
+            assertEquals(2, events.size)
+            assertIsInstance<MemberJoinRequestEvent>(events[0]) {
+                assertEquals(100000000, fromId)
+                assertEquals("demo", fromNick)
+                assertEquals("msg", message)
+            }
+            assertIsInstance<MemberJoinEvent>(events[1]) {
+                assertEquals(100000000, member.id)
+                assertEquals("demo", member.nick)
+            }
+        }
+
+        val member = group.member(100000000)
+        assertEquals(MemberPermission.MEMBER, member.permission)
+    }
+
+    @Test
+    internal fun testMockBotJoinGroupRequest() = runTest {
+        val invitor = bot.addFriend(5710, "demo")
+        runAndReceiveEventBroadcast {
+            invitor.broadcastInviteBotJoinGroupRequestEvent(999999999, "test")
+                .accept()
+        }.let { events ->
+            assertEquals(2, events.size)
+            assertIsInstance<BotInvitedJoinGroupRequestEvent>(events[0]) {
+                assertEquals(5710, invitorId)
+                assertEquals("demo", invitorNick)
+                assertEquals(999999999, groupId)
+                assertEquals("test", groupName)
+            }
+            assertIsInstance<BotJoinGroupEvent>(events[1]) {
+                assertNotSame(group.botAsMember, group.owner)
+                assertEquals(MemberPermission.MEMBER, group.botPermission)
+                assertEquals(999999999, group.id)
+                assertEquals(MemberPermission.OWNER, group.owner.permission)
+            }
+        }
+    }
+
+    @Test
+    internal fun testGroupAnnouncements() = runTest {
+        val group = bot.addGroup(8484541, "87")
+        group.announcements.publish(
+            MockOnlineAnnouncement(
+                content = "Hello World",
+                parameters = AnnouncementParameters.DEFAULT,
+                senderId = 971121,
+                allConfirmed = false,
+                confirmedMembersCount = 0,
+                publicationTime = 0
+            )
+        )
+        val anc = group.announcements.asFlow().toList()
+        assertEquals(1, anc.size)
+        assertEquals("Hello World", anc[0].content)
+        assertFalse(anc[0].fid.isEmpty())
+        assertEquals(anc[0], group.announcements.get(anc[0].fid))
+    }
+
+    @Test
+    internal fun testLeave() = runTest {
+        runAndReceiveEventBroadcast {
+            bot.addGroup(1, "1").quit()
+            bot.addFriend(2, "2").delete()
+            bot.addStranger(3, "3").delete()
+            bot.addGroup(4, "4")
+                .addMember0(simpleMemberInfo(5, "5", permission = MemberPermission.MEMBER))
+                .broadcastMemberLeave()
+            bot.addGroup(6, "6")
+                .addMember0(simpleMemberInfo(7, "7", permission = MemberPermission.OWNER))
+                .broadcastKickBot()
+        }.let { events ->
+            assertEquals(5, events.size)
+            assertIsInstance<BotLeaveEvent.Active>(events[0]) {
+                assertEquals(1, group.id)
+            }
+            assertIsInstance<FriendDeleteEvent>(events[1]) {
+                assertEquals(2, friend.id)
+            }
+            assertIsInstance<StrangerRelationChangeEvent.Deleted>(events[2]) {
+                assertEquals(3, stranger.id)
+            }
+            assertIsInstance<MemberLeaveEvent>(events[3]) {
+                assertEquals(4, group.id)
+                assertEquals(5, member.id)
+            }
+            assertIsInstance<BotLeaveEvent.Kick>(events[4]) {
+                assertEquals(6, group.id)
+                assertEquals(7, operator.id)
+            }
+        }
+    }
+
+    @Test
+    internal fun testGroupFileV1() = runTest {
+        val fsroot = bot.addGroup(5417, "58aw").filesRoot
+        fsroot.resolve("helloworld.txt").uploadAndSend(
+            "HelloWorld".toByteArray().toExternalResource().toAutoCloseable()
+        )
+        assertEquals(1, fsroot.listFilesCollection().size)
+        assertEquals(
+            "HelloWorld",
+            fsroot.resolve("helloworld.txt")
+                .getDownloadInfo()!!
+                .url.toUrl()
+                .also { println("Mock file url: $it") }
+                .readText()
+        )
+        fsroot.resolve("helloworld.txt").delete()
+        assertEquals(0, fsroot.listFilesCollection().size)
+    }
+}