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
src/test/kotlin/RunTerminal.kt
/api_key.txt

View File

@ -1,15 +1,20 @@
plugins {
val kotlinVersion = "1.5.10"
val kotlinVersion = "1.6.21"
kotlin("jvm") 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"
version = "0.1.1"
group = "top.jie65535"
version = "1.0.0"
repositories {
maven("https://maven.aliyun.com/repository/public")
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 top.jie65535.jcf.model.Category
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.request.*
import top.jie65535.jcf.model.request.SortOrder.*
@ -46,8 +48,54 @@ class CurseforgeApi(apiKey: String) {
//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
@ -137,6 +185,7 @@ class CurseforgeApi(apiKey: String) {
suspend fun getMods(modIds: IntArray): Array<Mod> {
return json.decodeFromString<GetModsResponse>(
http.post("/v1/mods") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetModsByIdsListRequestBody(modIds))
}
).data
@ -147,11 +196,12 @@ class CurseforgeApi(apiKey: String) {
*/
suspend fun getFeaturedMods(
gameId: Int,
excludedModIds: IntArray,
excludedModIds: IntArray = intArrayOf(),
gameVersionTypeId: Int? = null
): FeaturedModsResponse {
return json.decodeFromString<GetFeaturedModsResponse>(
http.get("/v1/mods/featured") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetFeaturedModsRequestBody(gameId, excludedModIds, gameVersionTypeId))
}
).data
@ -207,6 +257,7 @@ class CurseforgeApi(apiKey: String) {
suspend fun getFiles(fileIds: IntArray): Array<File> {
return json.decodeFromString<GetFilesResponse>(
http.post("/v1/mods/files") {
headers.append("Content-Type", "application/json")
body = json.encodeToString(GetModFilesRequestBody(fileIds))
}
).data

View File

@ -24,17 +24,17 @@ class Category(
/**
* The category slug as it appear in the URL
*/
val slug: String,
val slug: String?,
/**
* The category URL
*/
val url: String,
val url: String?,
/**
* URL for the category icon
*/
val iconUrl: String,
val iconUrl: String?,
/**
* Last modified date of the category
@ -45,20 +45,20 @@ class Category(
/**
* 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
*/
val classId: Int?,
val classId: Int? = null,
/**
* The parent category for this category
*/
val parentCategoryId: Int?,
val parentCategoryId: Int? = null,
/**
* 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
*/
val gameVersionTypeId: Int?
val gameVersionTypeId: Int? = null
)

View File

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

View File

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

View File

@ -12,5 +12,5 @@ enum class FileRelationType {
Incompatible,
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,
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,
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),
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
*/
val slug: String,
val slug: String?,
/**
* Relevant links for the mod such as Issue tracker and Wiki
@ -67,7 +67,7 @@ class Mod(
/**
* The class id this mod belongs to
*/
val classId: Int?,
val classId: Int? = null,
/**
* List of the mod's authors
@ -120,7 +120,7 @@ class Mod(
/**
* Is mod allowed to be distributed
*/
val allowModDistribution: Boolean?,
val allowModDistribution: Boolean? = null,
/**
* The mod popularity rank for the game

View File

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

View File

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

View File

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

View File

@ -12,5 +12,5 @@ enum class ModLoaderType {
Fabric,
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,
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(
val gameId: Int,
val excludedModIds: IntArray,
val gameVersionTypeId: Int?,
val gameVersionTypeId: Int? = null,
)

View File

@ -14,5 +14,5 @@ enum class ModsSearchSortField {
Category,
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
@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> {
override val descriptor: SerialDescriptor =
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)
override fun deserialize(decoder: Decoder): T =
requireNotNull(enumValues<T>().getOrNull(decoder.decodeInt())) {
"index: ${decoder.decodeInt()} not in ${enumValues<T>()}"
}
values[decoder.decodeInt() - offset]
}
}

View File

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