Loading
Current section: Authentication 6 exercises
solution

Exercise 04 (Solution)

Transcript

00:00 You will be interacting with authentication in two primary ways in your end to end tests. The first one is testing authentication as a feature, and in that case, you need to make sure that performing certain actions like a and b over here eventually results in the authenticated state, the user being successfully authenticated. This is the goal and intention of this test. But when authentication becomes a part of a different feature, for example, requiring the user to be logged in in order to access a certain page or perform a certain action, the story will be a little different. In that case, authentication becomes a prerequisite in order for us to even get to the things that we wanna test.

00:36 In this case, let's say we wanna make sure that performing action c results in the application getting to the state d. This is what we want to test. This is our system under test. But it won't work without the authentication. The problem here, if we try to follow a regular authentication flow as a part of this test, we might introduce unexpected issues.

00:55 For example, if the step b here fails, well, naturally authentication will also fail and our entire feature test will also be marked as failed. But does that really mean that performing c doesn't result in the d? Not really. Instead, it only means one thing that we made a mistake. We introduced extra context to this test in the form of the whole authentication flow, and now it affects the test results for the feature that's not really related to the step B over here or step A.

01:22 It's the golden rule of assertion all over again. The test must only fail when the intention behind it is not met. So in order for this test to be functioning correctly, this entire authenticated state has to be treated as a prerequisite, as a part of the test setup, a given. Basically, we're always starting in the correct and valid authenticated state and then continuing with what we want to do to test a particular feature. And there are multiple ways how you can do that in Playwright.

01:48 If you take a look at the authentication recipe in the Playwright documentation, it recommends creating a special test case for authentication that will follow the steps to authenticate, make sure that the end result is indeed a successful authentication session, and then it will store that session by using this storage state method that will write all the cookies and local storage entries and whatnot in the given file. That is so you can later hydrate from that stored file in other tests. You do that in your Playwright config by defining your tests, saying that they depend on this setup project, this is the one that runs this authentication flow and writes the state to disk, and use the option storage state providing you the path to this stored authentication state. And while this might sound fine on paper, I believe there are multiple issues with this approach. Well, the biggest issue here is that we are literally introducing shared state.

02:40 We are saying that all these tests depend on these tests. In other words, if the setup project here fails, all the dependent tests will also be marked as failed, even though there might not be necessary anything wrong with them. And another issue here is that the storage state API, while powerful, is rather low level. It provides you no means of actually validating that the stored authentication sessions are still valid. What if you refactored your logic to, let's say, add or remove certain fields from the Joke token that you store for your user?

03:08 Well, now you have risk of running your test against the obsolete authenticated state, giving you irrelevant results. Luckily, there is another way to manage authentication in Playwright, and that is by using personas. First, I will install a package called playwright persona as a dependency in my project. So in the terminal, npm install playwrights dash persona as a dev dependency. Alright.

03:31 If you think about it, there may be multiple types of users that can be logged in and interacting with your app. It may be a regular user, a user with 10 posts, an editor, an admin, maybe even a user who's been banned for suspicious activity. All of those are different personas that you can assume in your tests. And using the Playwright Persona package, as the name suggests, will help you model those different authentication scenarios as a part of your test setup. Because personas are going to use the browser to actually log in, I'm gonna implement them as a Playwright fixture.

04:03 So I will head to the file test extend. Ts where I have all my fixtures and declare my first persona. To do that, I will import from the playwright persona package a function called define persona. And then I will declare a variable called user and assign it the result of calling define persona. This function accepts two arguments.

04:25 The first one is the persona name. This is just a string for me to know what kind of persona I am using. You will see how will come into play in the tests a little later. And the second argument is the options object that will describe the life cycle of this persona. There are a few methods that belong to the persona's life cycle and the most important one is the create session method.

04:47 The library will call this method whenever it needs to create a new session belonging to this persona. And in this method, I'm going to describe how to actually achieve this persona through all the setup parts and the UI automation. I will start by creating a test user in a similar way how we did during testing authentication. So here, I will declare a variable called user and assign it the result of creating the user record in my database. Now having this test user, I need to go into my application and actually go through the login flow to be logged in as that user.

05:19 To do that, I need to automate the browser. Luckily, the create session method exposes the first object, which is the context, that has the page object. This is the same page fixture you use in your playwright tests. And with the page here, I can automate whatever I need to. For example, to go to the login page, and then enter the username and the password using my test user and then submit the form by clicking on the login button.

05:43 Here, I wanna wait until the user is logged in by waiting a specific element, just the username in the text, to become visible. Notice that this is not an assertion because technically this is not a test either. This is a part of the test setup. And the last thing that I want to do is to return the session object from this create session method. To do that, I will just return an object that contains the user record which points to the record created in the database.

06:07 The thing is different methods of this persona lifecycle will have access to my session object and you will see how I'm gonna use them. So the next method I have to define is called verify session. In this verify session method I need to describe the logic that can verify any stored authenticated state to be valid or invalid. And to do that, I can access both the page fixture and the session object in the arguments. The session here is the same object I returned from the create session method.

06:38 And now, again using playwright automation, I can check if the session is valid by locating certain elements on the page. So I'll go to the home page by page go to slash and make sure that the session dot user dot name is visible in the DOM. And notice that I am providing an option here to the to be visible matcher that has timeout set to one hundred milliseconds. That is done so my verification is blazing fast. In other words, I don't want Playwright to wait until this element appears.

07:07 If the stored session is invalid, that UI element will never appear, so I want to fail as fast as possible. Both create session and verify session are required methods on the persona. But there's also another method that I can use that is called destroy session. As you might have guessed from the name, this method will be called when the session is destroyed, and I can use it to clean up any temporary resources I introduced for the session, for example like my tested user. So here I can get access to the session object and use the Prisma client to delete the users by the id of my tested user.

07:42 I'm getting it from session dot user. Id. Once again, this is the same object returned from the createSession method over here. So createSession returns a user and I can use that session in both verify session and destroy session methods. Now that I have these different persona methods implemented, let's talk about when and why they will be called.

08:02 So when I'm running my first test that uses this persona, the library will call the create session method to create the session. It will follow my steps, preparing the resources, and clicking through the UI, and eventually arriving at the session object. Once this is complete, the library will store this authenticated state, meaning cookies and local storage, on the disk. That is done so the subsequent test runs don't have to authenticate again as long as the session is valid. And how does the library know if it's valid?

08:30 Yes, through the verify session method. So in a subsequent run, it will detect that there is a stored authenticated state and instead of creating session, it will call verify session first, giving you the stored session object. And here I am able to let the library know is this session still valid. If it is, it will continue with the test as usual using that stored authenticated state. But if it's not, something different will happen.

08:54 First, the library will call destroy session to dispose of the outdated invalid session. And this session object here actually points to the outdated authenticated state. So I can clean up the resources before new test resources are created. And once the session is destroyed, the library performs the entire authentication process again by calling the create session method. On its own, this user persona that I created over here is not a playwright fixture so let's make it into one.

09:21 I will go to the fixture interface here that describes the type definitions for my fixtures and introduce a new fixture called authenticate. To annotate it, I will once again go to the playwright persona package and import the type called authenticate function. I will use this type here and as the type argument I will provide an array of my personas. Type of user. So by doing this I will infer all the session types and and also the persona name on the authenticate fixture.

09:49 Now to implement the actual fixture, I will use another helper called combine personas. Now this function will combine this personas declarations, as many as you have, into an actual playwright fixture. So I will get to this fixture objects over here, the implementation, and implement authenticate by combining personas, giving it the only persona I have, which is the user persona. Now that I have my authenticate fixture ready, I can use it in my tests to authenticate. I will go to the test at end to end slash notes create.

10:20 Test. Ts and use that new authenticate fixture. I will call this fixture and await its result because it returns a promise. You might notice that it expects an argument. Here, I need to provide an object with a single key as which is the name of the persona I want to use to authenticate us.

10:38 And immediately you see the type safe authentication from the get go. The personas are all strictly typed as well as their session object. So let's get that one. As a result of calling the authenticate function, we're getting the session object with the user because this is this is the property I defined on the object that I returned from the create session method of my persona. Once I call this authenticate fixture with the persona that I want, my test will immediately assume that persona from this moment on.

11:06 So now I want to get to the functionality that is behind authentication. In this case, I want to make sure that logged in users can create notes. So I will grab the navigate function for type safe routing and call navigate users username nodes slash new. This route requires a dynamic segment here, dynamic path segment which is the username. So I'll provide it as the second argument by providing username, and I will grab the username from the user dot username.

11:38 Next, I want to create a note, and I will do it in the UI the same way the user would. I will locate an input by the label title and fill in the title of my note, in this case, my new note. Also, another input by the label content, well, I will provide hello world as that note's content and finally, I will click on the button that says submit. And hopefully, this creates a new note. But I don't wanna hope here, I wanna know for sure.

12:02 This is why I am writing this test to begin with. And I can assert that the note has been successfully created by writing two assertions. The first one, that the heading element on the page with my notes title will be visible, and the second one is that the content of my note will also be visible inside the respective element. Notice how I'm chaining this locators over here the same way you can chain locators in v test when testing your components. And now let's run this test by running npm run test end to end in the terminal and see this scenario as successfully passing.

12:37 Now let's recap everything that's happening in this test. So first things first, we're using the authenticate fixture to log in as the user persona. And this gives us the logged in authenticated state to actually get to this node creation functionality that is behind authentication. Very handy, we're grabbing the session object with our test user that's currently logged in, going to the route that's normally protected and requires the user session, provide this dynamic segment so we go into the proper notes of the proper user, and past that point just automating our actions in the UI by filling in the form to create the note, submitting it, and making sure that the note is created by being visible on the screen. As it often happens with end to end tests, all the heavy lifting here is done in the Authenticate fixture, so let's take a closer look at it.

13:26 We know that the Authenticate fixture is the result of calling combined personas, this is the function from the PlaywrightPersona library, And it accepts all the personas that we want to use in our tests. The persona itself is just an object that consists of the name, in this case user, and different lifecycle methods, where we describe how to get to the authenticated state specific to this user, also how to verify that state once we are restoring it from the file system, and how to clean up, how to destroy this session in case we creating some resources associated with it. There are multiple authentication strategies that you might be using in your end to end tests. You might have a designated test user by using fixed credentials like email and password. You may create a one time test user as I am doing over here.

14:13 Every time I have a new authentication, it will result in the random user. So this is what I call reproducible randomness. Or you may create a completely random resources. And luckily, the Playwright persona library accommodates all of those use cases through the create session, verify session, and also destroy session lifecycle methods. I have mentioned multiple times that successful sessions are stored on disk.

14:36 But where exactly is that? If you go to your project, you will find a playwright directory that has a dot auth directory. And this is where all these test sessions are stored in JSON files like this one. Because sessions contain sensitive information by design, make sure to ignore this entire dot auth directory in git, but you can still keep it persisted between your CI jobs for faster authentication.