an atproto pds written in F# (.NET 9) 馃
pds
fsharp
giraffe
dotnet
atproto
1module Auth.Tests
2
3open Xunit
4open PDSharp.Core.Auth
5open System
6open System.Collections.Concurrent
7
8/// Mock in-memory store for testing
9type VolatileAccountStore() =
10 let accounts = ConcurrentDictionary<string, Account>()
11 let handles = ConcurrentDictionary<string, string>()
12
13 interface IAccountStore with
14 member _.CreateAccount(account : Account) = async {
15 if handles.ContainsKey(account.Handle) then
16 return Error "Handle already taken"
17 elif accounts.ContainsKey(account.Did) then
18 return Error "Account already exists"
19 else
20 accounts.TryAdd(account.Did, account) |> ignore
21 handles.TryAdd(account.Handle, account.Did) |> ignore
22 return Ok()
23 }
24
25 member _.GetAccountByHandle(handle : string) = async {
26 match handles.TryGetValue(handle) with
27 | true, did ->
28 match accounts.TryGetValue(did) with
29 | true, acc -> return Some acc
30 | _ -> return None
31 | _ -> return None
32 }
33
34 member _.GetAccountByDid(did : string) = async {
35 match accounts.TryGetValue(did) with
36 | true, acc -> return Some acc
37 | _ -> return None
38 }
39
40[<Fact>]
41let ``Password hashing produces salt$hash format`` () =
42 let hash = hashPassword "mypassword"
43 Assert.Contains("$", hash)
44 let parts = hash.Split('$')
45 Assert.Equal(2, parts.Length)
46
47[<Fact>]
48let ``Password verification succeeds for correct password`` () =
49 let hash = hashPassword "mypassword"
50 Assert.True(verifyPassword "mypassword" hash)
51
52[<Fact>]
53let ``Password verification fails for wrong password`` () =
54 let hash = hashPassword "mypassword"
55 Assert.False(verifyPassword "wrongpassword" hash)
56
57[<Fact>]
58let ``Password verification fails for invalid hash format`` () =
59 Assert.False(verifyPassword "password" "invalidhash")
60 Assert.False(verifyPassword "password" "")
61
62[<Fact>]
63let ``JWT access token creation and validation`` () =
64 let secret = "test-secret-key-minimum-32-chars!"
65 let did = "did:web:test.example"
66
67 let token = createAccessToken secret did
68
69 let parts = token.Split('.')
70 Assert.Equal(3, parts.Length)
71
72 match validateToken secret token with
73 | Valid(extractedDid, tokenType, _) ->
74 Assert.Equal(did, extractedDid)
75 Assert.Equal(Access, tokenType)
76 | Invalid reason -> Assert.Fail $"Token should be valid, got: {reason}"
77
78[<Fact>]
79let ``JWT refresh token has correct type`` () =
80 let secret = "test-secret-key-minimum-32-chars!"
81 let did = "did:web:test.example"
82
83 let token = createRefreshToken secret did
84
85 match validateToken secret token with
86 | Valid(_, tokenType, _) -> Assert.Equal(Refresh, tokenType)
87 | Invalid reason -> Assert.Fail $"Token should be valid, got: {reason}"
88
89[<Fact>]
90let ``JWT validation fails with wrong secret`` () =
91 let secret = "test-secret-key-minimum-32-chars!"
92 let wrongSecret = "wrong-secret-key-minimum-32-chars!"
93 let did = "did:web:test.example"
94
95 let token = createAccessToken secret did
96
97 match validateToken wrongSecret token with
98 | Invalid _ -> Assert.True(true)
99 | Valid _ -> Assert.Fail "Token should be invalid with wrong secret"
100
101[<Fact>]
102let ``Account creation and lookup by handle`` () =
103 let store = VolatileAccountStore()
104
105 match
106 createAccount store "test.user" "password123" (Some "test@example.com")
107 |> Async.RunSynchronously
108 with
109 | Error msg -> Assert.Fail msg
110 | Ok account ->
111 Assert.Equal("test.user", account.Handle)
112 Assert.Equal("did:web:test.user", account.Did)
113 Assert.Equal(Some "test@example.com", account.Email)
114
115 let found =
116 (store :> IAccountStore).GetAccountByHandle "test.user"
117 |> Async.RunSynchronously
118
119 match found with
120 | None -> Assert.Fail "Account should be found"
121 | Some foundAcc -> Assert.Equal(account.Did, foundAcc.Did)
122
123[<Fact>]
124let ``Account creation fails for duplicate handle`` () =
125 let store = VolatileAccountStore()
126
127 createAccount store "duplicate.user" "password" None
128 |> Async.RunSynchronously
129 |> ignore
130
131 match createAccount store "duplicate.user" "password2" None |> Async.RunSynchronously with
132 | Error msg -> Assert.Contains("already", msg.ToLower())
133 | Ok _ -> Assert.Fail "Should fail for duplicate handle"
134
135[<Fact>]
136let ``Account lookup by DID`` () =
137 let store = VolatileAccountStore()
138
139 match createAccount store "did.user" "password123" None |> Async.RunSynchronously with
140 | Error msg -> Assert.Fail msg
141 | Ok account ->
142 let found =
143 (store :> IAccountStore).GetAccountByDid account.Did |> Async.RunSynchronously
144
145 match found with
146 | None -> Assert.Fail "Account should be found by DID"
147 | Some foundAcc -> Assert.Equal(account.Handle, foundAcc.Handle)