Added Curseforge Api Tests

Upgraded kotlin version to 1.6.21
Upgraded mirai console version to 2.11.1
This commit is contained in:
2022-06-17 23:41:26 +08:00
parent be20a22fe9
commit 0558bbfd07
32 changed files with 356 additions and 49 deletions

1
.gitignore vendored
View File

@ -119,3 +119,4 @@ run/
# Local Test Launch point # Local Test Launch point
src/test/kotlin/RunTerminal.kt src/test/kotlin/RunTerminal.kt
/api_key.txt

View File

@ -1,15 +1,20 @@
plugins { plugins {
val kotlinVersion = "1.5.10" val kotlinVersion = "1.6.21"
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion
id("net.mamoe.mirai-console") version "2.7.0" id("net.mamoe.mirai-console") version "2.11.1"
} }
group = "me.jie65535" group = "top.jie65535"
version = "0.1.1" version = "1.0.0"
repositories { repositories {
maven("https://maven.aliyun.com/repository/public") maven("https://maven.aliyun.com/repository/public")
mavenCentral() mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2")
} }

View File

@ -9,6 +9,8 @@ import kotlinx.serialization.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import top.jie65535.jcf.model.Category import top.jie65535.jcf.model.Category
import top.jie65535.jcf.model.file.File import top.jie65535.jcf.model.file.File
import top.jie65535.jcf.model.game.Game
import top.jie65535.jcf.model.game.GameVersionType
import top.jie65535.jcf.model.mod.* import top.jie65535.jcf.model.mod.*
import top.jie65535.jcf.model.request.* import top.jie65535.jcf.model.request.*
import top.jie65535.jcf.model.request.SortOrder.* import top.jie65535.jcf.model.request.SortOrder.*
@ -46,8 +48,54 @@ class CurseforgeApi(apiKey: String) {
//region - Game - //region - Game -
// Minecraft Game ID is 432 /**
// Ignore Game APIs * Get all games that are available to the provided API key.
*/
suspend fun getGames(index: Int? = null, pageSize: Int? = null): GetGamesResponse {
return json.decodeFromString(
http.get("/v1/games") {
parameter("index", index)
parameter("pageSize", pageSize)
}
)
}
/**
* Get a single game. A private game is only accessible by its respective API key.
*/
suspend fun getGame(gameId: Int): Game {
return json.decodeFromString<GetGameResponse>(
http.get("/v1/games/$gameId")
).data
}
/**
* Get all available versions for each known version type of the specified game.
* A private game is only accessible to its respective API key.
*/
suspend fun getVersions(gameId: Int): Array<GameVersionsByType> {
return json.decodeFromString<GetVersionsResponse>(
http.get("/v1/games/$gameId/versions")
).data
}
/**
* Get all available version types of the specified game.
*
* A private game is only accessible to its respective API key.
*
* Currently, when creating games via the CurseForge Core Console,
* you are limited to a single game version type.
* This means that this endpoint is probably not useful in most cases
* and is relevant mostly when handling existing games that have
* multiple game versions such as World of Warcraft and Minecraft
* (e.g. 517 for wow_retail).
*/
suspend fun getVersionTypes(gameId: Int): Array<GameVersionType> {
return json.decodeFromString<GetVersionTypesResponse>(
http.get("/v1/games/$gameId/versions")
).data
}
//endregion //endregion
@ -137,6 +185,7 @@ class CurseforgeApi(apiKey: String) {
suspend fun getMods(modIds: IntArray): Array<Mod> { suspend fun getMods(modIds: IntArray): Array<Mod> {
return json.decodeFromString<GetModsResponse>( return json.decodeFromString<GetModsResponse>(
http.post("/v1/mods") { http.post("/v1/mods") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetModsByIdsListRequestBody(modIds)) body = json.encodeToString(GetModsByIdsListRequestBody(modIds))
} }
).data ).data
@ -147,11 +196,12 @@ class CurseforgeApi(apiKey: String) {
*/ */
suspend fun getFeaturedMods( suspend fun getFeaturedMods(
gameId: Int, gameId: Int,
excludedModIds: IntArray, excludedModIds: IntArray = intArrayOf(),
gameVersionTypeId: Int? = null gameVersionTypeId: Int? = null
): FeaturedModsResponse { ): FeaturedModsResponse {
return json.decodeFromString<GetFeaturedModsResponse>( return json.decodeFromString<GetFeaturedModsResponse>(
http.get("/v1/mods/featured") { http.get("/v1/mods/featured") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetFeaturedModsRequestBody(gameId, excludedModIds, gameVersionTypeId)) body = json.encodeToString(GetFeaturedModsRequestBody(gameId, excludedModIds, gameVersionTypeId))
} }
).data ).data
@ -207,6 +257,7 @@ class CurseforgeApi(apiKey: String) {
suspend fun getFiles(fileIds: IntArray): Array<File> { suspend fun getFiles(fileIds: IntArray): Array<File> {
return json.decodeFromString<GetFilesResponse>( return json.decodeFromString<GetFilesResponse>(
http.post("/v1/mods/files") { http.post("/v1/mods/files") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetModFilesRequestBody(fileIds)) body = json.encodeToString(GetModFilesRequestBody(fileIds))
} }
).data ).data

View File

@ -24,17 +24,17 @@ class Category(
/** /**
* The category slug as it appear in the URL * The category slug as it appear in the URL
*/ */
val slug: String, val slug: String?,
/** /**
* The category URL * The category URL
*/ */
val url: String, val url: String?,
/** /**
* URL for the category icon * URL for the category icon
*/ */
val iconUrl: String, val iconUrl: String?,
/** /**
* Last modified date of the category * Last modified date of the category
@ -45,20 +45,20 @@ class Category(
/** /**
* A top level category for other categories * A top level category for other categories
*/ */
val isClass: Boolean?, val isClass: Boolean? = null,
/** /**
* The class id of the category, meaning - the class of which this category is under * The class id of the category, meaning - the class of which this category is under
*/ */
val classId: Int?, val classId: Int? = null,
/** /**
* The parent category for this category * The parent category for this category
*/ */
val parentCategoryId: Int?, val parentCategoryId: Int? = null,
/** /**
* The display index for this category * The display index for this category
*/ */
val displayIndex: Int? val displayIndex: Int? = null
) )

View File

@ -30,5 +30,5 @@ class SortableGameVersion(
/** /**
* Game version type id * Game version type id
*/ */
val gameVersionTypeId: Int? val gameVersionTypeId: Int? = null
) )

View File

@ -59,7 +59,7 @@ class File(
/** /**
* The file download URL * The file download URL
*/ */
val downloadUrl: String, val downloadUrl: String?,
/** /**
* List of game versions this file is relevant for * List of game versions this file is relevant for
*/ */
@ -75,23 +75,23 @@ class File(
/** /**
* none * none
*/ */
val exposeAsAlternative: Boolean?, val exposeAsAlternative: Boolean? = null,
/** /**
* none * none
*/ */
val parentProjectFileId: Int?, val parentProjectFileId: Int? = null,
/** /**
* none * none
*/ */
val alternateFileId: Int?, val alternateFileId: Int? = null,
/** /**
* none * none
*/ */
val isServerPack: Boolean?, val isServerPack: Boolean? = null,
/** /**
* none * none
*/ */
val serverPackFileId: Int?, val serverPackFileId: Int? = null,
/** /**
* none * none
*/ */

View File

@ -8,6 +8,6 @@ class FileIndex(
val fileId: Int, val fileId: Int,
val filename: String, val filename: String,
val releaseType: FileReleaseType, val releaseType: FileReleaseType,
val gameVersionTypeId: Int?, val gameVersionTypeId: Int? = null,
val modLoader: ModLoaderType, val modLoader: ModLoaderType? = null,
) )

View File

@ -12,5 +12,5 @@ enum class FileRelationType {
Incompatible, Incompatible,
Include; Include;
internal object IndexSerializer : KSerializer<FileRelationType> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<FileRelationType> by EnumIndexSerializer(values())
} }

View File

@ -9,5 +9,5 @@ enum class FileReleaseType {
Beta, Beta,
Alpha; Alpha;
internal object IndexSerializer : KSerializer<FileReleaseType> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<FileReleaseType> by EnumIndexSerializer(values())
} }

View File

@ -21,5 +21,5 @@ enum class FileStatus{
AwaitingPublishing, AwaitingPublishing,
FailedPublishing; FailedPublishing;
internal object IndexSerializer : KSerializer<FileStatus> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<FileStatus> by EnumIndexSerializer(values())
} }

View File

@ -8,5 +8,5 @@ enum class HashAlgo(val value: Int) {
Sha1(1), Sha1(1),
Md5(2); Md5(2);
internal object IndexSerializer : KSerializer<HashAlgo> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<HashAlgo> by EnumIndexSerializer(values())
} }

View File

@ -0,0 +1,12 @@
package top.jie65535.jcf.model.game
import kotlinx.serialization.KSerializer
import top.jie65535.jcf.util.EnumIndexSerializer
@kotlinx.serialization.Serializable(with = CoreApiStatus.IndexSerializer::class)
enum class CoreApiStatus {
Private,
Public;
internal object IndexSerializer : KSerializer<CoreApiStatus> by EnumIndexSerializer(values())
}

View File

@ -0,0 +1,16 @@
package top.jie65535.jcf.model.game
import kotlinx.serialization.KSerializer
import top.jie65535.jcf.util.EnumIndexSerializer
@kotlinx.serialization.Serializable(with = CoreStatus.IndexSerializer::class)
enum class CoreStatus {
Draft,
Test,
PendingReview,
Rejected,
Approved,
Live;
internal object IndexSerializer : KSerializer<CoreStatus> by EnumIndexSerializer(values())
}

View File

@ -0,0 +1,17 @@
package top.jie65535.jcf.model.game
import kotlinx.serialization.Serializable
import top.jie65535.jcf.util.OffsetDateTimeSerializer
import java.time.OffsetDateTime
@Serializable
class Game(
val id: Int,
val name: String,
val slug: String?,
@Serializable(OffsetDateTimeSerializer::class)
val dateModified: OffsetDateTime,
val assets: GameAssets,
val status: CoreStatus,
val apiStatus: CoreApiStatus,
)

View File

@ -0,0 +1,8 @@
package top.jie65535.jcf.model.game
@kotlinx.serialization.Serializable
class GameAssets(
val iconUrl: String?,
val tileUrl: String?,
val coverUrl: String?,
)

View File

@ -0,0 +1,9 @@
package top.jie65535.jcf.model.game
@kotlinx.serialization.Serializable
class GameVersionType(
val id: Int? = null,
val gameId: Int? = null,
val name: String? = null,
val slug: String? = null,
)

View File

@ -27,7 +27,7 @@ class Mod(
/** /**
* The mod slug that would appear in the URL * The mod slug that would appear in the URL
*/ */
val slug: String, val slug: String?,
/** /**
* Relevant links for the mod such as Issue tracker and Wiki * Relevant links for the mod such as Issue tracker and Wiki
@ -67,7 +67,7 @@ class Mod(
/** /**
* The class id this mod belongs to * The class id this mod belongs to
*/ */
val classId: Int?, val classId: Int? = null,
/** /**
* List of the mod's authors * List of the mod's authors
@ -120,7 +120,7 @@ class Mod(
/** /**
* Is mod allowed to be distributed * Is mod allowed to be distributed
*/ */
val allowModDistribution: Boolean?, val allowModDistribution: Boolean? = null,
/** /**
* The mod popularity rank for the game * The mod popularity rank for the game

View File

@ -6,6 +6,6 @@ class ModAsset(
val modId: Int, val modId: Int,
val title: String, val title: String,
val description: String, val description: String,
val thumbnailUrl: String, val thumbnailUrl: String?,
val url: String, val url: String?,
) )

View File

@ -4,5 +4,5 @@ package top.jie65535.jcf.model.mod
class ModAuthor( class ModAuthor(
val id: Int, val id: Int,
val name: String, val name: String,
val url: String, val url: String?,
) )

View File

@ -2,8 +2,8 @@ package top.jie65535.jcf.model.mod
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class ModLinks( class ModLinks(
val websiteUrl: String, val websiteUrl: String?,
val wikiUrl: String, val wikiUrl: String?,
val issuesUrl: String, val issuesUrl: String?,
val sourceUrl: String val sourceUrl: String?
) )

View File

@ -12,5 +12,5 @@ enum class ModLoaderType {
Fabric, Fabric,
Quilt; Quilt;
internal object IndexSerializer : KSerializer<ModLoaderType> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<ModLoaderType> by EnumIndexSerializer(values())
} }

View File

@ -16,5 +16,5 @@ enum class ModStatus {
Deleted, Deleted,
UnderReview; UnderReview;
internal object IndexSerializer : KSerializer<ModStatus> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<ModStatus> by EnumIndexSerializer(values())
} }

View File

@ -4,5 +4,5 @@ package top.jie65535.jcf.model.request
class GetFeaturedModsRequestBody( class GetFeaturedModsRequestBody(
val gameId: Int, val gameId: Int,
val excludedModIds: IntArray, val excludedModIds: IntArray,
val gameVersionTypeId: Int?, val gameVersionTypeId: Int? = null,
) )

View File

@ -14,5 +14,5 @@ enum class ModsSearchSortField {
Category, Category,
GameVersion; GameVersion;
internal object IndexSerializer : KSerializer<ModsSearchSortField> by EnumIndexSerializer() internal object IndexSerializer : KSerializer<ModsSearchSortField> by EnumIndexSerializer(values())
} }

View File

@ -0,0 +1,7 @@
package top.jie65535.jcf.model.response
@kotlinx.serialization.Serializable
class GameVersionsByType(
val type: Int,
val versions: Array<String>
)

View File

@ -0,0 +1,8 @@
package top.jie65535.jcf.model.response
import top.jie65535.jcf.model.game.Game
@kotlinx.serialization.Serializable
class GetGameResponse(
val data: Game
)

View File

@ -0,0 +1,10 @@
package top.jie65535.jcf.model.response
import top.jie65535.jcf.model.game.Game
import top.jie65535.jcf.model.Pagination
@kotlinx.serialization.Serializable
class GetGamesResponse(
val data: Array<Game>,
val pagination: Pagination
)

View File

@ -0,0 +1,8 @@
package top.jie65535.jcf.model.response
import top.jie65535.jcf.model.game.GameVersionType
@kotlinx.serialization.Serializable
class GetVersionTypesResponse(
val data: Array<GameVersionType>
)

View File

@ -0,0 +1,6 @@
package top.jie65535.jcf.model.response
@kotlinx.serialization.Serializable
class GetVersionsResponse(
val data: Array<GameVersionsByType>
)

View File

@ -8,7 +8,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
@Suppress("FunctionName") @Suppress("FunctionName")
inline fun <reified T : Enum<T>> EnumIndexSerializer(offset: Int = 1): KSerializer<T> { inline fun <reified T : Enum<T>> EnumIndexSerializer(values: Array<T>, offset: Int = 1): KSerializer<T> {
return object : KSerializer<T> { return object : KSerializer<T> {
override val descriptor: SerialDescriptor = override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(T::class.qualifiedName!!, PrimitiveKind.INT) PrimitiveSerialDescriptor(T::class.qualifiedName!!, PrimitiveKind.INT)
@ -17,8 +17,6 @@ inline fun <reified T : Enum<T>> EnumIndexSerializer(offset: Int = 1): KSerializ
encoder.encodeInt(value.ordinal + offset) encoder.encodeInt(value.ordinal + offset)
override fun deserialize(decoder: Decoder): T = override fun deserialize(decoder: Decoder): T =
requireNotNull(enumValues<T>().getOrNull(decoder.decodeInt())) { values[decoder.decodeInt() - offset]
"index: ${decoder.decodeInt()} not in ${enumValues<T>()}"
}
} }
} }

View File

@ -6,9 +6,11 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
object OffsetDateTimeSerializer : KSerializer<OffsetDateTime> { object OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
@ -16,10 +18,16 @@ object OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
override val descriptor: SerialDescriptor = override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(OffsetDateTime::class.qualifiedName!!, PrimitiveKind.STRING) PrimitiveSerialDescriptor(OffsetDateTime::class.qualifiedName!!, PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): OffsetDateTime = override fun deserialize(decoder: Decoder): OffsetDateTime {
OffsetDateTime.parse(decoder.decodeString(), formatter) return try {
OffsetDateTime.parse(decoder.decodeString(), formatter)
} catch(ignore: Throwable) {
OffsetDateTime.MIN
}
}
override fun serialize(encoder: Encoder, value: OffsetDateTime) = override fun serialize(encoder: Encoder, value: OffsetDateTime) =
encoder.encodeString(formatter.format(value)) encoder.encodeString(formatter.format(value))
} }

View File

@ -0,0 +1,143 @@
package top.jie65535
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Test
import top.jie65535.jcf.CurseforgeApi
import top.jie65535.jcf.model.request.ModsSearchSortField
import top.jie65535.jcf.model.request.SortOrder
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
class CurseforgeApiTest {
companion object {
private const val GAME_ID_MINECRAFT = 432
private const val MOD_ID_JEI = 238222
private const val FILE_ID_JEI = 3835406
private const val CLASS_ID_WORLDS = 17
private const val CLASS_ID_BUKKIT_PLUGINS = 5
private const val CLASS_ID_CUSTOMIZATION = 4546
private const val CLASS_ID_MODPACKS = 4471
private const val CLASS_ID_RESOURCE_PACKS = 12
private const val CLASS_ID_ADDONS = 4559
private const val CLASS_ID_MODS = 6
}
// 从 api_key.txt 文件中读取
private val api = CurseforgeApi(File("api_key.txt").readText())
@Test
fun getGames() = runTest {
val games = api.getGames()
val game = assertNotNull(games.data.find { it.name == "Minecraft" })
assert(game.id == GAME_ID_MINECRAFT)
printResult("getGames", games)
}
@Test
fun getGame() = runTest {
val game = api.getGame(GAME_ID_MINECRAFT)
assertEquals(game.name, "Minecraft")
printResult("getGame", game)
}
@Test
fun getVersions() = runTest {
val versions = api.getVersions(GAME_ID_MINECRAFT)
printResult("getVersions", versions)
}
@Test
fun getVersionTypes() = runTest {
val versionTypes = api.getVersionTypes(GAME_ID_MINECRAFT)
printResult("getVersionTypes", versionTypes)
}
@Test
fun getCategories() = runTest {
val categories = api.getCategories(GAME_ID_MINECRAFT)
val classes = categories.filter { it.isClass == true }
for (gameClass in classes) {
println("[${gameClass.id}] ${gameClass.name}")
for (category in categories.filter { it.classId == gameClass.id }) {
println(" | [${category.id}] ${category.name}")
}
}
printResult("getCategories", categories)
}
@Test
fun searchMods() = runTest {
val response = api.searchMods(GAME_ID_MINECRAFT, searchFilter = "create", sortField = ModsSearchSortField.TotalDownloads, sortOrder = SortOrder.DESC, pageSize = 10)
for (mod in response.data) {
println("[${mod.id}] ${mod.name}\t ${mod.summary}\t DownloadCount: ${mod.downloadCount}")
}
printResult("searchMods", response)
}
@Test
fun getMod() = runTest {
val mod = api.getMod(MOD_ID_JEI)
assertEquals(mod.id, MOD_ID_JEI)
printResult("getMod", mod)
}
@Test
fun getMods() = runTest {
val mods = api.getMods(intArrayOf(MOD_ID_JEI))
assertEquals(mods.size, 1)
assertEquals(mods[0].id, MOD_ID_JEI)
printResult("getMods", mods)
}
@Test
fun getFeaturedMods() = runTest {
// Error: HTTP 404
val featuredMods = api.getFeaturedMods(GAME_ID_MINECRAFT)
printResult("getFeaturedMods", featuredMods)
}
@Test
fun getModDescription() = runTest {
val modDescription = api.getModDescription(MOD_ID_JEI)
printResult("getModDescription", modDescription)
}
@Test
fun getModFile() = runTest {
val modFile = api.getModFile(MOD_ID_JEI, FILE_ID_JEI)
printResult("getModFile", modFile)
}
@Test
fun getModFiles() = runTest {
val modFiles = api.getModFiles(GAME_ID_MINECRAFT)
printResult("getModFiles", modFiles)
}
@Test
fun getFiles() = runTest {
val files = api.getFiles(intArrayOf(FILE_ID_JEI))
printResult("getFiles", files)
}
@Test
fun getModFileChangelog() = runTest {
val modFileChangelog = api.getModFileChangelog(MOD_ID_JEI, FILE_ID_JEI)
printResult("getModFileChangelog", modFileChangelog)
}
@Test
fun getModFileDownloadURL() = runTest {
val modFileDownloadUrl = api.getModFileDownloadURL(MOD_ID_JEI, FILE_ID_JEI)
printResult("getModFileDownloadURL", modFileDownloadUrl)
}
private inline fun <reified T> printResult(name: String, obj: T) {
println("$name result: ${Json.encodeToString(obj)}")
}
}