diff --git a/package-lock.json b/package-lock.json
index d34f7fd74896711b37ea3b54987946af47961782..143aaff3ba69fa30b0bd36c31bafd249a7ff1936 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
         "rollup-plugin-gzip": "^4.0.1",
         "typescript": "^5.7.3",
         "vite": "^6.0.11",
+        "vitest": "^3.0.5",
         "vue-tsc": "^2.2.0"
       }
     },
@@ -1252,6 +1253,129 @@
         "vue": "^3.2.25"
       }
     },
+    "node_modules/@vitest/expect": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
+      "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.0.5",
+        "@vitest/utils": "3.0.5",
+        "chai": "^5.1.2",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
+      "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.0.5",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0 || ^6.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
+      "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
+      "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "3.0.5",
+        "pathe": "^2.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
+      "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.0.5",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
+      "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^3.0.2"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
+      "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.0.5",
+        "loupe": "^3.1.2",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "node_modules/@volar/language-core": {
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz",
@@ -1544,6 +1668,16 @@
         "node": ">= 8"
       }
     },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1740,6 +1874,16 @@
         "ieee754": "^1.2.1"
       }
     },
+    "node_modules/cac": {
+      "version": "6.7.14",
+      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+      "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/call-bind-apply-helpers": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
@@ -1802,6 +1946,23 @@
       ],
       "license": "CC-BY-4.0"
     },
+    "node_modules/chai": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+      "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1830,6 +1991,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/check-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
     "node_modules/chokidar": {
       "version": "3.5.3",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -2193,12 +2364,13 @@
       "license": "MIT"
     },
     "node_modules/debug": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -2209,6 +2381,16 @@
         }
       }
     },
+    "node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2390,6 +2572,13 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-module-lexer": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
+      "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/es-object-atoms": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -2480,6 +2669,16 @@
         "node": ">=0.8.x"
       }
     },
+    "node_modules/expect-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
+      "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/fast-copy": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
@@ -3063,6 +3262,13 @@
       "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
       "dev": true
     },
+    "node_modules/loupe": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
+      "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/lru-cache": {
       "version": "11.0.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
@@ -3073,9 +3279,9 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.14",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
-      "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
       "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
@@ -3230,10 +3436,11 @@
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/muggle-string": {
       "version": "0.4.1",
@@ -3426,6 +3633,23 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathval": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
     "node_modules/picocolors": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4403,6 +4627,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -4461,6 +4692,13 @@
         "node": ">= 10.x"
       }
     },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -4470,6 +4708,13 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/std-env": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
+      "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -4606,6 +4851,50 @@
         "real-require": "^0.2.0"
       }
     },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinypool": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
+      "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tinyspy": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+      "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4817,6 +5106,99 @@
         }
       }
     },
+    "node_modules/vite-node": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
+      "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.4.0",
+        "es-module-lexer": "^1.6.0",
+        "pathe": "^2.0.2",
+        "vite": "^5.0.0 || ^6.0.0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
+      "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "3.0.5",
+        "@vitest/mocker": "3.0.5",
+        "@vitest/pretty-format": "^3.0.5",
+        "@vitest/runner": "3.0.5",
+        "@vitest/snapshot": "3.0.5",
+        "@vitest/spy": "3.0.5",
+        "@vitest/utils": "3.0.5",
+        "chai": "^5.1.2",
+        "debug": "^4.4.0",
+        "expect-type": "^1.1.0",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.2",
+        "std-env": "^3.8.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.2",
+        "tinypool": "^1.0.2",
+        "tinyrainbow": "^2.0.0",
+        "vite": "^5.0.0 || ^6.0.0",
+        "vite-node": "3.0.5",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/debug": "^4.1.12",
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "@vitest/browser": "3.0.5",
+        "@vitest/ui": "3.0.5",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/debug": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/vscode-uri": {
       "version": "3.0.8",
       "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
@@ -4911,6 +5293,23 @@
         "node": ">= 8"
       }
     },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/package.json b/package.json
index 6dc5c9da13740623bd990a08c9de7be5e13c8363..bc019ac47d90e9ca7d0cbfcdf94e573e29e3599c 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
     "dev": "concurrently -n Client,Server -c '#325D79,#45ADA8' 'npm:dev:client' 'npm:dev:server'",
     "dev:client": "vite",
     "dev:server": "nodemon --use-strict src/server/app.js | pino-pretty -t 'yyyy-mm-dd HH:MM:ss'",
+    "test": "NODE_ENV='production' vitest",
     "build": "vue-tsc && vite build",
     "build:db": "node src/scripts/buildDatabase.js",
     "stats": "node src/scripts/displayStatistics",
@@ -56,6 +57,7 @@
     "rollup-plugin-gzip": "^4.0.1",
     "typescript": "^5.7.3",
     "vite": "^6.0.11",
+    "vitest": "^3.0.5",
     "vue-tsc": "^2.2.0"
   }
 }
diff --git a/src/server/config/env.test.js b/src/server/config/env.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..f026aa22e75fd81a8d0c3614ae0471a1e80adde3
--- /dev/null
+++ b/src/server/config/env.test.js
@@ -0,0 +1,69 @@
+// ABSD
+// Copyright (C) 2023 Institut Pasteur
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import { describe, expect, test } from 'vitest'
+import serverConfig from './env'
+
+describe('Server env config', () => {
+  test('Config should exists', () => {
+    expect(serverConfig).toBeDefined()
+  })
+
+  test('Config should have all and only the defined properties', () => {
+    const envVariables = Object.keys(serverConfig).sort()
+    expect(envVariables).toStrictEqual([
+      "ABSD_DB_HOST",
+      "ABSD_DB_NAME",
+      "ABSD_DB_PORT",
+      "ABSD_SERVER_HOST",
+      "ABSD_SERVER_PORT",
+      "ABSD_SERVER_PROXY",
+      "NODE_ENV",
+    ])
+  })
+
+  test('ABSD_DB_HOST should be a string', () => {
+    expect(serverConfig.ABSD_DB_HOST).toBeTypeOf('string')
+  })
+
+  test('ABSD_DB_NAME should be a string', () => {
+    expect(serverConfig.ABSD_DB_NAME).toBeTypeOf('string')
+  })
+
+  test('ABSD_DB_PORT should be a positive integer', () => {
+    expect(serverConfig.ABSD_DB_PORT).toBeTypeOf('number')
+    expect(serverConfig.ABSD_DB_PORT).toBeGreaterThan(0)
+    expect(serverConfig.ABSD_DB_PORT % 1).toBe(0)
+  })
+
+  test('ABSD_SERVER_HOST should be a string', () => {
+    expect(serverConfig.ABSD_SERVER_HOST).toBeTypeOf('string')
+  })
+
+  test('ABSD_SERVER_PORT should be a positive integer', () => {
+    expect(serverConfig.ABSD_SERVER_PORT).toBeTypeOf('number')
+    expect(serverConfig.ABSD_SERVER_PORT).toBeGreaterThan(0)
+    expect(serverConfig.ABSD_SERVER_PORT % 1).toBe(0)
+  })
+
+  test('ABSD_SERVER_PROXY should be a boolean', () => {
+    expect(serverConfig.ABSD_SERVER_PROXY).toBeTypeOf('boolean')
+  })
+
+  test('NODE_ENV should be either development or production', () => {
+    expect(serverConfig.NODE_ENV).toBeOneOf(['development', 'production'])
+  })
+})
diff --git a/src/server/models/Antibody.test.js b/src/server/models/Antibody.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..7dd24c344dbdc7f03a9a42db11e63f6cf29c4119
--- /dev/null
+++ b/src/server/models/Antibody.test.js
@@ -0,0 +1,140 @@
+// ABSD
+// Copyright (C) 2023 Institut Pasteur
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import { describe, expect, test } from "vitest"
+import { Antibody } from "./Antibody"
+
+describe('Antibody class', () => {
+  test('should be a class', () => {
+    const antibody = new Antibody()
+    expect(antibody).toBeInstanceOf(Antibody)
+  })
+
+  test('should define the static method "listSources"', () => {
+    expect(Antibody.listSources).toBeInstanceOf(Function)
+  })
+
+  test('should define the static method "listSegments"', () => {
+    expect(Antibody.listSegments).toBeInstanceOf(Function)
+  })
+
+  test('should define the static method "generateHashId"', () => {
+    expect(Antibody.generateHashId).toBeInstanceOf(Function)
+  })
+
+  test('should define the static method "toFasta"', () => {
+    expect(Antibody.toFasta).toBeInstanceOf(Function)
+  })
+
+  test('should define the static method "toString"', () => {
+    expect(Antibody.toString).toBeInstanceOf(Function)
+  })
+})
+
+describe('Antibody class methods', () => {
+  const antibody = {
+    "id": "368.22.A.0062",
+    "species": "Homo sapiens",
+    "hashId": "53775feae7c2d86939d15177e0b963e20504141de8601e28ab701f9f4fe9d3a2",
+    "lightChain": {
+      "headers": [
+        {
+          "id": "368.22.A.0062",
+          "vGeneSegment": "IGLV4",
+          "source": "CoV-AbDab",
+          "header": "368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_light_"
+        },
+        {
+          "id": "SRR17729722",
+          "vGeneSegment": "IGLV4",
+          "source": "PairedNGS",
+          "header": "PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light"
+        },
+        {
+          "id": "SRR17778139",
+          "vGeneSegment": "IGLV4",
+          "source": "PairedNGS",
+          "header": "PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light"
+        }
+      ],
+      "sequence": "QLVLTQSPSASASLGASVKLTCTLSSGHSSYAIAWHQQQPEKGPRYLMKLNSDGSHSEGDGIPDRFSGSSSGAERYLTISSLQSEDEADYYCQTWGTGIHVFGTGTKVTVL",
+      "rawFasta": "368.22.A.0062|||368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_light_;368.22.A.0062;IGLV4;CoV-AbDab|||PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light;SRR17729722;IGLV4;PairedNGS|||PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light;SRR17778139;IGLV4;PairedNGS"
+    },
+    "heavyChain": {
+      "headers": [
+        {
+          "id": "368.22.A.0062",
+          "vGeneSegment": "IGHV3",
+          "source": "CoV-AbDab",
+          "header": "368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_heavy_"
+        },
+        {
+          "id": "SRR17729722",
+          "vGeneSegment": "IGHV3",
+          "source": "PairedNGS",
+          "header": "PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy"
+        },
+        {
+          "id": "SRR17778139",
+          "vGeneSegment": "IGHV3",
+          "source": "PairedNGS",
+          "header": "PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy"
+        }
+      ],
+      "sequence": "QVQLVESGGGVVQPGRSLRLSCAASGFTFSGYAMHWVRQAPGKGLEWVAVISYDGSNRYYADSVKGRFSISRDNSKKTLYLQMNSLRDEDTAVYYCAKVEGGNYVGAFDVWGQGTMVTVSS",
+      "rawFasta": "368.22.A.0062|||368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_heavy_;368.22.A.0062;IGHV3;CoV-AbDab|||PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy;SRR17729722;IGHV3;PairedNGS|||PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy;SRR17778139;IGHV3;PairedNGS"
+    }
+  }
+
+  test('"listSources" should return the list of sources as a Set', () => {
+    const sources = Antibody.listSources(antibody)
+    const expected = new Set()
+      .add('CoV-AbDab')
+      .add('PairedNGS')
+    expect(sources).toStrictEqual(expected)
+  })
+
+  test('"listSegments" should return the list of gene segments as an object containg two Sets', () => {
+    const segments = Antibody.listSegments(antibody)
+    const expected = {
+      heavyChain: new Set().add('IGHV3'),
+      lightChain: new Set().add('IGLV4')
+    }
+    expect(segments).toStrictEqual(expected)
+  })
+
+  test('"generateHashId" should return the SHA256 of the antibody', () => {
+    const hashId = Antibody.generateHashId(antibody)
+    expect(hashId).toBe(antibody.hashId)
+  })
+
+  test('"toFasta" should return the FASTA version of the antibody', () => {
+    const fasta = Antibody.toFasta(antibody)
+    const expected = ">368.22.A.0062|||368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_light_;368.22.A.0062;IGLV4;CoV-AbDab|||PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light;SRR17729722;IGLV4;PairedNGS|||PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_light;SRR17778139;IGLV4;PairedNGS"
+      .concat("\nQLVLTQSPSASASLGASVKLTCTLSSGHSSYAIAWHQQQPEKGPRYLMKLNSDGSHSEGDGIPDRFSGSSSGAERYLTISSLQSEDEADYYCQTWGTGIHVFGTGTKVTVL")
+      .concat("\n>368.22.A.0062|||368.22.A.0062_B-cells SARS-CoV2 Human Patient_human_heavy_;368.22.A.0062;IGHV3;CoV-AbDab|||PRJNA777934|SRR17729722|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02,IGHG1|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy;SRR17729722;IGHV3;PairedNGS|||PRJNA777934|SRR17778139|human|GCTGCAGGTACCTACA|IGHV3_30*04,IGHD4_23*01,IGHJ3*02|IGLV4_69*01,IGLJ1*01,IGLC1|AKVEGGNYVGAFDV|QTWGTGIHV_heavy;SRR17778139;IGHV3;PairedNGS")
+      .concat("\nQVQLVESGGGVVQPGRSLRLSCAASGFTFSGYAMHWVRQAPGKGLEWVAVISYDGSNRYYADSVKGRFSISRDNSKKTLYLQMNSLRDEDTAVYYCAKVEGGNYVGAFDVWGQGTMVTVSS")
+
+    expect(fasta).toBe(expected)
+  })
+
+  test('"toString" should return the JSON version of the antibody', () => {
+    const json = Antibody.toString(antibody)
+    const expected = JSON.stringify(antibody)
+
+    expect(json).toBe(expected)
+  })
+})
diff --git a/vite.config.ts b/vite.config.ts
index 3bdd5fe50f1706e4186a71709382434ba240f19c..f0a8bc86dfa55eeded30af4d2845122fe633e4b2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,3 +1,4 @@
+/// <reference types='vitest' />
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import brotliPlugin from 'rollup-plugin-brotli'
@@ -29,5 +30,11 @@ export default defineConfig({
     __VUE_OPTIONS_API__: false,
     __VUE_PROD_DEVTOOLS__: false,
     __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
+  },
+  test: {
+    // https://vitest.dev/config/
+    name: 'Server tests',
+    root: 'src/server',
+    include: ['**/*.test.js']
   }
 })