an atproto pds written in F# (.NET 9) 馃
pds
fsharp
giraffe
dotnet
atproto
1module Handlers.Tests
2
3open System
4open System.IO
5open System.Text
6open System.Text.Json
7open System.Threading.Tasks
8open System.Collections.Generic
9open Xunit
10open Microsoft.AspNetCore.Http
11open Giraffe
12open PDSharp.Core.Config
13open PDSharp.Core.BlockStore
14open PDSharp.Core
15open PDSharp.Core.SqliteStore
16open PDSharp.Core.Auth
17
18type MockAccountStore() =
19 let mutable accounts = Map.empty<string, Account>
20
21 interface IAccountStore with
22 member _.CreateAccount(account) = async {
23 if accounts.ContainsKey account.Did then
24 return Error "Exists"
25 else
26 accounts <- accounts.Add(account.Did, account)
27 return Ok()
28 }
29
30 member _.GetAccountByHandle(handle) = async {
31 return accounts |> Map.tryPick (fun _ v -> if v.Handle = handle then Some v else None)
32 }
33
34 member _.GetAccountByDid did = async { return accounts.TryFind did }
35
36type MockBlockStore() =
37 let mutable blocks = Map.empty<string, byte[]>
38
39 interface IBlockStore with
40 member _.Put(data) = async {
41 let hash = Crypto.sha256 data
42 let cid = Cid.FromHash hash
43 blocks <- blocks.Add(cid.ToString(), data)
44 return cid
45 }
46
47 member _.Get cid = async { return blocks.TryFind(cid.ToString()) }
48 member _.Has cid = async { return blocks.ContainsKey(cid.ToString()) }
49
50 member _.GetAllCidsAndData() = async {
51 return
52 blocks
53 |> Map.toList
54 |> List.choose (fun (k, v) -> Cid.TryParse k |> Option.map (fun c -> (c, v)))
55 }
56
57type MockRepoStore() =
58 let mutable repos = Map.empty<string, RepoRow>
59
60 interface IRepoStore with
61 member _.GetRepo(did) = async { return repos.TryFind did }
62 member _.SaveRepo(repo) = async { repos <- repos.Add(repo.did, repo) }
63
64type MockJsonSerializer() =
65 interface Giraffe.Json.ISerializer with
66 member _.SerializeToString x = JsonSerializer.Serialize x
67 member _.SerializeToBytes x = JsonSerializer.SerializeToUtf8Bytes x
68 member _.Deserialize<'T>(json : string) = JsonSerializer.Deserialize<'T> json
69
70 member _.Deserialize<'T>(bytes : byte[]) =
71 JsonSerializer.Deserialize<'T>(ReadOnlySpan bytes)
72
73 member _.DeserializeAsync<'T>(stream : Stream) = task { return! JsonSerializer.DeserializeAsync<'T>(stream) }
74
75 member _.SerializeToStreamAsync<'T> (x : 'T) (stream : Stream) = task {
76 do! JsonSerializer.SerializeAsync<'T>(stream, x)
77 }
78
79let mockContext (services : (Type * obj) list) (body : string) (query : Map<string, string>) =
80 let ctx = new DefaultHttpContext()
81 let serializer = MockJsonSerializer()
82 let allServices = (typeof<Giraffe.Json.ISerializer>, box serializer) :: services
83
84 let sp =
85 { new IServiceProvider with
86 member _.GetService(serviceType) =
87 allServices
88 |> List.tryPick (fun (t, s) -> if t = serviceType then Some s else None)
89 |> Option.toObj
90 }
91
92 ctx.RequestServices <- sp
93
94 if not (String.IsNullOrEmpty body) then
95 let stream = new MemoryStream(Encoding.UTF8.GetBytes(body))
96 ctx.Request.Body <- stream
97 ctx.Request.ContentLength <- stream.Length
98
99 if not query.IsEmpty then
100 let dict = Dictionary<string, Microsoft.Extensions.Primitives.StringValues>()
101
102 for kvp in query do
103 dict.Add(kvp.Key, Microsoft.Extensions.Primitives.StringValues(kvp.Value))
104
105 ctx.Request.Query <- QueryCollection dict
106
107 ctx
108
109[<Fact>]
110let ``Auth.createAccountHandler creates account successfully`` () = task {
111 let accountStore = MockAccountStore()
112
113 let config = {
114 PublicUrl = "https://pds.example.com"
115 DidHost = "did:web:pds.example.com"
116 JwtSecret = "secret"
117 SqliteConnectionString = ""
118 DisableWalAutoCheckpoint = false
119 BlobStore = Disk "blobs"
120 }
121
122 let services = [ typeof<AppConfig>, box config; typeof<IAccountStore>, box accountStore ]
123
124 let req : PDSharp.Handlers.Auth.CreateAccountRequest = {
125 handle = "alice.test"
126 email = Some "alice@test.com"
127 password = "password123"
128 inviteCode = None
129 }
130
131 let body = JsonSerializer.Serialize req
132 let ctx = mockContext services body Map.empty
133 let next : HttpFunc = fun _ -> Task.FromResult(None)
134 let! result = PDSharp.Handlers.Auth.createAccountHandler next ctx
135 Assert.Equal(200, ctx.Response.StatusCode)
136
137 let store = accountStore :> IAccountStore
138 let! accountOpt = store.GetAccountByHandle "alice.test"
139 Assert.True accountOpt.IsSome
140}
141
142[<Fact>]
143let ``Server.indexHandler returns HTML`` () = task {
144 let ctx = new DefaultHttpContext()
145 let next : HttpFunc = fun _ -> Task.FromResult(None)
146 let! result = PDSharp.Handlers.Server.indexHandler next ctx
147 Assert.Equal(200, ctx.Response.StatusCode)
148 Assert.Equal("text/html", ctx.Response.ContentType)
149}
150
151[<Fact>]
152let ``Repo.createRecordHandler invalid collection returns error`` () = task {
153 let blockStore = MockBlockStore()
154 let repoStore = MockRepoStore()
155 let keyStore = PDSharp.Handlers.SigningKeyStore()
156 let firehose = PDSharp.Handlers.FirehoseState()
157
158 let services = [
159 typeof<IBlockStore>, box blockStore
160 typeof<IRepoStore>, box repoStore
161 typeof<PDSharp.Handlers.SigningKeyStore>, box keyStore
162 typeof<PDSharp.Handlers.FirehoseState>, box firehose
163 ]
164
165 let record = JsonSerializer.Deserialize<JsonElement> "{\"text\":\"hello\"}"
166
167 let req : PDSharp.Handlers.Repo.CreateRecordRequest = {
168 repo = "did:web:alice.test"
169 collection = "app.bsky.feed.post"
170 record = record
171 rkey = None
172 }
173
174 let body = JsonSerializer.Serialize(req)
175 let ctx = mockContext services body Map.empty
176 let next : HttpFunc = fun _ -> Task.FromResult(None)
177 let! result = PDSharp.Handlers.Repo.createRecordHandler next ctx
178 Assert.Equal(400, ctx.Response.StatusCode)
179}