I started with a boilerplate http4k project genera...
# http4k
a
I started with a boilerplate http4k project generated by the Project Wizard. I have a Web App user interface that needs to make REST calls to the http4k backend. This initially failed because CORS is required. As a result, I added the CORS Filter to the http4k project to get the Web App REST calls working. Alas, while CORS enabled the Web App UI to talk to the http4k server, this broke the simple Unit Test. I get an error like:
Copy code
Ping test() FAILED
    org.opentest4j.AssertionFailedError: expected: <HTTP/1.1 200 OK
    access-control-allow-origin: null
    access-control-allow-headers: 
    access-control-allow-methods: GET, POST

    pong> but was: <HTTP/1.1 200 OK


    pong>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
        at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1142)
        at app//com.perusal.Perusalhttp4kTest.Ping test(Perusalhttp4kTest.kt:14)

1 test completed, 1 failed
How can I enable CORS but also make the Unit Test work? I tried adding headers like:
Copy code
.header("access-control-request-method", "GET")
              .header("origin", "<http://localhost:3000>"),
but I get a similar failure:
Copy code
Ping test() FAILED
    org.opentest4j.AssertionFailedError: expected: <HTTP/1.1 200 OK
    access-control-allow-origin: null
    access-control-allow-headers: 
    access-control-allow-methods: GET, POST
    access-control-request-method: GET
    origin: <http://localhost:3000>

    pong> but was: <HTTP/1.1 200 OK


    pong>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
        at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1142)
        at app//com.perusal.Perusalhttp4kTest.Ping test(Perusalhttp4kTest.kt:14)

1 test completed, 1 failed
d
Hi! Can you just post the content of your app and test (maybe in a gist so it can be reached asoly recreated?
I think for a start that the expected and actual are the wrong way around in the test case. We can definitely fix that easily if it's the case in the toolbox ( it would be our error) But also the cors filter would be interesting to see how you have set it up
(because the header values look a little weird)
a
Cors setup like:
Copy code
val corsPolicy = CorsPolicy(
    originPolicy = OriginPolicy.Only("<http://localhost:3000>"), // TODO Replace with the appropriate client origin(s)
    headers = emptyList(), // listOf("Content-Type", "Authorization"), // TODO Consider adding back Authorization
    methods = listOf(GET, POST /*, Method.PUT, Method.DELETE */) // TODO Double Check Completeness
)

val corsMiddleware = Cors(corsPolicy)

val app: HttpHandler = corsMiddleware.then(
    routes(
    "/ping" bind GET to {
        Response(OK).body("pong")
    },
Then, the test looks like:
Copy code
@Test
    fun `Ping test`() {
        assertEquals(app(
            Request(GET, "/ping"))
              .header("access-control-request-method", "GET")
              .header("origin", "<http://localhost:3000>"),
            Response(OK).body("pong"))
    }
(I added the header thinking I was consistent with CORS, but perhaps not.)
Note that in this simple case, the web app runs on localhost port 3000 talking to an http4k server on port 9000.
d
@Anthony Whitford Ah - I can see the problem. What's happening is that you're adding the headers in the wrong place. You should add the headers to the response and not the request
Copy code
@Test
    fun `Ping test`() {
        assertEquals(app(Request(GET, "/ping")),
            Response(OK).body("pong")
              .header("access-control-request-method", "GET")
              .header("origin", "<http://localhost:3000>")
           )
    }
a
Hmm... That seems to fail too:
Copy code
Ping test() FAILED
    org.opentest4j.AssertionFailedError: expected: <HTTP/1.1 200 OK
    access-control-allow-origin: null
    access-control-allow-headers: 
    access-control-allow-methods: GET, POST

    pong> but was: <HTTP/1.1 200 OK
    access-control-request-method: GET
    origin: <http://localhost:3000>

    pong>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
        at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
        at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1142)
        at app//com.perusal.Perusalhttp4kTest.Ping test(Perusalhttp4kTest.kt:13)

1 test completed, 1 failed
d
Both access-control-request-method and origin are request headers. they will never be issued by the server. I think you have copied the headers from the request instead of the response,
This works - not sure what you meant about the 3000/8000 difference though 🙂
Copy code
package org.http4k.connect.openai.auth.user

import org.http4k.core.HttpHandler
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.CorsPolicy
import org.http4k.filter.Only
import org.http4k.filter.OriginPolicy
import org.http4k.filter.ServerFilters
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class FooTest {

    @Test
    fun `test`() {
        val corsPolicy = CorsPolicy(
            originPolicy = OriginPolicy.Only("localhost:3000"), // TODO Replace with the appropriate client origin(s)
            headers = emptyList(), // listOf("Content-Type", "Authorization"), // TODO Consider adding back Authorization
            methods = listOf(GET /*, Method.PUT, Method.DELETE */) // TODO Double Check Completeness
        )

        val corsMiddleware = ServerFilters.Cors(corsPolicy)

        val app: HttpHandler = corsMiddleware.then(
            routes(
                "/ping" bind GET to { Response(OK).body("pong") }
            )
        )

        val expected: Response = Response(OK).body("pong")
            .header("access-control-allow-origin", "localhost:3000")
            .header("access-control-allow-headers", "")
            .header("access-control-allow-methods", "GET")

        val actual: Response = app(
            Request(GET, "/ping").header("origin", "localhost:3000")
        )

        assertEquals(expected, actual)

    }
}
a
Thank you @dave! I think I figured out a few issues with your help. Final code looks like:
Copy code
fun `Ping test`() {
        assertEquals(
          Response(OK).body("pong")
            .header("access-control-allow-origin", "localhost:3000")
            .header("access-control-allow-headers", "")
            .header("access-control-allow-methods", "GET")
          ,          
          app(
            Request(GET, "/ping")
              .header("origin", "localhost:3000")
          )
        )
    }
d
no worries. we've already fixed the toolbox so the Junit assertEquals (expected/actual) mix up shouldn't happen again for anyone else... it's a stupid API TBH 😂
a
Now the unit test is working, but my app is failing (from Chrome)...
Access to fetch at 'http://localhost:9000/jdbc' from origin 'http://localhost:3000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'null' that is not equal to the supplied origin. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Why would Chrome be sending null? 🤔
Fixed it.. Apparently I need to say http in the origin policy.
Spoke too soon... Had to make another tweak to the test:
Copy code
fun `Ping test`() {
        assertEquals(
          Response(OK).body("pong")
            .header("access-control-allow-origin", "null")
            .header("access-control-allow-headers", "")
            .header("access-control-allow-methods", "GET")
          ,          
          app(
            Request(GET, "/ping")
              .header("origin", "localhost:3000")
          )
        )
    }
Had to specify:
Copy code
.header("access-control-allow-origin", "null")
for expected.
Reflecting on this experience, this whole approach seems flawed. For example, if I add a
POST
entry point to my server, then the
access-control-allow-methods
value changes from
GET
to
GET, POST
-- this will require adjustment to the tests. Is it best practice to include the CORS policy for Unit Testing? I wonder if unit tests should simply circumvent the CORS policy.
d
well if depends on the level you are testing at. The nice thing about the model is that it gives you options - you can test at the endpoint or the in-memory server or the live server level. You can layer your app so your API is a "unit" and then just layer on filters or other levels on the outside for the runtime environment. For instance if you want to have your API accessed from several different ways with difference requirements:
Copy code
fun main() {
    val endpoint = "/foo" bind GET to { req: Request -> Response(OK).body("foobar") }
    val api = routes(endpoint)

    val app = routes(
        "/web" bind Cors(UnsafeGlobalPermissive).then(api),
        "/api" bind ServerFilters.BearerAuth("apiKey").then(api)
    )
    
    endpoint(Request(GET, "/foo"))
    api(Request(GET, "/foo"))
    
    app(Request(GET, "/web/foo"))
    app(Request(GET, "/app/foo"))

    JavaHttpClient()(Request(GET, "<http://localhost:9000>"))
}
everything is about layering, and you're in total control of what happens and how things are expsoed.
you should never circumvent any layers of security for testing - you just ned to pick the appopiiate kind of test! 🙂
because it's just composition, you can also just extract out the filter stack and test it separately:
Copy code
val incomingStack: Filter = DebuggingFilters.PrintRequestAndResponse()
        .then(Cors(UnsafeGlobalPermissive))
        .then(ResponseFilters.ReportHttpTransaction {
            println("${it.request} took ${it.duration}")
        })

    val forTestingStack = incomingStack.then { req: Request -> Response(OK) }
    val appWithStack = incomingStack.then(app)
277 Views