A social knowledge tool for researchers built on ATProto
at development 8.9 kB view raw
1import { GetNoteCardsForUrlUseCase } from '../../application/useCases/queries/GetNoteCardsForUrlUseCase'; 2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository'; 3import { InMemoryCardQueryRepository } from '../utils/InMemoryCardQueryRepository'; 4import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository'; 5import { CuratorId } from '../../domain/value-objects/CuratorId'; 6import { CardBuilder } from '../utils/builders/CardBuilder'; 7import { URL } from '../../domain/value-objects/URL'; 8import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 9 10describe('GetNoteCardsForUrlUseCase', () => { 11 let useCase: GetNoteCardsForUrlUseCase; 12 let cardRepository: InMemoryCardRepository; 13 let cardQueryRepository: InMemoryCardQueryRepository; 14 let collectionRepository: InMemoryCollectionRepository; 15 let curator1: CuratorId; 16 let curator2: CuratorId; 17 let curator3: CuratorId; 18 19 beforeEach(() => { 20 cardRepository = new InMemoryCardRepository(); 21 collectionRepository = new InMemoryCollectionRepository(); 22 cardQueryRepository = new InMemoryCardQueryRepository( 23 cardRepository, 24 collectionRepository, 25 ); 26 27 useCase = new GetNoteCardsForUrlUseCase(cardQueryRepository); 28 29 curator1 = CuratorId.create('did:plc:curator1').unwrap(); 30 curator2 = CuratorId.create('did:plc:curator2').unwrap(); 31 curator3 = CuratorId.create('did:plc:curator3').unwrap(); 32 }); 33 34 afterEach(() => { 35 cardRepository.clear(); 36 collectionRepository.clear(); 37 cardQueryRepository.clear(); 38 }); 39 40 describe('Multiple users with notes for same URL', () => { 41 it('should return all note cards for the specified URL', async () => { 42 const testUrl = 'https://example.com/shared-article'; 43 const url = URL.create(testUrl).unwrap(); 44 45 // Create note cards for different users with the same URL 46 const noteCard1 = new CardBuilder() 47 .withCuratorId(curator1.value) 48 .withNoteCard('First user note about the article') 49 .withUrl(url) 50 .buildOrThrow(); 51 52 const noteCard2 = new CardBuilder() 53 .withCuratorId(curator2.value) 54 .withNoteCard('Second user note about the article') 55 .withUrl(url) 56 .buildOrThrow(); 57 58 const noteCard3 = new CardBuilder() 59 .withCuratorId(curator3.value) 60 .withNoteCard('Third user note about the article') 61 .withUrl(url) 62 .buildOrThrow(); 63 64 // Save note cards 65 await cardRepository.save(noteCard1); 66 await cardRepository.save(noteCard2); 67 await cardRepository.save(noteCard3); 68 69 // Execute the use case 70 const query = { 71 url: testUrl, 72 }; 73 74 const result = await useCase.execute(query); 75 76 // Verify the result 77 expect(result.isOk()).toBe(true); 78 const response = result.unwrap(); 79 80 expect(response.notes).toHaveLength(3); 81 expect(response.pagination.totalCount).toBe(3); 82 83 // Check that all three users' notes are included 84 const authorIds = response.notes.map((note) => note.authorId); 85 expect(authorIds).toContain(curator1.value); 86 expect(authorIds).toContain(curator2.value); 87 expect(authorIds).toContain(curator3.value); 88 89 // Check note content 90 const noteTexts = response.notes.map((note) => note.note); 91 expect(noteTexts).toContain('First user note about the article'); 92 expect(noteTexts).toContain('Second user note about the article'); 93 expect(noteTexts).toContain('Third user note about the article'); 94 }); 95 96 it('should return empty result when no notes exist for the URL', async () => { 97 const testUrl = 'https://example.com/nonex istent-article'; 98 99 const query = { 100 url: testUrl, 101 }; 102 103 const result = await useCase.execute(query); 104 105 expect(result.isOk()).toBe(true); 106 const response = result.unwrap(); 107 108 expect(response.notes).toHaveLength(0); 109 expect(response.pagination.totalCount).toBe(0); 110 }); 111 112 it('should not return notes for different URLs', async () => { 113 const testUrl1 = 'https://example.com/article1'; 114 const testUrl2 = 'https://example.com/article2'; 115 const url1 = URL.create(testUrl1).unwrap(); 116 const url2 = URL.create(testUrl2).unwrap(); 117 118 // Create note cards with different URLs 119 const noteCard1 = new CardBuilder() 120 .withCuratorId(curator1.value) 121 .withNoteCard('Note for article 1') 122 .withUrl(url1) 123 .buildOrThrow(); 124 125 const noteCard2 = new CardBuilder() 126 .withCuratorId(curator2.value) 127 .withNoteCard('Note for article 2') 128 .withUrl(url2) 129 .buildOrThrow(); 130 131 await cardRepository.save(noteCard1); 132 await cardRepository.save(noteCard2); 133 134 // Query for testUrl1 135 const query = { 136 url: testUrl1, 137 }; 138 139 const result = await useCase.execute(query); 140 141 expect(result.isOk()).toBe(true); 142 const response = result.unwrap(); 143 144 expect(response.notes).toHaveLength(1); 145 expect(response.notes[0]!.note).toBe('Note for article 1'); 146 expect(response.notes[0]!.authorId).toBe(curator1.value); 147 }); 148 }); 149 150 describe('Pagination', () => { 151 it('should paginate results correctly', async () => { 152 const testUrl = 'https://example.com/popular-article'; 153 const url = URL.create(testUrl).unwrap(); 154 155 // Create 5 note cards with the same URL from different users 156 for (let i = 1; i <= 5; i++) { 157 const curator = CuratorId.create(`did:plc:curator${i}`).unwrap(); 158 159 const noteCard = new CardBuilder() 160 .withCuratorId(curator.value) 161 .withNoteCard(`Note ${i} about the article`) 162 .withUrl(url) 163 .buildOrThrow(); 164 165 await cardRepository.save(noteCard); 166 } 167 168 // Test first page with limit 2 169 const query1 = { 170 url: testUrl, 171 page: 1, 172 limit: 2, 173 }; 174 175 const result1 = await useCase.execute(query1); 176 expect(result1.isOk()).toBe(true); 177 const response1 = result1.unwrap(); 178 179 expect(response1.notes).toHaveLength(2); 180 expect(response1.pagination.currentPage).toBe(1); 181 expect(response1.pagination.totalCount).toBe(5); 182 expect(response1.pagination.totalPages).toBe(3); 183 expect(response1.pagination.hasMore).toBe(true); 184 185 // Test second page 186 const query2 = { 187 url: testUrl, 188 page: 2, 189 limit: 2, 190 }; 191 192 const result2 = await useCase.execute(query2); 193 expect(result2.isOk()).toBe(true); 194 const response2 = result2.unwrap(); 195 196 expect(response2.notes).toHaveLength(2); 197 expect(response2.pagination.currentPage).toBe(2); 198 expect(response2.pagination.hasMore).toBe(true); 199 200 // Test last page 201 const query3 = { 202 url: testUrl, 203 page: 3, 204 limit: 2, 205 }; 206 207 const result3 = await useCase.execute(query3); 208 expect(result3.isOk()).toBe(true); 209 const response3 = result3.unwrap(); 210 211 expect(response3.notes).toHaveLength(1); 212 expect(response3.pagination.currentPage).toBe(3); 213 expect(response3.pagination.hasMore).toBe(false); 214 }); 215 216 it('should respect limit cap of 100', async () => { 217 const query = { 218 url: 'https://example.com/test', 219 limit: 200, // Should be capped at 100 220 }; 221 222 const result = await useCase.execute(query); 223 expect(result.isOk()).toBe(true); 224 const response = result.unwrap(); 225 226 expect(response.pagination.limit).toBe(100); 227 }); 228 }); 229 230 describe('Sorting', () => { 231 it('should use default sorting parameters', async () => { 232 const testUrl = 'https://example.com/test-article'; 233 234 const query = { 235 url: testUrl, 236 }; 237 238 const result = await useCase.execute(query); 239 expect(result.isOk()).toBe(true); 240 const response = result.unwrap(); 241 242 expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT); 243 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 244 }); 245 246 it('should use provided sorting parameters', async () => { 247 const testUrl = 'https://example.com/test-article'; 248 249 const query = { 250 url: testUrl, 251 sortBy: CardSortField.CREATED_AT, 252 sortOrder: SortOrder.ASC, 253 }; 254 255 const result = await useCase.execute(query); 256 expect(result.isOk()).toBe(true); 257 const response = result.unwrap(); 258 259 expect(response.sorting.sortBy).toBe(CardSortField.CREATED_AT); 260 expect(response.sorting.sortOrder).toBe(SortOrder.ASC); 261 }); 262 }); 263 264 describe('Validation', () => { 265 it('should fail with invalid URL', async () => { 266 const query = { 267 url: 'not-a-valid-url', 268 }; 269 270 const result = await useCase.execute(query); 271 272 expect(result.isErr()).toBe(true); 273 if (result.isErr()) { 274 expect(result.error.message).toContain('Invalid URL'); 275 } 276 }); 277 }); 278});