diff --git a/src/main/kotlin/dev/hossain/githubstats/service/IssueSearchPagerService.kt b/src/main/kotlin/dev/hossain/githubstats/service/IssueSearchPagerService.kt index 326d557..ad97ae5 100644 --- a/src/main/kotlin/dev/hossain/githubstats/service/IssueSearchPagerService.kt +++ b/src/main/kotlin/dev/hossain/githubstats/service/IssueSearchPagerService.kt @@ -5,6 +5,7 @@ import dev.hossain.githubstats.logging.Log import dev.hossain.githubstats.model.Issue import dev.hossain.githubstats.model.IssueSearchResult import dev.hossain.githubstats.service.GithubApiService.Companion.DEFAULT_PAGE_SIZE +import dev.hossain.githubstats.util.ErrorInfo import dev.hossain.githubstats.util.ErrorProcessor import kotlinx.coroutines.delay import kotlin.math.ceil @@ -33,8 +34,14 @@ class IssueSearchPagerService constructor( size = pageSize, ) } catch (exception: Exception) { - val errorInfo = errorProcessor.getDetailedError(exception) - throw errorInfo.exception + val errorInfo: ErrorInfo = errorProcessor.getDetailedError(exception) + + if (errorInfo.isUserNotFound()) { + Log.w("❌ User not found. Skipping the PR list search for the user.\n") + return emptyList() + } else { + throw errorInfo.exception + } } val totalItemCount: Int = issueSearchResult.total_count diff --git a/src/main/kotlin/dev/hossain/githubstats/util/ErrorInfo.kt b/src/main/kotlin/dev/hossain/githubstats/util/ErrorInfo.kt index 5023993..eb74ec3 100644 --- a/src/main/kotlin/dev/hossain/githubstats/util/ErrorInfo.kt +++ b/src/main/kotlin/dev/hossain/githubstats/util/ErrorInfo.kt @@ -11,7 +11,9 @@ data class ErrorInfo( val exception: Exception, val debugGuideMessage: String = "", val githubError: GithubError? = null, -) +) { + fun isUserNotFound(): Boolean = ErrorProcessor.isUserMissingError(githubError) +} /** * Error threshold information. @@ -24,13 +26,15 @@ data class ErrorThreshold( /** * Error response from GitHub API. * - * Sample error message. + * Sample error messages. * ```json * {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"} * * {"message":"Validation Failed","errors":[{"resource":"Issue","code":"missing_field","field":"title"}],"documentation_url":"https://docs.github.com/rest/reference/issues#create-an-issue"} * * {"message":"Not Found","documentation_url":"https://docs.github.com/rest/pulls/pulls#get-a-pull-request","status":"404"} + * + * {"message":"Validation Failed","errors":[{"message":"The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.","resource":"Search","field":"q","code":"invalid"}],"documentation_url":"https://docs.github.com/v3/search/","status":"422"} * ``` */ @JsonClass(generateAdapter = true) @@ -38,4 +42,26 @@ data class GithubError( @Json(name = "message") val message: String, @Json(name = "documentation_url") val documentationUrl: String, @Json(name = "status") val status: Int? = null, + @Json(name = "errors") val errors: List = emptyList(), +) + +/** + * Error details from GitHub API. + * + * Example error detail: + * ```json + * { + * "resource": "Search", + * "code": "invalid", + * "message": "The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.", + * "field": "q" + * } + * ``` + */ +@JsonClass(generateAdapter = true) +data class GithubErrorDetail( + @Json(name = "resource") val resource: String, + @Json(name = "code") val code: String, + @Json(name = "field") val field: String, + @Json(name = "message") val message: String, ) diff --git a/src/main/kotlin/dev/hossain/githubstats/util/ErrorProcessor.kt b/src/main/kotlin/dev/hossain/githubstats/util/ErrorProcessor.kt index 49c03dc..484a465 100644 --- a/src/main/kotlin/dev/hossain/githubstats/util/ErrorProcessor.kt +++ b/src/main/kotlin/dev/hossain/githubstats/util/ErrorProcessor.kt @@ -21,6 +21,36 @@ class ErrorProcessor { * ``` */ private const val TOKEN_ERROR_MESSAGE = "Bad credentials" + + /** + * Error message when search query is invalid. + * + * Sample error message. + * ```json + * {"message":"Validation Failed","errors":[{"message":"The listed users cannot be searched.","resource":"Search","field":"q","code":"invalid"}],"documentation_url":"https://docs.github.com/v3/search/","status":"422"} + * ``` + */ + private const val VALIDATION_FAILED_ERROR_MESSAGE = "Validation Failed" + + /** + * Resource type for search error. + */ + private const val RESOURCE_TYPE_SEARCH = "Search" + + /** + * Check if user is missing in the search query. + */ + fun isUserMissingError(githubError: GithubError?): Boolean { + if (githubError == null || githubError.message != VALIDATION_FAILED_ERROR_MESSAGE) { + return false + } + + return githubError.errors.any { + it.resource == RESOURCE_TYPE_SEARCH && + // Yes, hardcoding the server message string to avoid any false positive + it.message.contains("users cannot be searched") + } + } } /** diff --git a/src/test/kotlin/dev/hossain/githubstats/util/ErrorProcessorTest.kt b/src/test/kotlin/dev/hossain/githubstats/util/ErrorProcessorTest.kt index 9b04916..ff81c0c 100644 --- a/src/test/kotlin/dev/hossain/githubstats/util/ErrorProcessorTest.kt +++ b/src/test/kotlin/dev/hossain/githubstats/util/ErrorProcessorTest.kt @@ -117,4 +117,72 @@ class ErrorProcessorTest { assertThat(errorInfo.errorMessage).contains("HTTP 404") assertThat(errorInfo.githubError).isNull() } + + @Test + fun `getDetailedError - given HttpException with JSON github errors list - returns ErrorInfo processed data with errors`() { + // language=JSON + val jsonErrorBody = + """ + { + "message": "Validation Failed", + "errors": [ + { + "message": "The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.", + "resource": "Search", + "field": "q", + "code": "invalid" + } + ], + "documentation_url": "https://docs.github.com/v3/search/", + "status": "422" + } + """.trimIndent() + val httpException = HttpException(Response.error(422, jsonErrorBody.toResponseBody("application/json".toMediaTypeOrNull()))) + val errorProcessor = ErrorProcessor() + + val errorInfo = errorProcessor.getDetailedError(httpException) + + assertThat(errorInfo).isInstanceOf(ErrorInfo::class.java) + assertThat(errorInfo.githubError?.message).isEqualTo("Validation Failed") + assertThat(errorInfo.githubError?.status).isEqualTo(422) + assertThat(errorInfo.githubError?.errors).isNotEmpty() + + val githubErrorDetail = errorInfo.githubError?.errors?.get(0)!! + assertThat( + githubErrorDetail.message, + ).isEqualTo( + "The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.", + ) + assertThat(githubErrorDetail.resource).isEqualTo("Search") + assertThat(githubErrorDetail.field).isEqualTo("q") + assertThat(githubErrorDetail.code).isEqualTo("invalid") + } + + @Test + fun `getDetailedError - given HttpException with JSON github error user not found - validates user not found`() { + // language=JSON + val jsonErrorBody = + """ + { + "message": "Validation Failed", + "errors": [ + { + "message": "The listed users cannot be searched either because the users do not exist or you do not have permission to view the users.", + "resource": "Search", + "field": "q", + "code": "invalid" + } + ], + "documentation_url": "https://docs.github.com/v3/search/", + "status": "422" + } + """.trimIndent() + val httpException = HttpException(Response.error(422, jsonErrorBody.toResponseBody("application/json".toMediaTypeOrNull()))) + val errorProcessor = ErrorProcessor() + + val errorInfo = errorProcessor.getDetailedError(httpException) + + assertThat(errorInfo).isInstanceOf(ErrorInfo::class.java) + assertThat(ErrorProcessor.isUserMissingError(errorInfo.githubError)).isTrue() + } }