Examining test layering in multi-tenant requests

Welcome to No Compromises. A peek into the mind of two old web devs who have seen some things. This is Joel.

And this is Aaron. The other day we were doing a code review and came across something in my code, Joel, that you had sort of a comment on or maybe a question, and we went back and forth. I thought it'd be fun to talk about that because I'm not sure if I explained my thought process properly and it might be something good to talk about.

So this will be like a true confession or something is what I'm hearing?

Yeah.

Okay, good.

Exactly. We were writing a... or, I was writing a unit test, and this was around a particular endpoint for a client that had some particular requirements that are necessary on all requests. And we had to send maybe a header or some information along with all different requests. And one of the things that we do when we write our unit tests is we kind of start... our feature tests in Laravel, for example, we start with a very basic, very plain, very broken down version of our request. So the very first test for an endpoint would be what happens if I call this with nothing? With no parameters, not authenticated, no permissions, all that kind of stuff and we want to test that failure scenario and make sure it's proper. And we kind of build off from there. So, you understand the context so far, right?

I do. Can I just add? I just want to add one thing for those that test like us. The way I conceptualize what you just described is almost, I kind of visualize the request coming into my app and the different middleware it passed through. You know, so authentication in this case requires a certain ID to be present in the header. We'll test those things even before the happy path as we lay out our test file, not necessarily the order we write the tests in. But, okay, all right. Keep going, I'm with you.

And you actually said an important sort of phrase or word that I've been trying to kind of look at. It's sort of about the middleware, it's sort of about the sort of stack that you have for your application. So, this particular code review was about... we were starting out with a very basic request again and it was starting to do with authentication. But what I wanted to do is I wanted to send this extra identifier along. And there was a bit of a discussion about, "Well, that's not the simplest version of the request, why would you do that?" So, that's kind of what I want to talk about.

And I will also say in every other project, that is not how we would do it. I think that was more the thing that triggered it. And there's another bit of a context which I'm sure you're going to share, as to why you did it that way. But I think that's what sort of set me off at first, is like, "Why are you doing this? We would not normally do that."

Yeah. What we're kind of talking about here is the layers and the priority of the different things that may affect your request is your request's response. Is what we're talking about here. And the first thing you mentioned is, "We wouldn't do that on other project," correct? Because this is the first one that I know of that we had this particular type of requirement. Which was the requirement was the same level as something like authentication. So, in this case, that requirement was also an identifier. If you look at authentication as identification in a way, then this header we were trying to send was identification as well. And it wasn't so much that they were stacked, it was they were equal level.

I don't know if I agree with that. Yeah, well, thats great. Okay, so you're saying conceptually they're an equal level. And me, I'm like, well actually Laravel is checking authorization before this header. So in my mind, the authorization happens first, that's layer one. Let's just say it. I know we don't want to get into specifics with the client. It's a multi-tenant application so every request-

Authentication.

What?

Authentication. You were saying authorization, we're talking about authentication.

Oh, okay. You're very good. It's a multi-tenant app in it's API, and every request needs a tenant ID header. Right? So this is not like trade secrets here. Yes, you need an authentication header and you need a tenant ID header. In Laravel, the middleware priority, it checks authentication first and then it checks tenant ID second.

Just because that's how you set it up.

Hmm, okay. True. Yes, I agree it could be set up another way.

So in this particular example, I disagree with that, but that's the fun of this podcast.

Well, you disagree with the conceptual, not the reality.

No, I disagree with the reality as well. Because the first test I was able to write was passing in a tenant ID with no authentication. Then we got our authentication issue so if that is our authentication, failure. If you do a flip side, you get a tenant ID failure and not necessarily... and you also get authentication failure. But the point is, with one I think they're at the same level, even though there may be different order in the code. Because the way that you respond... This is so abstract, it's really hard to deal with this without looking at code. But let me look at it a different way.

Okay.

I was able to write a test that showed that we could get a particular type of response from sending a tenant ID but not an authentication.

A 401, right? Unauthorized.

Yeah. And if you think about it, I went a different direction at that than what you would imagined someone would use that as. So that was kind of my whole point, which was that if you think about these things stacked in the way that we put them together in code, that isn't necessarily the way that the code functions. If I'm able to send a tenant ID with no authentication but we always check authentication first, how was I able to get an authentication error with the tenant ID? And then is that different than if I were to never send a tenant ID? You know, did it validate the tenant ID first if I send in bad information then tenant ID? Can I cause a buffer overflow? All these different things that have really nothing to do with the fact that whether they're authenticated or not. And I wanted to make sure I didn't necessarily have authentication in there as a layer that would affect the processing of the tenant ID.

I see what you're saying. Let me try to summarize this back to you.

You're welcome to disagree at the end of this too. That's fine.

I do, a little bit. But before I disagree-

You're welcome to be wrong.

I'm okay with that. I'm very comfortable being wrong. You are correct in that there is an assumption baked in to maybe the typical way we would have tested it. Which is, oh, we know authorization is checked first so we won't send any...

Authentication.

My goodness, why do I keep saying that? Authentication. I'll just say AuthN and then it can be either. We know that authentication is checked first so there's no point in doing anything with a tenant because we just know. We're assuming the way the code is written and how we structured it, it's never even going to get to that other tenant thing. But what you're saying is that's a choice we made.

Yeah. I'm saying what happens if you pass in tenant 0? Maybe all the authentication passes.

Right. You didn't write that test but anyways.

Oh, well, but the point is if you start looking at tests as in the information that you know from the code, you're going to miss scenarios.

Sure. And if we were penetration testers or something, we would mess around with all that stuff. So I get what you're saying. I don't think they're conceptually the same level, but I think that's irrelevant because the levels are sort of a artificial construct we made in how we happen to stack the middleware together.

Let me go then. So authentication identifies something, right?

It identifies the user.

It identifies the user, it identifies something. Doesn't tenant ID also identifies something?

I mean, every word identifies something if you want to get philosophical. But no, the tenant ID is I'm a user. I might have access to 20 tenants. I'm saying which tenant I want to work with. Authentication is who am I? Like, all future decisions, including do I have access to this tenant? We require that user to be valid. And that's why we ordered the middleware in that order. Okay. I feel like we're coming closer together on this. I also want to add one more detail that was specific to why you did this. Are you okay with me?

Yeah.

Okay. We were adding-

Please try to make me look better.

Okay, good. We were adding in... Okay, this is an API, I said that already. The API has a formal open API specification that we're writing and maintaining as part of it. We were backfilling in the tests because we had permission from the client to spend time on this specification validation. Like, in our tests every time we make a request, we added in a library that would check the request made against the spec and then would check the response returned against the spec. And if anything deviated it would throw an error. So that's the actual reason you made a change to this test, correct?

I mean, yeah.

Okay. Well, no, I think that's-

00:10:12.000
Yeah.

But again, I rarely work with multi-tenant sort of scenarios like this. So it's not a thing that I think about testing. It just so happened that that was almost a by... That task made me think about, "Am I testing this properly?" The mechanics of it is it was able to solve the testing issue I had but the reason I wanted to talk about it on this podcast is because I thought it brought up a philosophical question.

Yeah. And I'll be honest, after we talked about it, I was thinking more about it and I could actually see the benefits in what you had done, even though it was different from what I was used to. And now hearing you explain it even further, I'm even closer to your point. So I think this was productive because I never actually even considered... I know this wasn't even why we did it, but in the abstract, the security ramifications or the other things it could surface if your implementation wasn't correct in how you layered those middleware together. I think it is a valid point.

Yeah, it's the whole concept of, you know, what order do you want to do your route model binding in? You do it before or after your authentication? Do you want to give away that information? And I'm still not 100% sold even on after this that that was the right decision that I did. Because I know there's also, like you said, there's a limit to how detailed you get in your testing too. Like, do I have to test every scenario? And obviously you don't rely on code coverage for everything, but this is covered three different times. How many more times do I need to cover it and what different scenarios? But, yeah, I think that we're probably pretty close and I think we could probably wrap it up saying we agree a little bit to disagree on a few different things.

Fair by me.

Some people may not remember this because they're not seasoned and salty like us. But do you remember when it was the big hullabaloo and a big excitement to get free steak knives as a gift?

Free steak knives? No, maybe I'm on the wrong generation for that.

No, you're not.

Okay.

I'm not that much older than you.

Like, is it ad or something?

Yeah. It was like you order online like... I just remember infomercials, "Order now and you get a free set of steak knives," and whatever.

Okay. TV infomercial, that's coming to me.

Starting to come back, right?

Sure.

Like, I don't know if this is still going on, I assume it is. Or maybe it's on TikTok and all that stuff. But I just remember the simple, the weird little things that you would win in order to do something. Like, "Oh, a free gift. A Snuggie." I don't really want that.

I remember those. I never had one.

Surprising. You need someone to look over your tests and make sure they're tip-top.

Well, you know that we love looking at tests and talking about it. So head over to masteringlaravel.io and contact us if you'd like some help.

No Compromises, LLC