DEV Community

Cover image for Authentication system using rust (actix-web) and sveltekit - Automated testing
John Owolabi Idogun
John Owolabi Idogun

Posted on

Authentication system using rust (actix-web) and sveltekit - Automated testing

Introduction

We have come thus far. From a skeletal project to a feature-complete authentication system. Boldly, we can pat ourselves on the back for a job well done. But wait, we haven't completed the development phase of Software Engineering. Though we have manually tested our backend service and frontend application, automated tests are still missing. Not everyone has the luxury to manually browse through your application to test whether or not something is broken. We can assist those persons by providing some tests which can easily be run to confirm the correctness of our development efforts. This article will try to lay some foundations for testing our current system. You are at liberty to include stuff such as mocking of function calls and the rest. The project's repository awaits your PRs!

Source code

The source code for this series is hosted on GitHub via:

GitHub logo Sirneij / rust-auth

A fullstack authentication system using rust, sveltekit, and Typescript

rust-auth

A full-stack secure and performant authentication system using rust's Actix web and JavaScript's SvelteKit.

This application resulted from this series of articles and it's currently live here(I have disabled the backend from running live).

Run locally

You can run the application locally by first cloning it:

~/$ git clone https://github.com/Sirneij/rust-auth.git
Enter fullscreen mode Exit fullscreen mode

After that, change directory into each subdirectory: backend and frontend in different terminals. Then following the instructions in each subdirectory to run them.




Implementation

Step 1: Automated backend testing

You can get the overview of the code for this section on github.

When starting out, we made some design decisions at the backend. The decision will allow us to independently test the service without interfering with the real application using a term called integration testing. We'll utilize two "dev" packages: reqwest and fake. Dev dependencies only get introduced into your application in development or during testing. In production, they are not included:

~/rust-auth/backend$ cargo add --dev reqwest --features json,cookies,rustls-tls

~/rust-auth/backend$ cargo add --dev fake
Enter fullscreen mode Exit fullscreen mode

For reqwest, we activated json, cookies, and rustls-tls in addition to its default features. json provides serialization and deserialization for JSON bodies; cookies allows cookie session support; and rustls-tls enables TLS functionality provided by rustls. Now to the test proper.

Create an api subfolder in the tests folder. Then create a helper.rs file which has the following content:

// backend/tests/api/helpers.rs
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
    Argon2,
};
use once_cell::sync::Lazy;
use sqlx::Row;

static TRACING: Lazy<()> = Lazy::new(|| {
    let subscriber = backend::telemetry::get_subscriber(false);
    backend::telemetry::init_subscriber(subscriber);
});

pub struct TestApp {
    pub address: String,
    pub test_user: TestUser,
    pub api_client: reqwest::Client,
}

impl TestApp {
    pub async fn post_login<Body>(&self, body: &Body) -> reqwest::Response
    where
        Body: serde::Serialize,
    {
        self.api_client
            .post(&format!("{}/users/login/", &self.address))
            .json(body)
            .send()
            .await
            .expect("Failed to execute request.")
    }
}

pub async fn spawn_app(pool: sqlx::postgres::PgPool) -> TestApp {
    dotenv::from_filename(".env.test").ok();
    Lazy::force(&TRACING);

    let settings = {
        let mut s = backend::settings::get_settings().expect("Failed to read settings.");
        // Use a random OS port
        s.application.port = 0;
        s
    };

    let application = backend::startup::Application::build(settings.clone(), Some(pool.clone()))
        .await
        .expect("Failed to build application.");
    let address = format!("http://127.0.0.1:{}", application.port());

    let _ = tokio::spawn(application.run_until_stopped());

    let client = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .cookie_store(true)
        .build()
        .unwrap();

    let test_app = TestApp {
        address,
        test_user: TestUser::generate().await,
        api_client: client,
    };

    test_app.test_user.store(&pool).await;

    test_app
}

pub struct TestUser {
    pub email: String,
    pub password: String,
    first_name: String,
    last_name: String,
}

impl TestUser {
    pub async fn generate() -> Self {
        Self {
            email: uuid::Uuid::new_v4().to_string(),
            password: uuid::Uuid::new_v4().to_string(),
            first_name: uuid::Uuid::new_v4().to_string(),
            last_name: uuid::Uuid::new_v4().to_string(),
        }
    }

    async fn store(&self, pool: &sqlx::postgres::PgPool) {
        let salt = SaltString::generate(&mut OsRng);

        let password_hash = Argon2::default()
            .hash_password(self.password.as_bytes(), &salt)
            .expect("Unable to hash password.")
            .to_string();

        let user_id = sqlx::query(
            "INSERT INTO users (email, password, first_name, last_name, is_active, is_staff, is_superuser) 
            VALUES ($1, $2, $3, $4, true, true, true) RETURNING id"
        )
        .bind(&self.email)
        .bind(password_hash)
        .bind(&self.first_name)
        .bind(&self.last_name)
        .map(|row: sqlx::postgres::PgRow| -> uuid::Uuid{
            row.get("id")
       })
        .fetch_one(pool)
        .await
        .expect("Failed to store test user.");

        sqlx::query(
            "INSERT INTO user_profile (user_id) 
                    VALUES ($1) 
                ON CONFLICT (user_id) 
                DO NOTHING",
        )
        .bind(user_id)
        .execute(pool)
        .await
        .expect("Cannot store user_profile to the DB");
    }
}
Enter fullscreen mode Exit fullscreen mode

By now, long snippets shouldn't faze you again. What is needed is some skimming and you will definitely get an idea of what it does. As for this snippet, we first imported a couple of things. Then we lazily initiated our telemetry module which helps provide request/response tracing functionality. Next, a TestApp struct that holds some important defaults of our test application was defined. A post_login method was then defined for this struct so that with the app's instance in scope, we can easily make a login request. spawn_app was where we built our application; fed it with a database pool — will be supplied by SQLx — started the app with any available port; initialized our reqwest client with some defaults such as enabling cookies for all requests; gave TestApp fields the values we wanted; created a default application test user and then returned the TestApp. TestUser is the struct that houses our test user and it has some pretty basic methods to help perform its job. All these are done to ease our testing experience.

In backend/tests/api/main.rs, we made our test module:

// backend/tests/api/main.rs
mod helpers;
mod users;
Enter fullscreen mode Exit fullscreen mode

This module expects that a backend/tests/api/users/mod.rs file has been created:

// backend/tests/api/users/mod.rs
mod current_user;
mod login;
mod logout;
mod regenerate_token;
mod register;
mod update_users;
Enter fullscreen mode Exit fullscreen mode

The repo currently has those tests. I want collaborators to come up with a more encompassing test suite. For this article, we will only discuss the login, register and update_users tests. Other ones are very identical.

To the register test suite:

// backend/tests/api/users/register.rs
use crate::helpers::spawn_app;
use fake::faker::{
    internet::en::SafeEmail,
    name::en::{FirstName, LastName, NameWithTitle},
};
use fake::Fake;
use sqlx::Row;

#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct NewUser<'a> {
    email: &'a str,
    password: String,
    first_name: String,
    last_name: String,
}

#[sqlx::test]
async fn test_register_user_success(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // Request data
    let email: String = SafeEmail().fake();
    let first_name: String = FirstName().fake();
    let last_name: String = LastName().fake();
    let password = NameWithTitle().fake();
    let new_user = NewUser {
        email: &email,
        password,
        first_name,
        last_name,
    };

    let response = app
        .api_client
        .post(&format!("{}/users/register/", &app.address))
        .json(&new_user)
        .header("Content-Type", "application/json")
        .send()
        .await
        .expect("Failed to execute request.");

    assert!(response.status().is_success());

    let saved_user = sqlx::query(
        "SELECT 
            u.id AS u_id, 
            u.email AS u_email, 
            u.password AS u_password, 
            u.first_name AS u_first_name, 
            u.last_name AS u_last_name, 
            u.is_active AS u_is_active, 
            u.is_staff AS u_is_staff, 
            u.is_superuser AS u_is_superuser, 
            u.thumbnail AS u_thumbnail, 
            u.date_joined AS u_date_joined, 
            p.id AS p_id, 
            p.user_id AS p_user_id, 
            p.phone_number AS p_phone_number, 
            p.birth_date AS p_birth_date, 
            p.github_link AS p_github_link 
        FROM 
            users u 
            LEFT JOIN user_profile p ON p.user_id = u.id
        WHERE 
            u.is_active=false AND u.email=$1
    ",
    )
    .bind(&email)
    .map(|row: sqlx::postgres::PgRow| backend::types::User {
        id: row.get("u_id"),
        email: row.get("u_email"),
        first_name: row.get("u_first_name"),
        password: row.get("u_password"),
        last_name: row.get("u_last_name"),
        is_active: row.get("u_is_active"),
        is_staff: row.get("u_is_staff"),
        is_superuser: row.get("u_is_superuser"),
        thumbnail: row.get("u_thumbnail"),
        date_joined: row.get("u_date_joined"),
        profile: backend::types::UserProfile {
            id: row.get("p_id"),
            user_id: row.get("p_user_id"),
            phone_number: row.get("p_phone_number"),
            birth_date: row.get("p_birth_date"),
            github_link: row.get("p_github_link"),
        },
    })
    .fetch_one(&pool)
    .await
    .expect("msg");

    assert_eq!(saved_user.is_active, false);
    assert_eq!(saved_user.email, email);
    assert_eq!(saved_user.thumbnail, None);
    assert_eq!(saved_user.profile.user_id, saved_user.id);
    assert_eq!(saved_user.profile.phone_number, None)
}
#[sqlx::test]
async fn test_register_user_failure_email(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // First request data
    let email = "backend@api.com".to_string();
    let first_name: String = FirstName().fake();
    let last_name: String = LastName().fake();
    let password = NameWithTitle().fake();
    let new_user_one = NewUser {
        email: &email,
        password,
        first_name,
        last_name,
    };

    let response_one = app
        .api_client
        .post(&format!("{}/users/register/", &app.address))
        .json(&new_user_one)
        .header("Content-Type", "application/json")
        .send()
        .await
        .expect("Failed to execute request.");

    assert!(response_one.status().is_success());

    // First request data
    let email = "backend@api.com".to_string();
    let first_name: String = FirstName().fake();
    let last_name: String = LastName().fake();
    let password = NameWithTitle().fake();
    let new_user_two = NewUser {
        email: &email,
        password,
        first_name,
        last_name,
    };

    let response_two = app
        .api_client
        .post(&format!("{}/users/register/", &app.address))
        .json(&new_user_two)
        .header("Content-Type", "application/json")
        .send()
        .await
        .expect("Failed to execute request.");

    assert!(response_two.status().is_client_error());

    let error_response = response_two
        .json::<backend::types::ErrorResponse>()
        .await
        .expect("Cannot get user response");

    assert_eq!(
        error_response.error,
        "A user with that email address already exists"
    );
}
Enter fullscreen mode Exit fullscreen mode

We used fake to generate names, emails and passwords. Notice the #[sqlx::test] macro. That's an incredibly nifty macro that automatically creates test databases, connects to them, applies migrations, and deletes databases. An automatic test database management suit. Under the hood, it uses either #[tokio::test] or #[async_std::test] depending on your choice of async runtime. To successfully use it, you must activate SQLx migrate feature and all test functions that need to use the pool it creates need to pass the pool: sqlx::postgres::PgPool parameter. In test_register_user_success, we tested the success path of our register route. All its side effects were asserted. We also test its unhappy paths. A major drawback of these tests is the absence of mocking. You are very welcome to send a PR.

Next is backend/tests/api/users/login.rs:

// backend/tests/api/users/login.rs
use crate::helpers::spawn_app;
use fake::Fake;

#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct LoginUser {
    email: String,
    password: String,
}

#[sqlx::test]
async fn test_login_user_failure_bad_request(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // Act - Part 1 - Login
    let login_body = LoginUser {
        email: app.test_user.email.clone(),
        password: fake::faker::name::en::NameWithTitle().fake(),
    };
    let login_response = app.post_login(&login_body).await;
    assert!(login_response.status().is_client_error());

    let error_response = login_response
        .json::<backend::types::ErrorResponse>()
        .await
        .expect("Cannot get user response");

    assert_eq!(error_response.error, "Email and password do not match");
}

#[sqlx::test]
async fn test_login_user_failure_notfound(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // Act - Part 1 - Login
    let login_body = LoginUser {
        email: fake::faker::internet::en::SafeEmail().fake(),
        password: app.test_user.password.clone(),
    };
    let login_response = app.post_login(&login_body).await;
    assert!(login_response.status().is_client_error());

    let error_response = login_response
        .json::<backend::types::ErrorResponse>()
        .await
        .expect("Cannot get user response");

    assert_eq!(error_response.error, "A user with these details does not exist. If you registered with these details, ensure you activate your account by clicking on the link sent to your e-mail address");
}

#[sqlx::test]
async fn test_login_user_success(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    let login_body = LoginUser {
        email: app.test_user.email.clone(),
        password: app.test_user.password.clone(),
    };
    let login_response = app.post_login(&login_body).await;
    assert!(login_response.status().is_success());

    // Check that there is cookie present
    let headers = login_response.headers();
    assert!(headers.get("set-cookie").is_some());
    let cookie_str = headers.get("set-cookie").unwrap().to_str().unwrap();
    assert!(cookie_str.contains("sessionid="));

    // Check response
    let response = login_response
        .json::<backend::types::UserVisible>()
        .await
        .expect("Cannot get user response");

    assert_eq!(response.email, app.test_user.email);
    assert!(response.is_active);
    assert_eq!(response.id, response.profile.user_id);
}
Enter fullscreen mode Exit fullscreen mode

We first ensured that its unhappy paths were properly tested before proceeding to its happy path where we ensured that cookie is present in the response header using:

let headers = login_response.headers();
    assert!(headers.get("set-cookie").is_some());
    let cookie_str = headers.get("set-cookie").unwrap().to_str().unwrap();
    assert!(cookie_str.contains("sessionid="));
Enter fullscreen mode Exit fullscreen mode

Thanks to reqwest's cookie feature.

Last is testing the api endpoint that expects a multipart form:

// backend/tests/api/users/update_users.rs
use crate::helpers::spawn_app;

#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct LoginUser {
    email: String,
    password: String,
}

#[sqlx::test]
async fn test_update_user_failure_not_logged_in(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // multipart form
    let form = reqwest::multipart::Form::new()
        .text("github_link", "https://github.com/Sirneij")
        .text("phone_number", "+2348135459073");

    let update_user_response = app
        .api_client
        .patch(&format!("{}/users/update-user/", &app.address))
        .multipart(form)
        .send()
        .await
        .expect("Failed to execute request.");

    // Check response
    let response = update_user_response
        .json::<backend::types::ErrorResponse>()
        .await
        .expect("Cannot get user response");

    assert_eq!(
        response.error,
        "You are not logged in. Kindly ensure you are logged in and try again"
    );
}

#[sqlx::test]
async fn test_update_user_success(pool: sqlx::postgres::PgPool) {
    let app = spawn_app(pool.clone()).await;

    // First login
    let login_body = LoginUser {
        email: app.test_user.email.clone(),
        password: app.test_user.password.clone(),
    };
    let login_response = app.post_login(&login_body).await;
    assert!(login_response.status().is_success());

    // Check that there is cookie present
    let headers = login_response.headers();
    assert!(headers.get("set-cookie").is_some());
    let cookie_str = headers.get("set-cookie").unwrap().to_str().unwrap();
    assert!(cookie_str.contains("sessionid="));

    // multipart form
    let form = reqwest::multipart::Form::new()
        .text("github_link", "https://github.com/Sirneij")
        .text("phone_number", "+2348135459073");

    let update_user_response = app
        .api_client
        .patch(&format!("{}/users/update-user/", &app.address))
        .multipart(form)
        .send()
        .await
        .expect("Failed to execute request.");

    // Check response
    let response = update_user_response
        .json::<backend::types::UserVisible>()
        .await
        .expect("Cannot get user response");

    assert_eq!(response.email, app.test_user.email);
    assert!(response.is_active);
    assert_eq!(response.id, response.profile.user_id);
    assert_eq!(
        response.profile.github_link,
        Some("https://github.com/Sirneij".to_string())
    );
    assert_eq!(
        response.profile.phone_number,
        Some("+2348135459073".to_string())
    );
}
Enter fullscreen mode Exit fullscreen mode

With reqwest, sending multipart forms is a breeze. You must activate the multipart feature though. In these tests, we didn't upload images because I hadn't found a good way to mock aws's s3 functionalities yet. If you do, kindly send a PR.

That's the bit about the tests. The full tests included can be found in the repo.

Step 2: Automated frontend testing

You can get the overview of the code for this section on github.

The front-end already helped us include some awesome testing libraries: vitest for unit testing and playwright for end-to-end testing. When we started our app, we already chose to use these libraries and a tests folder was created for playwright. vitest uses any file found somewhere else that has *.test.js|ts as part of their filenames. For this article, we will only test the home page (/ route) and login page (/auth/login route). PRs are welcome for other routes. I also included unit tests for some helper functions.

Let's start with the home page (/ route):

// frontend/tests/index.test.ts
import { expect, test } from '@playwright/test';

test('index page has title', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveTitle('Written articles | Actix Web & SvelteKit');
});

test('index page has h1 content', async ({ page }) => {
    await page.goto('/');
    expect(await page.textContent('h1')).toContain(
        'Authentication system using Actix Web and Sveltekit'
    );
});

test('index page has img alt content', async ({ page }) => {
    await page.goto('/');
    const isVisible = await page.getByAltText('Rust (actix-web) and Sveltekit').isVisible();
    expect(isVisible).toBe(true);
});

test('test some elements', async ({ page }) => {
    await page.goto('/');
    await page
        .getByRole('heading', { name: 'Authentication system using Actix Web and Sveltekit' })
        .click();
    await page.getByRole('link', { name: 'Login' }).click();

    await page.getByRole('button', { name: '+' }).click();
});
Enter fullscreen mode Exit fullscreen mode

It is pretty simple to read through. Being a first-timer with JavaScript automated testing, I fell in love with it instantly. In the tests, we're trying to be sure that those strings are indeed on the page. Next is frontend/tests/login.test.ts:

// frontend/tests/login.test.ts
import { expect, test } from '@playwright/test';

test('login page has title, h1 and url', async ({ page }) => {
    await page.goto('/auth/login');
    await expect(page).toHaveTitle('Auth - Login | Actix Web & SvelteKit');
    await expect(page).toHaveURL('/auth/login');
    expect(await page.textContent('h1')).toBe('Login');
});

test('login page form element', async ({ page }) => {
    await page.goto('/auth/login');

    // Form element
    const formElement = page.locator('form');
    const formAction = await formElement.getAttribute('action');
    expect(formAction).toBe('?/login');

    // Email input
    const inputField = page.getByRole('textbox', { name: 'Email address' });
    await inputField.click();
    await inputField.type('jane.doe@example.com');
    await expect(inputField).toHaveValue('jane.doe@example.com');

    // Password input
    const passwordInput = page.getByRole('textbox', { name: 'Password' });
    await passwordInput.click();
    await passwordInput.type('mypassword');
    expect(await page.inputValue("input[type='password']")).toBe('mypassword');
});

test('login page has some links', async ({ page }) => {
    await page.goto('/auth/login');
    await page.getByRole('link', { name: 'Create an account.' }).click();
    await page.getByRole('link', { name: 'Forgot password?' }).click();
});
Enter fullscreen mode Exit fullscreen mode

We took a step further to interact with the form element and its input elements. How cool is that?!

One of the unit tests included is:

// frontend/src/lib/utils/helpers/input.validation.test.ts

import { test, expect } from 'vitest';
import { isValidEmail, isValidPasswordMedium, isValidPasswordStrong } from './input.validation';

test('test isValidEmail', () => {
    let email = 'good@email.com';
    expect(isValidEmail(email)).toEqual(true);

    email = 'bad@email';
    expect(isValidEmail(email)).toEqual(false);
});

test('test isValidPasswordStrong', () => {
    let password = '123456Data@gmail.Com';
    expect(isValidPasswordStrong(password)).toEqual(true);

    password = 'badpassword';
    expect(isValidEmail(password)).toEqual(false);
});
test('test isValidPasswordMedium', () => {
    let password = '123456Data';
    expect(isValidPasswordMedium(password)).toEqual(true);

    password = 'badpassword';
    expect(isValidEmail(password)).toEqual(false);
});
Enter fullscreen mode Exit fullscreen mode

These tests use the power of vitest to test the helper functions included.

That's it!!! It's finally time to draw the curtains after this long series. As stated, I welcome gigs, comments, criticisms (constructive), collaborations and all other nifty stuff... Bye for now.

Outro

Enjoyed this article? Consider contacting me for a job, something worthwhile or buying a coffee ☕. You can also connect with/follow me on LinkedIn and Twitter. It isn't bad if you help share this article for wider coverage. I will appreciate it...

Top comments (6)

Collapse
 
jaer profile image
jaer

Hi John,

Your code can't be compiled. Could you inform me how to run it?
The packages seem to be outdated.

Looking forward to your reply.
Thanks

Collapse
 
sirneij profile image
John Owolabi Idogun

Hi Jaer,

Can you explain more on this? I am very sure that some of the packages have latest revisions but I haven't had time to update them yet.

Collapse
 
jaer profile image
jaer

Hi John,

Thank you for replying.
Below is the snippet of the error output.
Appreciate if you could give me some hints on how to solve this issue. Thanks

error[E0433]: failed to resolve: unresolved import
--> src/routes/users/register.rs:64:24
|
64 | crate::types::ErrorResponse {
| ^^^^^
| |
| unresolved import
| help: a similar path exists:
sqlx::types`

error[E0433]: failed to resolve: unresolved import
--> src/routes/users/register.rs:68:24
|
68 | crate::types::ErrorResponse {
| ^^^^^
| |
| unresolved import
| help: a similar path exists: sqlx::types

error[E0433]: failed to resolve: unresolved import
--> src/routes/users/register.rs:81:72
|
81 | actix_web::HttpResponse::InternalServerError().json(crate::types::ErrorResponse {
| ^^^^^
| |
| unresolved import
| help: a similar path exists: sqlx::types

error[E0433]: failed to resolve: could not find utils in the crate root
--> src/routes/users/register.rs:87:12
|
87 | crate::utils::send_multipart_email(
| ^^^^^ could not find utils in the crate root

error[E0433]: failed to resolve: unresolved import
--> src/routes/users/register.rs:104:47
|
104 | actix_web::HttpResponse::Ok().json(crate::types::SuccessResponse {
| ^^^^^
| |
| unresolved import`

Thread Thread
 
sirneij profile image
John Owolabi Idogun • Edited

I just cloned the repo. Updated the packages using cargo update and my rust version using rust update. Everything compiles without any error. I have even pushed a small update to github.

Thread Thread
 
jaer profile image
jaer

Hi John,

Thanks for the highlight. But the issues during the compilation like this:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.75s
Running
target/debug/backend
thread 'main' panicked at /Users/eric/workspace/Rust/github/rust-auth/backend/src/startup.rs:111:54:
Failed to get AWS key.: NotPresent
note: run with
RUST_BACKTRACE=1environment variable to display a backtrace

Could you show me how to solve this issue?
Thanks

Thread Thread
 
sirneij profile image
John Owolabi Idogun

Kindly read what the error says. You need to provide your AWS S3 credentials. I included .env.test. It contains the required data. To run the app, create a .env.development file and put in the following data:

APP_EMAIL__HOST_USER=email@example.com
APP_EMAIL__HOST_USER_PASSWORD=some_email_password

AWS_REGION=some_region
AWS_ACCESS_KEY_ID=some_access_id
AWS_SECRET_ACCESS_KEY=some_access_key
AWS_S3_BUCKET_NAME=some_bucket_name

DATABASE_URL=postgres://postgres:password@localhost:5432/postgres
Enter fullscreen mode Exit fullscreen mode

Modify them accordingly.