diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp
index 4ab991a7df..a1ce4525d6 100644
--- a/src/input_common/drivers/udp_client.cpp
+++ b/src/input_common/drivers/udp_client.cpp
@@ -536,42 +536,46 @@ CalibrationConfigurationJob::CalibrationConfigurationJob(
     std::function<void(u16, u16, u16, u16)> data_callback) {
 
     std::thread([=, this] {
+        u16 min_x{UINT16_MAX};
+        u16 min_y{UINT16_MAX};
+        u16 max_x{};
+        u16 max_y{};
+
         Status current_status{Status::Initialized};
-        SocketCallback callback{
-            [](Response::Version) {}, [](Response::PortInfo) {},
-            [&](Response::PadData data) {
-                static constexpr u16 CALIBRATION_THRESHOLD = 100;
-                static constexpr u16 MAX_VALUE = UINT16_MAX;
+        SocketCallback callback{[](Response::Version) {}, [](Response::PortInfo) {},
+                                [&](Response::PadData data) {
+                                    constexpr u16 CALIBRATION_THRESHOLD = 100;
 
-                if (current_status == Status::Initialized) {
-                    // Receiving data means the communication is ready now
-                    current_status = Status::Ready;
-                    status_callback(current_status);
-                }
-                const auto& touchpad_0 = data.touch[0];
-                if (touchpad_0.is_active == 0) {
-                    return;
-                }
-                LOG_DEBUG(Input, "Current touch: {} {}", touchpad_0.x, touchpad_0.y);
-                const u16 min_x = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.x));
-                const u16 min_y = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.y));
-                if (current_status == Status::Ready) {
-                    // First touch - min data (min_x/min_y)
-                    current_status = Status::Stage1Completed;
-                    status_callback(current_status);
-                }
-                if (touchpad_0.x - min_x > CALIBRATION_THRESHOLD &&
-                    touchpad_0.y - min_y > CALIBRATION_THRESHOLD) {
-                    // Set the current position as max value and finishes configuration
-                    const u16 max_x = touchpad_0.x;
-                    const u16 max_y = touchpad_0.y;
-                    current_status = Status::Completed;
-                    data_callback(min_x, min_y, max_x, max_y);
-                    status_callback(current_status);
+                                    if (current_status == Status::Initialized) {
+                                        // Receiving data means the communication is ready now
+                                        current_status = Status::Ready;
+                                        status_callback(current_status);
+                                    }
+                                    if (data.touch[0].is_active == 0) {
+                                        return;
+                                    }
+                                    LOG_DEBUG(Input, "Current touch: {} {}", data.touch[0].x,
+                                              data.touch[0].y);
+                                    min_x = std::min(min_x, static_cast<u16>(data.touch[0].x));
+                                    min_y = std::min(min_y, static_cast<u16>(data.touch[0].y));
+                                    if (current_status == Status::Ready) {
+                                        // First touch - min data (min_x/min_y)
+                                        current_status = Status::Stage1Completed;
+                                        status_callback(current_status);
+                                    }
+                                    if (data.touch[0].x - min_x > CALIBRATION_THRESHOLD &&
+                                        data.touch[0].y - min_y > CALIBRATION_THRESHOLD) {
+                                        // Set the current position as max value and finishes
+                                        // configuration
+                                        max_x = data.touch[0].x;
+                                        max_y = data.touch[0].y;
+                                        current_status = Status::Completed;
+                                        data_callback(min_x, min_y, max_x, max_y);
+                                        status_callback(current_status);
 
-                    complete_event.Set();
-                }
-            }};
+                                        complete_event.Set();
+                                    }
+                                }};
         Socket socket{host, port, std::move(callback)};
         std::thread worker_thread{SocketLoop, &socket};
         complete_event.Wait();
diff --git a/src/input_common/helpers/udp_protocol.h b/src/input_common/helpers/udp_protocol.h
index bcba12c58e..2d5d54ddbd 100644
--- a/src/input_common/helpers/udp_protocol.h
+++ b/src/input_common/helpers/udp_protocol.h
@@ -54,6 +54,18 @@ struct Message {
 template <typename T>
 constexpr Type GetMessageType();
 
+template <typename T>
+Message<T> CreateMessage(const u32 magic, const T data, const u32 sender_id) {
+    boost::crc_32_type crc;
+    Header header{
+        magic, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, sender_id, GetMessageType<T>(),
+    };
+    Message<T> message{header, data};
+    crc.process_bytes(&message, sizeof(Message<T>));
+    message.header.crc = crc.checksum();
+    return message;
+}
+
 namespace Request {
 
 enum RegisterFlags : u8 {
@@ -101,14 +113,7 @@ static_assert(std::is_trivially_copyable_v<PadData>,
  */
 template <typename T>
 Message<T> Create(const T data, const u32 client_id = 0) {
-    boost::crc_32_type crc;
-    Header header{
-        CLIENT_MAGIC, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, client_id, GetMessageType<T>(),
-    };
-    Message<T> message{header, data};
-    crc.process_bytes(&message, sizeof(Message<T>));
-    message.header.crc = crc.checksum();
-    return message;
+    return CreateMessage(CLIENT_MAGIC, data, client_id);
 }
 } // namespace Request
 
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index c4c012f3d8..4a20c0768e 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -10,11 +10,12 @@ add_executable(tests
     core/network/network.cpp
     tests.cpp
     video_core/buffer_base.cpp
+    input_common/calibration_configuration_job.cpp
 )
 
 create_target_directory_groups(tests)
 
-target_link_libraries(tests PRIVATE common core)
+target_link_libraries(tests PRIVATE common core input_common)
 target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch-single-include Threads::Threads)
 
 add_test(NAME tests COMMAND tests)
diff --git a/src/tests/input_common/calibration_configuration_job.cpp b/src/tests/input_common/calibration_configuration_job.cpp
new file mode 100644
index 0000000000..8c77d81e9b
--- /dev/null
+++ b/src/tests/input_common/calibration_configuration_job.cpp
@@ -0,0 +1,136 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <array>
+#include <string>
+#include <thread>
+#include <boost/asio.hpp>
+#include <boost/crc.hpp>
+#include <catch2/catch.hpp>
+
+#include "input_common/drivers/udp_client.h"
+#include "input_common/helpers/udp_protocol.h"
+
+class FakeCemuhookServer {
+public:
+    FakeCemuhookServer()
+        : socket(io_service, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
+
+    ~FakeCemuhookServer() {
+        is_running = false;
+        boost::system::error_code error_code;
+        socket.shutdown(boost::asio::socket_base::shutdown_both, error_code);
+        socket.close();
+        if (handler.joinable()) {
+            handler.join();
+        }
+    }
+
+    u16 GetPort() {
+        return socket.local_endpoint().port();
+    }
+
+    std::string GetHost() {
+        return socket.local_endpoint().address().to_string();
+    }
+
+    void Run(const std::vector<InputCommon::CemuhookUDP::Response::TouchPad> touch_movement_path) {
+        constexpr size_t HeaderSize = sizeof(InputCommon::CemuhookUDP::Header);
+        constexpr size_t PadDataSize =
+            sizeof(InputCommon::CemuhookUDP::Message<InputCommon::CemuhookUDP::Response::PadData>);
+
+        REQUIRE(touch_movement_path.size() > 0);
+        is_running = true;
+        handler = std::thread([touch_movement_path, this]() {
+            auto current_touch_position = touch_movement_path.begin();
+            while (is_running) {
+                boost::asio::ip::udp::endpoint sender_endpoint;
+                boost::system::error_code error_code;
+                auto received_size = socket.receive_from(boost::asio::buffer(receive_buffer),
+                                                         sender_endpoint, 0, error_code);
+
+                if (received_size < HeaderSize) {
+                    continue;
+                }
+
+                InputCommon::CemuhookUDP::Header header{};
+                std::memcpy(&header, receive_buffer.data(), HeaderSize);
+                switch (header.type) {
+                case InputCommon::CemuhookUDP::Type::PadData: {
+                    InputCommon::CemuhookUDP::Response::PadData pad_data{};
+                    pad_data.touch[0] = *current_touch_position;
+                    const auto pad_message = InputCommon::CemuhookUDP::CreateMessage(
+                        InputCommon::CemuhookUDP::SERVER_MAGIC, pad_data, 0);
+                    std::memcpy(send_buffer.data(), &pad_message, PadDataSize);
+                    socket.send_to(boost::asio::buffer(send_buffer, PadDataSize), sender_endpoint,
+                                   0, error_code);
+
+                    bool can_advance =
+                        std::next(current_touch_position) != touch_movement_path.end();
+                    if (can_advance) {
+                        std::advance(current_touch_position, 1);
+                    }
+                    break;
+                }
+                case InputCommon::CemuhookUDP::Type::PortInfo:
+                case InputCommon::CemuhookUDP::Type::Version:
+                default:
+                    break;
+                }
+            }
+        });
+    }
+
+private:
+    boost::asio::io_service io_service;
+    boost::asio::ip::udp::socket socket;
+    std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer;
+    std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer;
+    bool is_running = false;
+    std::thread handler;
+};
+
+TEST_CASE("CalibrationConfigurationJob completed", "[input_common]") {
+    Common::Event complete_event;
+    FakeCemuhookServer server;
+    server.Run({{
+                    .is_active = 1,
+                    .x = 0,
+                    .y = 0,
+                },
+                {
+                    .is_active = 1,
+                    .x = 200,
+                    .y = 200,
+                }});
+
+    InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status status{};
+    u16 min_x{};
+    u16 min_y{};
+    u16 max_x{};
+    u16 max_y{};
+    InputCommon::CemuhookUDP::CalibrationConfigurationJob job(
+        server.GetHost(), server.GetPort(),
+        [&status,
+         &complete_event](InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status status_) {
+            status = status_;
+            if (status ==
+                InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status::Completed) {
+                complete_event.Set();
+            }
+        },
+        [&](u16 min_x_, u16 min_y_, u16 max_x_, u16 max_y_) {
+            min_x = min_x_;
+            min_y = min_y_;
+            max_x = max_x_;
+            max_y = max_y_;
+        });
+
+    complete_event.WaitUntil(std::chrono::system_clock::now() + std::chrono::seconds(10));
+    REQUIRE(status == InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status::Completed);
+    REQUIRE(min_x == 0);
+    REQUIRE(min_y == 0);
+    REQUIRE(max_x == 200);
+    REQUIRE(max_y == 200);
+}