-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
714-rules-to-aos--AvoidLinkHeadersRule #763
Changes from all commits
0144fa6
5018e15
70ad4ab
14fc1f8
10f787b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,32 @@ | ||
package de.zalando.zally.rule.zalando | ||
|
||
import com.typesafe.config.Config | ||
import de.zalando.zally.rule.Context | ||
import de.zalando.zally.rule.HttpHeadersRule | ||
import de.zalando.zally.rule.api.Check | ||
import de.zalando.zally.rule.api.Rule | ||
import de.zalando.zally.rule.api.Severity | ||
import de.zalando.zally.rule.api.Violation | ||
import de.zalando.zally.rule.api.Rule | ||
import io.swagger.models.Swagger | ||
import de.zalando.zally.util.getAllHeaders | ||
import org.springframework.beans.factory.annotation.Autowired | ||
|
||
@Rule( | ||
ruleSet = ZalandoRuleSet::class, | ||
id = "166", | ||
severity = Severity.MUST, | ||
title = "Avoid Link in Header Rule" | ||
ruleSet = ZalandoRuleSet::class, | ||
id = "166", | ||
severity = Severity.MUST, | ||
title = "Avoid Link in Header Rule" | ||
) | ||
class AvoidLinkHeadersRule(@Autowired rulesConfig: Config) : HttpHeadersRule(rulesConfig) { | ||
private val description = "Do Not Use Link Headers with JSON entities" | ||
class AvoidLinkHeadersRule(@Autowired rulesConfig: Config) { | ||
|
||
@Check(severity = Severity.MUST) | ||
override fun validate(swagger: Swagger): Violation? { | ||
return super.validate(swagger) | ||
} | ||
private val headersWhitelist = rulesConfig.getStringList(HttpHeadersRule::class.simpleName + ".whitelist").toSet() | ||
|
||
override fun isViolation(header: String) = header == "Link" | ||
private val description = "Do Not Use Link Headers with JSON entities" | ||
|
||
override fun createViolation(paths: List<String>): Violation { | ||
return Violation(description, paths) | ||
@Check(severity = Severity.MUST) | ||
fun validate(context: Context): List<Violation> { | ||
val allHeaders = context.api.getAllHeaders() | ||
return allHeaders | ||
.filter { it.name !in headersWhitelist && it.name == "Link" } | ||
.map { context.violation(description, it.element) } // createViolation(context, it) } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package de.zalando.zally.util | ||
|
||
import io.swagger.v3.oas.models.OpenAPI | ||
import io.swagger.v3.oas.models.parameters.Parameter | ||
import io.swagger.v3.oas.models.responses.ApiResponse | ||
|
||
data class HeaderElement( | ||
val name: String, | ||
val element: Any | ||
) | ||
|
||
fun OpenAPI.getAllHeaders(): Set<HeaderElement> { | ||
|
||
fun Collection<Parameter>?.extractHeaders() = orEmpty() | ||
.filter { it.`in` == "header" } | ||
.map { HeaderElement(it.name, it) } | ||
.toSet() | ||
|
||
fun Collection<ApiResponse>?.extractHeaders() = orEmpty() | ||
.flatMap { it.headers.orEmpty().entries } | ||
.map { HeaderElement(it.key, it.value) } | ||
.toSet() | ||
|
||
val fromParams = components.parameters.orEmpty().values.extractHeaders() | ||
|
||
val fromPaths = paths.orEmpty().flatMap { (_, path) -> | ||
val fromPathParameters = path.parameters.extractHeaders() | ||
val fromOperations = path.readOperations().flatMap { operation -> | ||
val fromOpParams = operation.parameters.extractHeaders() | ||
val fromOpResponses = operation.responses.orEmpty().values.extractHeaders() | ||
fromOpParams + fromOpResponses | ||
} | ||
fromPathParameters + fromOperations | ||
} | ||
|
||
return fromParams + fromPaths | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,135 @@ | ||
package de.zalando.zally.rule.zalando | ||
|
||
import de.zalando.zally.getFixture | ||
import de.zalando.zally.rule.Context | ||
import de.zalando.zally.rule.ZallyAssertions.Companion.assertThat | ||
import de.zalando.zally.testConfig | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.intellij.lang.annotations.Language | ||
import org.junit.Test | ||
|
||
class AvoidLinkHeadersRuleTest { | ||
|
||
private val rule = AvoidLinkHeadersRule(testConfig) | ||
|
||
@Test | ||
fun positiveCaseSpp() { | ||
val swagger = getFixture("api_spp.json") | ||
assertThat(rule.validate(swagger)).isNull() | ||
fun `a Swagger API with no header called Link produces no violation`() { | ||
@Language("YAML") | ||
val context = Context.createSwaggerContext(""" | ||
swagger: 2.0 | ||
info: | ||
title: Clean Swagger API | ||
paths: | ||
/foo: | ||
get: | ||
description: Lorem Ipsum | ||
responses: | ||
202: | ||
description: Lorem Ipsum | ||
headers: | ||
Location: # should not violate since not called `Link` | ||
type: string | ||
format: url | ||
parameters: | ||
FlowId: # should not violate since not named `Link` | ||
name: X-Flow-Id | ||
in: header | ||
type: string | ||
Link: # should not violate since not a header | ||
name: Link | ||
in: query | ||
type: string | ||
ProductId: # should not violate since not a header nor named `Link` | ||
name: product_id | ||
in: path | ||
type: string | ||
""".trimIndent(), failOnParseErrors = true)!! | ||
val violations = rule.validate(context) | ||
assertThat(violations).isEmpty() | ||
} | ||
|
||
@Test | ||
fun positiveCaseSpa() { | ||
val swagger = getFixture("api_spa.yaml") | ||
assertThat(rule.validate(swagger)).isNull() | ||
fun `an OpenAPI 3 API with no header called Link produces no violation`() { | ||
@Language("YAML") | ||
val context = Context.createOpenApiContext(""" | ||
openapi: 3.0.0 | ||
info: | ||
title: Clean Swagger API | ||
version: 1.0.0 | ||
paths: | ||
/foo: | ||
get: | ||
description: Lorem Ipsum | ||
responses: | ||
202: | ||
description: Lorem Ipsum | ||
headers: | ||
Location: | ||
schema: | ||
type: string | ||
format: url | ||
components: | ||
parameters: | ||
FlowId: | ||
name: X-Flow-Id | ||
in: header | ||
required: false | ||
schema: | ||
type: string | ||
Authorization: | ||
name: Authorization | ||
in: header | ||
required: true | ||
schema: | ||
type: string | ||
ProductId: | ||
name: product_id | ||
in: path | ||
required: true | ||
schema: | ||
type: string | ||
""".trimIndent(), failOnParseErrors = true)!! | ||
val violations = rule.validate(context) | ||
assertThat(violations).isEmpty() | ||
} | ||
|
||
@Test | ||
fun negativeCase() { | ||
val swagger = getFixture("avoidLinkHeaderRuleInvalid.json") | ||
val violation = rule.validate(swagger)!! | ||
assertThat(violation.paths).hasSameElementsAs( | ||
listOf("/product-put-requests/{product_path} Link", "/products Link")) | ||
fun `an API with Link headers causes violations`() { | ||
@Language("YAML") | ||
val context = Context.createSwaggerContext(""" | ||
swagger: 2.0 | ||
info: | ||
title: Clean Swagger API | ||
paths: | ||
/foo: | ||
get: | ||
parameters: | ||
- name: Authorization | ||
in: header | ||
type: string | ||
- name: Link | ||
in: header | ||
type: string | ||
responses: | ||
202: | ||
description: Lorem Ipsum | ||
headers: | ||
Location: | ||
type: string | ||
format: url | ||
post: | ||
responses: | ||
202: | ||
description: Lorem Ipsum | ||
headers: | ||
Link: | ||
type: string | ||
format: url | ||
""".trimIndent(), failOnParseErrors = true)!! | ||
val violations = rule.validate(context) | ||
assertThat(violations) | ||
.descriptionsAllEqualTo("Do Not Use Link Headers with JSON entities") | ||
.pointersEqualTo( | ||
"/paths/~1foo/get/parameters/1", | ||
"/paths/~1foo/post/responses/202/headers/Link" | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,7 +120,7 @@ class MediaTypesRuleTest { | |
|
||
@Test | ||
fun `the SPP API generates violations`() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would replace tests based on a complete API specification with test cases describing the behavior of the check functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has not changed since the PR for this rule. Only the |
||
val context = getContextFromFixture("api_spp.json")!! | ||
val context = getContextFromFixture("api_spp.json") | ||
val result = rule.validate(context) | ||
assertThat(result).hasSameElementsAs(listOf( | ||
// --- consumes --- | ||
|
@@ -137,7 +137,7 @@ class MediaTypesRuleTest { | |
|
||
@Test | ||
fun `the SPA API generates no violations`() { | ||
val context = getContextFromFixture("api_spa.yaml")!! | ||
val context = getContextFromFixture("api_spa.yaml") | ||
assertThat(rule.validate(context)).isEmpty() | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can have an even smaller example, like:
BTW, the
parameters
are wrongly intended in your example (should be underget:
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those parameters are for all of the API, not just for that path/verb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted the example to include:
to make sure the test does not return false negatives.
I can remove one of the header parameter for shortening it.
I can also add one that is called
Link
but is not a header, again to shield against false negatives in the test.