diff --git a/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateChecker.kt b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateChecker.kt new file mode 100644 index 000000000..0d0fa13a6 --- /dev/null +++ b/boat-quay/boat-quay-rules/src/main/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateChecker.kt @@ -0,0 +1,74 @@ +package com.backbase.oss.boat.quay.ruleset + +import org.zalando.zally.core.toJsonPointer +import org.zalando.zally.rule.api.* +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException + +@Rule( + ruleSet = BoatRuleSet::class, + id = "B015", + severity = Severity.MUST, + title = "Check x-sunset-date is set and valid when x-deprecated is true" +) +class InfoBlockSunsetDateChecker { + + @Check(Severity.MUST) + fun validate(context: Context): List { + val violations = mutableListOf() + val extensions = context.api.info.extensions ?: return emptyList() + + val isDeprecated = isTrue(extensions["x-deprecated"]) + val sunsetDateValue = extensions["x-sunset-date"] + val hasSunsetDate = sunsetDateValue != null && sunsetDateValue.toString().isNotBlank() + + when { + isDeprecated && !hasSunsetDate -> { + violations.add( + context.violation( + "x-sunset-date must be set (as YYYY-MM-DD) when x-deprecated is true", + "/openapi/info/x-sunset-date".toJsonPointer() + ) + ) + } + isDeprecated && hasSunsetDate -> { + if (!isValidDate(sunsetDateValue.toString())) { + violations.add( + context.violation( + "x-sunset-date must be a valid YYYY-MM-DD date, got: ${sunsetDateValue}", + "/openapi/info/x-sunset-date".toJsonPointer() + ) + ) + } + } + !isDeprecated && hasSunsetDate -> { + violations.add( + context.violation( + "x-sunset-date is set but x-deprecated is not true — remove the stale sunset date or mark the API deprecated", + "/openapi/info/x-sunset-date".toJsonPointer() + ) + ) + } + } + + return violations + } + + private fun isTrue(value: Any?): Boolean { + return when (value) { + is Boolean -> value + is String -> value.equals("true", ignoreCase = true) + else -> false + } + } + + private fun isValidDate(value: String): Boolean { + return try { + LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE) + true + } catch (e: DateTimeParseException) { + false + } + } +} diff --git a/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateCheckerTest.kt b/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateCheckerTest.kt new file mode 100644 index 000000000..5d85ed1e3 --- /dev/null +++ b/boat-quay/boat-quay-rules/src/test/kotlin/com/backbase/oss/boat/quay/ruleset/InfoBlockSunsetDateCheckerTest.kt @@ -0,0 +1,193 @@ +package com.backbase.oss.boat.quay.ruleset + +import com.backbase.oss.boat.quay.ruleset.test.ZallyAssertions +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test +import org.zalando.zally.core.DefaultContextFactory + +class InfoBlockSunsetDateCheckerTest { + + private val cut = InfoBlockSunsetDateChecker() + + @Test + fun `x-deprecated true with valid x-sunset-date passes`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: true + x-sunset-date: "2027-04-15" + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isEmpty() + } + + @Test + fun `x-deprecated true without x-sunset-date fails`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: true + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isNotEmpty + } + + @Test + fun `x-deprecated true with empty x-sunset-date fails`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: true + x-sunset-date: "" + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isNotEmpty + } + + @Test + fun `x-deprecated true with malformed x-sunset-date fails`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: true + x-sunset-date: "2027.04" + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isNotEmpty + } + + @Test + fun `x-deprecated false without x-sunset-date passes`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: false + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isEmpty() + } + + @Test + fun `no x-deprecated without x-sunset-date passes`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isEmpty() + } + + @Test + fun `x-deprecated false with x-sunset-date fails`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-deprecated: false + x-sunset-date: "2027-04-15" + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isNotEmpty + } + + @Test + fun `no x-deprecated with x-sunset-date fails`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + x-sunset-date: "2027-04-15" + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isNotEmpty + } + + @Test + fun `no extensions passes`() { + @Language("YAML") + val context = DefaultContextFactory().getOpenApiContext( + """ + openapi: 3.0.3 + info: + title: API + version: 1.0.0 + """.trimIndent() + ) + + val violations = cut.validate(context) + + ZallyAssertions + .assertThat(violations) + .isEmpty() + } +}