Sup, having a testing problem because of SPA `"/bo...
# http4k
r
Sup, having a testing problem because of SPA
"/boards" / idLens / "lists" meta ....
(api/boards/:id/lists) this is my route where idLens should be an integer, before implementing the SPA code, a test like this would pass:
Copy code
@Test
    fun `create list with invalid board ID`() {
        val list = BoardList(1337, 1, "Test List", 1)
        val req = client(Request(POST, "$uri/boards/abc/lists").body(Json.encodeToString(list)))
        assertEquals(Status.NOT_FOUND, req.status) // req should return NOT_FOUND because abc is not an integer
    }
however, after adding the spa into my routes
Copy code
val handler = routes(
        "/api" bind filter.then(apiContract),

        singlePageApp(ResourceLoader.Directory("static-content")),
Now the test fails, because the request returns OK because it returns a html page??? How should I fix this?
a
What's the value of
uri
? Your request url must include the
/api
path to be routed to the
apiContract
.
r
Yeah it does:
private val uri = "<http://localhost>:${server.port()}/api"
The problem is looks like the route is not found on the API but the SPA returns something.. 🤔
a
So, the SPA is obviously greedy by design, but the odd thing is that you'd think the
/api
handler would have precedence in
routes
. What happens if you reverse the order of the api and spa in
routes
?
r
Same result, page from the SPA
with
singlePageApp(ResourceLoader.Directory("static-content"))
Without:
a
Let me see if I can replicate this behaviour...
r
I've also tried
"/site" bind singlePageApp(ResourceLoader.Directory("static-content")),
but still same result 🤔
a
That's a bizarre development.
So far, I've been unable to reproduce the problem with
Copy code
class RoutingTest {
    @Test
    fun `test greedy spa`() {
        val handler = routes(
            "/api" bind routes(
                "/bar" bind Method.GET to {
                    Response(Status.OK).body("hi")
                }
            ),
            singlePageApp(ResourceLoader.Classpath())
        )

        val resp = handler(Request(Method.GET, "/api/bar"))
        resp.shouldHaveStatus(Status.OK)
        resp.shouldHaveBody("hi")
    }
}
Your test seems to be running against a real server though, right?
The real server doesn't seem to make a difference. Think you could distil the problem down to the minimal reproducible code to share?
r
Yes, give me one second
a
Actually, I've reproduced it. The path lens is the issue. The router sees the URI doesn't satisfy the path lens, so it never matches the handler you want it to hit.
I think you might have to make the path lens accept String
r
Copy code
val contractRoutes: List<ContractRoute> = listOf(
    "/foo" / idLens / "bar" bindContract Method.GET to { id, _ -> {
        req -> Response(Status.OK)
    } },
)

private val apiContract = contract {
    routes += contractRoutes
}

class RoutingTest {
    @Test
    fun `test greedy spa`() {
        val handler = routes(
            //singlePageApp(ResourceLoader.Directory("static-content")),

            "/api" bind apiContract,
        )

        val resp = handler(Request(Method.GET, "/api/foo/abc/bar"))
        println(resp)
        assertEquals(Status.OK, resp.status)
    }
}
abc is not an integer, it throws 404 not found if no SPA, but returns OK if SPA
a
Hmm. Can you share your
idLens
?
r
val idLens = <http://Path.int|Path.int>().of("id")
also, looks like using
ResourceLoader.Directory("static-content")
and
ResourceLoader.Classpath
might interfere in the final result
a
Yeah, I'm certain the
idLens
is the issue here. If your lens requires an
Int
, then
/api/foo/123/bar
will match the api route. However,
/api/foo/abc/bar
doesn't match the route, so the router will fall back to the next thing that does: the SPA. I think the only solution you have is to modify the lens to accept a string, and then perform the validation inside the handler.
I'm not certain if this behaviour is intentional, but I can see the benefit: have multiple routes that all accept a different lens type.
r
Dang, that's unfortunate...
Anyways, thank you for the help 🙂
a
Yeah, glad I could help. At least you have a workaround. My personal preference is to deploy the API and SPA separately. The SPA can take advantage of a file server for superior performance. You can route between your API and SPA with your Load Balancer's path-based routing, or by your DNS with different subdomains.
r
Ok wait... something is weird
The SPA is overwriting my docs route aswell.. (?)
Ah ok NVM, I forgot to change the routes order
d
I'm not certain if this behaviour is intentional, but I can see the benefit: have multiple routes that all accept a different lens type.
Yes - this is exactly working as intended. It's the same with all of the routing logic - overall if the request cannot be matched, then the request falls through to the next layer. If you think about it, you can use things like accept headers in the same way so that you can service requests for different content types with different handlers