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"
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]]
name = "aho-corasick"
version = "1.1.3"
@ -137,16 +125,6 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "async-channel"
version = "1.9.0"
@ -301,12 +279,6 @@ dependencies = [
"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]]
name = "autocfg"
version = "0.1.8"
@ -324,14 +296,14 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.7.9"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
@ -359,11 +331,10 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.4.5"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.2.0",
@ -380,45 +351,15 @@ dependencies = [
[[package]]
name = "axum-macros"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"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]]
name = "az"
version = "1.2.1"
@ -577,12 +518,6 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "bytesize"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]]
name = "cairo-rs"
version = "0.18.5"
@ -646,7 +581,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "changeset"
version = "0.1.0"
dependencies = [
"uuid 1.12.0",
"uuid 0.8.2",
]
[[package]]
@ -812,16 +747,6 @@ dependencies = [
"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]]
name = "cookie-factory"
version = "0.3.3"
@ -1046,12 +971,6 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
@ -1220,18 +1139,6 @@ dependencies = [
"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]]
name = "fastrand"
version = "2.3.0"
@ -1267,7 +1174,7 @@ dependencies = [
"bytes",
"chrono",
"clap",
"cookie 0.17.0",
"cookie",
"cool_asserts",
"futures-util",
"hex-string",
@ -1656,6 +1563,18 @@ dependencies = [
"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]]
name = "gif"
version = "0.11.4"
@ -1983,15 +1902,6 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
@ -2003,22 +1913,13 @@ dependencies = [
"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]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.2",
"hashbrown",
]
[[package]]
@ -2255,7 +2156,6 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
@ -2278,16 +2178,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.5.2",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
@ -2510,25 +2407,6 @@ dependencies = [
"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]]
name = "indent_write"
version = "2.2.0"
@ -2542,7 +2420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
"hashbrown",
]
[[package]]
@ -2852,9 +2730,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
@ -3555,16 +3433,6 @@ dependencies = [
"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]]
name = "pretty_env_logger"
version = "0.5.0"
@ -3766,7 +3634,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.15",
]
[[package]]
@ -3956,16 +3824,6 @@ dependencies = [
"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]]
name = "result-extended"
version = "0.1.0"
@ -3993,47 +3851,6 @@ dependencies = [
"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]]
name = "rustc-demangle"
version = "0.1.24"
@ -4271,6 +4088,20 @@ dependencies = [
"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]]
name = "sgf"
version = "0.1.0"
@ -4446,8 +4277,8 @@ dependencies = [
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.2",
"hashlink 0.10.0",
"hashbrown",
"hashlink",
"indexmap",
"log 0.4.25",
"memchr",
@ -4737,7 +4568,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"getrandom 0.2.15",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@ -5279,12 +5110,6 @@ dependencies = [
"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]]
name = "utf-8"
version = "0.7.6"
@ -5325,17 +5150,17 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"getrandom 0.2.15",
"serde 1.0.217",
]
[[package]]
name = "uuid"
version = "1.12.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [
"getrandom",
"getrandom 0.3.1",
]
[[package]]
@ -5368,37 +5193,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "wait-timeout"
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"
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]]
name = "wasite"
version = "0.1.0"
@ -5768,6 +5571,15 @@ dependencies = [
"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]]
name = "write16"
version = "1.0.0"
@ -5780,12 +5592,6 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yansi-term"
version = "0.1.2"

View File

@ -10,3 +10,7 @@ tasks:
- npm install typescript
- typeshare --lang typescript --output-file gen/types.ts ../server/src
- 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
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"prettier": "^3.5.1",
"ts-jest": "^29.2.5",
"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 {
auth: (
username: string,
password: string,
) => Promise<ClientResponse<SessionId>>
listUsers: (sessionId: SessionId) => Promise<ClientResponse<UserOverview[]>>
}
export type ClientResponse<A> =
| { status: 'ok'; content: VResponse<A> }
| { status: 'ok'; content: A }
| { status: 'password-reset'; content: SessionId }
| { status: 'unauthorized' }
| { status: 'unexpected'; code: number }
export class Connection implements Client {
private base: URL
// private sessionId: string | undefined;
constructor(baseUrl: URL) {
this.base = baseUrl
@ -25,14 +27,46 @@ export class Connection implements Client {
password: string,
): Promise<ClientResponse<SessionId>> {
const url = new URL(this.base)
url.pathname = `/api/v1/auth`
url.pathname = `/api/test/auth`
const response = await fetch(url, {
method: 'POST',
headers: [['Content-Type', 'application/json']],
body: JSON.stringify({ username: username, password: password }),
})
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) {
return { status: 'unauthorized' }
} else {

View File

@ -10,3 +10,5 @@ tokio = { version = "1.43.0", features = ["full", "rt"] }
tower-http = { version = "0.6.2", features = ["cors"] }
typeshare = "1.0.4"
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 thiserror::Error;
use tower_http::cors::{Any, CorsLayer};
use typeshare::typeshare;
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)]
#[typeshare]
struct AuthRequest {
username: String,
password: String
password: String,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[typeshare]
pub struct SessionId(String);
struct SessionId(String);
impl SessionId {
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)]
#[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]
enum VResponse<A> {
Success(A),
PasswordReset(A),
PasswordReset(SessionId),
Nothing,
}
#[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;
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" {
(StatusCode::OK, Json(Some(VResponse::PasswordReset("shephard-session-id".into()))))
(
StatusCode::OK,
Json(Some(VResponse::PasswordReset("shephard-session-id".into()))),
)
} else {
(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]
async fn main() {
let app = Router::new()
.route(
"/api/v1/health",
"/api/test/health",
get(|| async { (StatusCode::OK, Json(None::<String>)) }),
).layer(
)
.layer(
CorsLayer::new()
.allow_methods([Method::GET]).allow_origin(Any),
.allow_methods([Method::GET])
.allow_origin(Any),
)
.route(
"/api/v1/auth",
"/api/test/auth",
post(|req: Json<AuthRequest>| check_password(req)),
).layer(
)
.layer(
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")
.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>>
}