Resolve AT Protocol DIDs, handles, and schemas with intelligent caching for Laravel
1<?php
2
3namespace SocialDept\AtpResolver\Tests\Unit;
4
5use PHPUnit\Framework\TestCase;
6use SocialDept\AtpResolver\Contracts\CacheStore;
7use SocialDept\AtpResolver\Contracts\DidResolver;
8use SocialDept\AtpResolver\Contracts\HandleResolver;
9use SocialDept\AtpResolver\Data\DidDocument;
10use SocialDept\AtpResolver\Resolver;
11
12class ResolverTest extends TestCase
13{
14 public function test_it_can_resolve_pds_from_did(): void
15 {
16 $didResolver = $this->createMock(DidResolver::class);
17 $handleResolver = $this->createMock(HandleResolver::class);
18 $cache = $this->createMock(CacheStore::class);
19
20 $didDocument = DidDocument::fromArray([
21 'id' => 'did:plc:abc123',
22 'service' => [
23 [
24 'type' => 'AtprotoPersonalDataServer',
25 'serviceEndpoint' => 'https://pds.example.com',
26 ],
27 ],
28 ]);
29
30 $didResolver->expects($this->once())
31 ->method('resolve')
32 ->with('did:plc:abc123')
33 ->willReturn($didDocument);
34
35 $cache->method('has')->willReturn(false);
36
37 // Expect multiple cache puts (DID document + PDS endpoint)
38 $cache->expects($this->exactly(2))
39 ->method('put')
40 ->willReturnCallback(function ($key, $value, $ttl) {
41 $this->assertContains($key, ['did:did:plc:abc123', 'pds:did:plc:abc123']);
42
43 return null;
44 });
45
46 $beacon = new Resolver($didResolver, $handleResolver, $cache);
47 $pds = $beacon->resolvePds('did:plc:abc123');
48
49 $this->assertSame('https://pds.example.com', $pds);
50 }
51
52 public function test_it_can_resolve_pds_from_handle(): void
53 {
54 $didResolver = $this->createMock(DidResolver::class);
55 $handleResolver = $this->createMock(HandleResolver::class);
56 $cache = $this->createMock(CacheStore::class);
57
58 $handleResolver->expects($this->once())
59 ->method('resolve')
60 ->with('user.bsky.social')
61 ->willReturn('did:plc:abc123');
62
63 $didDocument = DidDocument::fromArray([
64 'id' => 'did:plc:abc123',
65 'service' => [
66 [
67 'type' => 'AtprotoPersonalDataServer',
68 'serviceEndpoint' => 'https://pds.example.com',
69 ],
70 ],
71 ]);
72
73 $didResolver->expects($this->once())
74 ->method('resolve')
75 ->with('did:plc:abc123')
76 ->willReturn($didDocument);
77
78 $cache->method('has')->willReturn(false);
79
80 // Expect multiple cache puts (handle + DID document + PDS endpoint)
81 $cache->expects($this->exactly(3))
82 ->method('put')
83 ->willReturnCallback(function ($key, $value, $ttl) {
84 $this->assertContains($key, ['handle:user.bsky.social', 'did:did:plc:abc123', 'pds:user.bsky.social']);
85
86 return null;
87 });
88
89 $beacon = new Resolver($didResolver, $handleResolver, $cache);
90 $pds = $beacon->resolvePds('user.bsky.social');
91
92 $this->assertSame('https://pds.example.com', $pds);
93 }
94
95 public function test_it_returns_null_when_no_pds_endpoint(): void
96 {
97 $didResolver = $this->createMock(DidResolver::class);
98 $handleResolver = $this->createMock(HandleResolver::class);
99 $cache = $this->createMock(CacheStore::class);
100
101 $didDocument = DidDocument::fromArray([
102 'id' => 'did:plc:abc123',
103 'service' => [],
104 ]);
105
106 $didResolver->expects($this->once())
107 ->method('resolve')
108 ->with('did:plc:abc123')
109 ->willReturn($didDocument);
110
111 $cache->method('has')->willReturn(false);
112
113 // DID document is still cached, but PDS endpoint is not (since it's null)
114 $cache->expects($this->once())
115 ->method('put')
116 ->with('did:did:plc:abc123', $didDocument, $this->anything());
117
118 $beacon = new Resolver($didResolver, $handleResolver, $cache);
119 $pds = $beacon->resolvePds('did:plc:abc123');
120
121 $this->assertNull($pds);
122 }
123
124 public function test_it_uses_cached_pds_endpoint(): void
125 {
126 $didResolver = $this->createMock(DidResolver::class);
127 $handleResolver = $this->createMock(HandleResolver::class);
128 $cache = $this->createMock(CacheStore::class);
129
130 $cache->expects($this->once())
131 ->method('has')
132 ->with('pds:did:plc:abc123')
133 ->willReturn(true);
134
135 $cache->expects($this->once())
136 ->method('get')
137 ->with('pds:did:plc:abc123')
138 ->willReturn('https://cached-pds.example.com');
139
140 $didResolver->expects($this->never())->method('resolve');
141
142 $beacon = new Resolver($didResolver, $handleResolver, $cache);
143 $pds = $beacon->resolvePds('did:plc:abc123');
144
145 $this->assertSame('https://cached-pds.example.com', $pds);
146 }
147
148 public function test_it_can_clear_pds_cache(): void
149 {
150 $didResolver = $this->createMock(DidResolver::class);
151 $handleResolver = $this->createMock(HandleResolver::class);
152 $cache = $this->createMock(CacheStore::class);
153
154 $cache->expects($this->once())
155 ->method('forget')
156 ->with('pds:did:plc:abc123');
157
158 $beacon = new Resolver($didResolver, $handleResolver, $cache);
159 $beacon->clearPdsCache('did:plc:abc123');
160 }
161}