Set up client tests

These are definitely temporary tests, as testing the lcient API will become more difficult and require more infrastructure as real data starts entering the system.
This commit is contained in:
Savanni D'Gerinel 2025-02-16 14:10:28 -05:00
parent 182020e136
commit 41bb21c254
11 changed files with 3966 additions and 4146 deletions

324
Cargo.lock generated
View File

@ -29,18 +29,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check 0.9.5",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -137,16 +125,6 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde 1.0.217",
"serde_json",
]
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "1.9.0" version = "1.9.0"
@ -301,12 +279,6 @@ dependencies = [
"uuid 0.4.0", "uuid 0.4.0",
] ]
[[package]]
name = "auto-future"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "0.1.8" version = "0.1.8"
@ -324,14 +296,14 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.7.9" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [ dependencies = [
"async-trait",
"axum-core", "axum-core",
"axum-macros", "axum-macros",
"bytes", "bytes",
"form_urlencoded",
"futures-util", "futures-util",
"http 1.2.0", "http 1.2.0",
"http-body 1.0.1", "http-body 1.0.1",
@ -359,11 +331,10 @@ dependencies = [
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.4.5" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [ dependencies = [
"async-trait",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.2.0", "http 1.2.0",
@ -380,45 +351,15 @@ dependencies = [
[[package]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.4.2" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "axum-test"
version = "16.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e3a443d2608936a02a222da7b746eb412fede7225b3030b64fe9be99eab8dc"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum",
"bytes",
"bytesize",
"cookie 0.18.1",
"http 1.2.0",
"http-body-util",
"hyper 1.5.2",
"hyper-util",
"mime 0.3.17",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2",
"serde 1.0.217",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower",
"url 2.5.4",
]
[[package]] [[package]]
name = "az" name = "az"
version = "1.2.1" version = "1.2.1"
@ -577,12 +518,6 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "bytesize"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@ -646,7 +581,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "changeset" name = "changeset"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"uuid 1.12.0", "uuid 0.8.2",
] ]
[[package]] [[package]]
@ -812,16 +747,6 @@ dependencies = [
"version_check 0.9.5", "version_check 0.9.5",
] ]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"time 0.3.37",
"version_check 0.9.5",
]
[[package]] [[package]]
name = "cookie-factory" name = "cookie-factory"
version = "0.3.3" version = "0.3.3"
@ -1046,12 +971,6 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1220,18 +1139,6 @@ dependencies = [
"zune-inflate", "zune-inflate",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -1267,7 +1174,7 @@ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap",
"cookie 0.17.0", "cookie",
"cool_asserts", "cool_asserts",
"futures-util", "futures-util",
"hex-string", "hex-string",
@ -1656,6 +1563,18 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.11.4" version = "0.11.4"
@ -1983,15 +1902,6 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.2"
@ -2003,22 +1913,13 @@ dependencies = [
"foldhash", "foldhash",
] ]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [ dependencies = [
"hashbrown 0.15.2", "hashbrown",
] ]
[[package]] [[package]]
@ -2255,7 +2156,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
"tokio", "tokio",
"want",
] ]
[[package]] [[package]]
@ -2278,16 +2178,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel",
"futures-util", "futures-util",
"http 1.2.0", "http 1.2.0",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.5.2", "hyper 1.5.2",
"pin-project-lite", "pin-project-lite",
"socket2",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -2510,25 +2407,6 @@ dependencies = [
"tiff 0.9.1", "tiff 0.9.1",
] ]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "indent_write" name = "indent_write"
version = "2.2.0" version = "2.2.0"
@ -2542,7 +2420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.2", "hashbrown",
] ]
[[package]] [[package]]
@ -2852,9 +2730,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "md-5" name = "md-5"
@ -3555,16 +3433,6 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]] [[package]]
name = "pretty_env_logger" name = "pretty_env_logger"
version = "0.5.0" version = "0.5.0"
@ -3766,7 +3634,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
] ]
[[package]] [[package]]
@ -3956,16 +3824,6 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "reserve-port"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283"
dependencies = [
"lazy_static",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "result-extended" name = "result-extended"
version = "0.1.0" version = "0.1.0"
@ -3993,47 +3851,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags 2.8.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink 0.9.1",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rusqlite_migration"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923b42e802f7dc20a0a6b5e097ba7c83fe4289da07e49156fecf6af08aa9cd1c"
dependencies = [
"include_dir",
"log 0.4.25",
"rusqlite",
]
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 0.2.12",
"mime 0.3.17",
"mime_guess 2.0.5",
"rand 0.8.5",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@ -4271,6 +4088,20 @@ dependencies = [
"version_check 0.9.5", "version_check 0.9.5",
] ]
[[package]]
name = "server"
version = "0.1.0"
dependencies = [
"axum",
"result-extended",
"serde 1.0.217",
"thiserror 2.0.11",
"tokio",
"tower-http",
"typeshare",
"uuid 1.13.1",
]
[[package]] [[package]]
name = "sgf" name = "sgf"
version = "0.1.0" version = "0.1.0"
@ -4446,8 +4277,8 @@ dependencies = [
"futures-intrusive", "futures-intrusive",
"futures-io", "futures-io",
"futures-util", "futures-util",
"hashbrown 0.15.2", "hashbrown",
"hashlink 0.10.0", "hashlink",
"indexmap", "indexmap",
"log 0.4.25", "log 0.4.25",
"memchr", "memchr",
@ -4737,7 +4568,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"getrandom", "getrandom 0.2.15",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -5279,12 +5110,6 @@ dependencies = [
"percent-encoding 2.3.1", "percent-encoding 2.3.1",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -5325,17 +5150,17 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
"serde 1.0.217", "serde 1.0.217",
] ]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.12.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.1",
] ]
[[package]] [[package]]
@ -5368,37 +5193,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "visions"
version = "0.1.0"
dependencies = [
"async-std",
"async-trait",
"authdb",
"axum",
"axum-test",
"chrono",
"cool_asserts",
"futures",
"include_dir",
"lazy_static",
"mime 0.3.17",
"mime_guess 2.0.5",
"pretty_env_logger",
"result-extended",
"rusqlite",
"rusqlite_migration",
"serde 1.0.217",
"serde_json",
"thiserror 2.0.11",
"tokio",
"tokio-stream",
"tower-http",
"typeshare",
"urlencoding",
"uuid 1.12.0",
]
[[package]] [[package]]
name = "wait-timeout" name = "wait-timeout"
version = "0.2.0" version = "0.2.0"
@ -5458,6 +5252,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasite" name = "wasite"
version = "0.1.0" version = "0.1.0"
@ -5768,6 +5571,15 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"
@ -5780,12 +5592,6 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yansi-term" name = "yansi-term"
version = "0.1.2" version = "0.1.2"

View File

@ -10,3 +10,7 @@ tasks:
- npm install typescript - npm install typescript
- typeshare --lang typescript --output-file gen/types.ts ../server/src - typeshare --lang typescript --output-file gen/types.ts ../server/src
- npx tsc - npx tsc
test:
cmds:
- npx jest src/

View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: "node",
transform: {
"^.+.tsx?$": ["ts-jest",{}],
},
};

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@
"tabWidth": 4 "tabWidth": 4
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"prettier": "^3.5.1", "prettier": "^3.5.1",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"

View File

@ -0,0 +1,97 @@
import { Connection } from './client'
describe('what happens in an authentication', () => {
it('handles a successful response', async () => {
let client = new Connection(new URL('http://127.0.0.1:8001'))
let response = await client.auth('vakarian', 'aoeu')
expect(response).toEqual({
status: 'ok',
content: 'vakarian-session-id',
})
})
it('handles an authentication failure', async () => {
let client = new Connection(new URL('http://127.0.0.1:8001'))
{
let response = await client.auth('vakarian', '')
expect(response).toEqual({ status: 'unauthorized' })
}
{
let response = await client.auth('grunt', '')
expect(response).toEqual({ status: 'unauthorized' })
}
})
it('handles a password-reset condition', async () => {
let client = new Connection(new URL('http://127.0.0.1:8001'))
{
let response = await client.auth('shephard', 'aoeu')
expect(response).toEqual({
status: 'password-reset',
content: 'shephard-session-id',
})
}
{
let response = await client.auth('shephard', '')
expect(response).toEqual({ status: 'unauthorized' })
}
})
it('lists users on an authenticated connection', async () => {
let client = new Connection(new URL('http://127.0.0.1:8001'))
{
let authResponse = await client.auth('vakarian', 'aoeu')
if (authResponse.status === 'ok') {
let sessionId = authResponse.content
let response = await client.listUsers(sessionId)
expect(response).toEqual({
status: 'ok',
content: [
{
id: 'vakarian-id',
name: 'vakarian',
status: { type: 'ok', content: undefined },
},
{
id: 'shephard-id',
name: 'shephard',
status: {
type: 'password-reset',
content: '2050-01-01 00:00:00',
},
},
{
id: 'tali-id',
name: 'tali',
status: { type: 'locked', content: undefined },
},
],
})
} else {
throw new Error('authorization should have been ok')
}
}
{
let authResponse = await client.auth('shephard', 'aoeu')
if (authResponse.status === 'password-reset') {
let sessionId = authResponse.content
let response = await client.listUsers(sessionId)
expect(response).toEqual({ status: 'unauthorized' })
} else {
throw new Error('authorization should have been password-reset')
}
}
/*
{
let response = await client.listUsers('');
expect(response).toEqual({ status: 'unauthorized' })
}
{
let authResponse = await client.auth('shephard', 'aoeu')
let response = await client.listUsers(authResponse.content);
expect(response).toEqual({ status: 'password-reset' })
}
*/
})
})

View File

@ -1,20 +1,22 @@
import { VResponse, SessionId } from '../gen/types' import { VResponse, SessionId, UserOverview } from '../gen/types'
export interface Client { export interface Client {
auth: ( auth: (
username: string, username: string,
password: string, password: string,
) => Promise<ClientResponse<SessionId>> ) => Promise<ClientResponse<SessionId>>
listUsers: (sessionId: SessionId) => Promise<ClientResponse<UserOverview[]>>
} }
export type ClientResponse<A> = export type ClientResponse<A> =
| { status: 'ok'; content: VResponse<A> } | { status: 'ok'; content: A }
| { status: 'password-reset'; content: SessionId }
| { status: 'unauthorized' } | { status: 'unauthorized' }
| { status: 'unexpected'; code: number } | { status: 'unexpected'; code: number }
export class Connection implements Client { export class Connection implements Client {
private base: URL private base: URL
// private sessionId: string | undefined;
constructor(baseUrl: URL) { constructor(baseUrl: URL) {
this.base = baseUrl this.base = baseUrl
@ -25,14 +27,46 @@ export class Connection implements Client {
password: string, password: string,
): Promise<ClientResponse<SessionId>> { ): Promise<ClientResponse<SessionId>> {
const url = new URL(this.base) const url = new URL(this.base)
url.pathname = `/api/v1/auth` url.pathname = `/api/test/auth`
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: [['Content-Type', 'application/json']], headers: [['Content-Type', 'application/json']],
body: JSON.stringify({ username: username, password: password }), body: JSON.stringify({ username: username, password: password }),
}) })
if (response.ok) { if (response.ok) {
return await response.json() let resp = await response.json()
switch (resp.type) {
case 'success':
return { status: 'ok', content: resp.content }
case 'password-reset':
return { status: 'password-reset', content: resp.content }
}
return { status: 'ok', content: resp }
} else if (response.status == 401) {
return { status: 'unauthorized' }
} else {
return { status: 'unexpected', code: response.status }
}
}
async listUsers(
sessionId: SessionId,
): Promise<ClientResponse<UserOverview[]>> {
const url = new URL(this.base)
url.pathname = `/api/test/list-users`
const response = await fetch(url, {
method: 'GET',
headers: [['Authorization', `Bearer ${sessionId}`]],
})
if (response.ok) {
let resp = await response.json()
switch (resp.type) {
case 'success':
return { status: 'ok', content: resp.content }
case 'password-reset':
return { status: 'password-reset', content: resp.content }
}
return { status: 'ok', content: resp }
} else if (response.status == 401) { } else if (response.status == 401) {
return { status: 'unauthorized' } return { status: 'unauthorized' }
} else { } else {

View File

@ -10,3 +10,5 @@ tokio = { version = "1.43.0", features = ["full", "rt"] }
tower-http = { version = "0.6.2", features = ["cors"] } tower-http = { version = "0.6.2", features = ["cors"] }
typeshare = "1.0.4" typeshare = "1.0.4"
uuid = { version = "1.13.1", features = ["v4"] } uuid = { version = "1.13.1", features = ["v4"] }
result-extended = { path = "../../result-extended" }
thiserror = "2.0.11"

View File

@ -1,19 +1,39 @@
use axum::{http::{Method, StatusCode}, routing::{get, post}, Json, Router}; use std::future::Future;
use axum::{
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, Method, StatusCode,
},
routing::{get, post},
Json, Router,
};
use result_extended::{error, ok, ResultExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use typeshare::typeshare; use typeshare::typeshare;
use uuid::Uuid; use uuid::Uuid;
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "content", rename_all = "kebab-case")]
#[typeshare]
enum AccountStatus {
Ok,
PasswordReset(String),
Locked,
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[typeshare] #[typeshare]
struct AuthRequest { struct AuthRequest {
username: String, username: String,
password: String password: String,
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[typeshare] #[typeshare]
pub struct SessionId(String); struct SessionId(String);
impl SessionId { impl SessionId {
pub fn new() -> Self { pub fn new() -> Self {
@ -37,42 +57,183 @@ impl From<String> for SessionId {
} }
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[typeshare]
struct UserId(String);
impl UserId {
pub fn new() -> Self {
Self(format!("{}", Uuid::new_v4().hyphenated()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for UserId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for UserId {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "content")] #[typeshare]
struct UserOverview {
id: UserId,
name: String,
status: AccountStatus,
}
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "content", rename_all = "kebab-case")]
#[typeshare] #[typeshare]
enum VResponse<A> { enum VResponse<A> {
Success(A), Success(A),
PasswordReset(A), PasswordReset(SessionId),
Nothing,
} }
#[axum::debug_handler] #[axum::debug_handler]
async fn check_password(request: Json<AuthRequest>) -> (StatusCode, Json<Option<VResponse<SessionId>>>) { async fn check_password(
request: Json<AuthRequest>,
) -> (StatusCode, Json<Option<VResponse<SessionId>>>) {
let Json(request) = request; let Json(request) = request;
if request.username == "vakarian" && request.password == "aoeu" { if request.username == "vakarian" && request.password == "aoeu" {
(StatusCode::OK, Json(Some(VResponse::Success("vakarian-session-id".into())))) (
StatusCode::OK,
Json(Some(VResponse::Success("vakarian-session-id".into()))),
)
} else if request.username == "shephard" && request.password == "aoeu" { } else if request.username == "shephard" && request.password == "aoeu" {
(StatusCode::OK, Json(Some(VResponse::PasswordReset("shephard-session-id".into())))) (
StatusCode::OK,
Json(Some(VResponse::PasswordReset("shephard-session-id".into()))),
)
} else { } else {
(StatusCode::UNAUTHORIZED, Json(None)) (StatusCode::UNAUTHORIZED, Json(None))
} }
} }
#[derive(Debug, Error)]
enum AppError {
#[error("no user authorized")]
Unauthorized,
#[error("bad request")]
BadRequest,
}
#[derive(Debug, Error)]
enum FatalError {
#[error("on unknown fatal error occurred")]
Unknown,
}
impl result_extended::FatalError for FatalError {}
fn parse_session_header(headers: HeaderMap) -> ResultExt<Option<SessionId>, AppError, FatalError> {
match headers.get("Authorization") {
Some(token) => {
match token
.to_str()
.unwrap()
.split(" ")
.collect::<Vec<&str>>()
.as_slice()
{
[_schema, token] => ok(Some(SessionId::from(*token))),
_ => error(AppError::BadRequest),
}
}
None => ok(None),
}
}
async fn auth_required<B, F, Fut>(headers: HeaderMap, f: F) -> (StatusCode, Json<VResponse<B>>)
where
F: Fn() -> Fut,
Fut: Future<Output = (StatusCode, B)>,
{
match parse_session_header(headers) {
ResultExt::Ok(Some(session_id)) => {
if session_id == "vakarian-session-id".into() {
let (code, result) = f().await;
(code, Json(VResponse::Success(result)))
} else if session_id == "shephard-id".into() {
(StatusCode::OK, Json(VResponse::PasswordReset("shephard-session-id".into())))
} else {
(StatusCode::UNAUTHORIZED, Json(VResponse::Nothing))
}
}
ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(VResponse::Nothing)),
ResultExt::Err(AppError::Unauthorized) => (StatusCode::UNAUTHORIZED, Json(VResponse::Nothing)),
ResultExt::Err(AppError::BadRequest) => (StatusCode::BAD_REQUEST, Json(VResponse::Nothing)),
ResultExt::Fatal(err) => {
panic!("{}", err);
}
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let app = Router::new() let app = Router::new()
.route( .route(
"/api/v1/health", "/api/test/health",
get(|| async { (StatusCode::OK, Json(None::<String>)) }), get(|| async { (StatusCode::OK, Json(None::<String>)) }),
).layer( )
.layer(
CorsLayer::new() CorsLayer::new()
.allow_methods([Method::GET]).allow_origin(Any), .allow_methods([Method::GET])
.allow_origin(Any),
) )
.route( .route(
"/api/v1/auth", "/api/test/auth",
post(|req: Json<AuthRequest>| check_password(req)), post(|req: Json<AuthRequest>| check_password(req)),
).layer( )
.layer(
CorsLayer::new() CorsLayer::new()
.allow_methods([Method::POST]).allow_origin(Any), .allow_methods([Method::POST])
.allow_origin(Any),
)
.route(
"/api/test/list-users",
get(|headers: HeaderMap| {
auth_required(headers, || async {
(
StatusCode::OK,
Some(vec![
UserOverview {
id: "vakarian-id".into(),
name: "vakarian".to_owned(),
status: AccountStatus::Ok,
},
UserOverview {
id: "shephard-id".into(),
name: "shephard".to_owned(),
status: AccountStatus::PasswordReset(
"2050-01-01 00:00:00".to_owned(),
),
},
UserOverview {
id: "tali-id".into(),
name: "tali".to_owned(),
status: AccountStatus::Locked,
},
]),
)
})
}),
)
.layer(
CorsLayer::new()
.allow_headers([AUTHORIZATION])
.allow_methods([Method::GET])
.allow_origin(Any),
); );
let listener = tokio::net::TcpListener::bind("127.0.0.1:8001") let listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
.await .await

View File

@ -1,53 +0,0 @@
import { Client, ReqResponse, SessionId } from "./client";
class MockClient implements Client {
users: { [_: string]: string }
constructor() {
this.users = { 'vakarian': 'aoeu', 'shephard': 'aoeu' }
}
async auth(username: string, password: string): Promise<ReqResponse<SessionId>> {
if (this.users[username] == password) {
if (username == 'shephard') {
return { type: 'password-reset' }
}
return { type: "ok", content: "auth-successful" }
} else {
return { type: "error", content: 401 }
}
}
}
describe("what happens in an authentication", () => {
it("handles a successful response", async () => {
let client = new MockClient()
let response = await client.auth("vakarian", "aoeu")
expect(response).toEqual({ type: "ok", content: "auth-successful" })
})
it("handles an authentication failure", async () => {
let client = new MockClient();
{
let response = await client.auth("vakarian", "")
expect(response).toEqual({ type: "error", content: 401 });
}
{
let response = await client.auth("grunt", "")
expect(response).toEqual({ type: "error", content: 401 });
}
})
it("handles a password-reset condition", async () => {
let client = new MockClient();
{
let response = await client.auth("shephard", "aoeu")
expect(response).toEqual({ type: "password-reset" });
}
{
let response = await client.auth("shephard", "")
expect(response).toEqual({ type: "error", content: 401 });
}
})
})

View File

@ -1,11 +0,0 @@
export type UserId = string;
export type SessionId = string;
export type ReqResponse<A> = { type: "ok", content: A } | { type: "password-reset" } | { type: "error", content: number }
export interface Client {
auth: (username: string, password: string) => Promise<ReqResponse<SessionId>>
// createUser: (sessionId: SessionId, username: string) => Promise<ReqResponse<UserId>>
// deleteUser: (sessionId: SessionId, userId: string) => Promise<ReqResponse<void>>
}