A social knowledge tool for researchers built on ATProto
1import {
2 PostgreSqlContainer,
3 StartedPostgreSqlContainer,
4} from '@testcontainers/postgresql';
5import postgres from 'postgres';
6import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
7import { DrizzleCardQueryRepository } from '../../infrastructure/repositories/DrizzleCardQueryRepository';
8import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository';
9import { CuratorId } from '../../domain/value-objects/CuratorId';
10import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID';
11import { cards } from '../../infrastructure/repositories/schema/card.sql';
12import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql';
13import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql';
14import { CardBuilder } from '../utils/builders/CardBuilder';
15import { URL } from '../../domain/value-objects/URL';
16import { UrlMetadata } from '../../domain/value-objects/UrlMetadata';
17import { createTestSchema } from '../test-utils/createTestSchema';
18import { CardTypeEnum } from '../../domain/value-objects/CardType';
19
20describe('DrizzleCardQueryRepository - getUrlCardBasic', () => {
21 let container: StartedPostgreSqlContainer;
22 let db: PostgresJsDatabase;
23 let queryRepository: DrizzleCardQueryRepository;
24 let cardRepository: DrizzleCardRepository;
25
26 // Test data
27 let curatorId: CuratorId;
28 let otherCuratorId: CuratorId;
29
30 // Setup before all tests
31 beforeAll(async () => {
32 // Start PostgreSQL container
33 container = await new PostgreSqlContainer('postgres:14').start();
34
35 // Create database connection
36 const connectionString = container.getConnectionUri();
37 process.env.DATABASE_URL = connectionString;
38 const client = postgres(connectionString);
39 db = drizzle(client);
40
41 // Create repositories
42 queryRepository = new DrizzleCardQueryRepository(db);
43 cardRepository = new DrizzleCardRepository(db);
44
45 // Create schema using helper function
46 await createTestSchema(db);
47
48 // Create test data
49 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
50 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
51 }, 60000); // Increase timeout for container startup
52
53 // Cleanup after all tests
54 afterAll(async () => {
55 // Stop container
56 await container.stop();
57 });
58
59 // Clear data between tests
60 beforeEach(async () => {
61 await db.delete(libraryMemberships);
62 await db.delete(cards);
63 await db.delete(publishedRecords);
64 });
65
66 describe('getUrlCardBasic', () => {
67 it('should return null when card does not exist', async () => {
68 const nonExistentCardId = new UniqueEntityID().toString();
69
70 const result = await queryRepository.getUrlCardBasic(nonExistentCardId);
71
72 expect(result).toBeNull();
73 });
74
75 it('should return null when card exists but is not a URL card', async () => {
76 // Create a note card
77 const noteCard = new CardBuilder()
78 .withCuratorId(curatorId.value)
79 .withNoteCard('This is a note')
80 .buildOrThrow();
81
82 await cardRepository.save(noteCard);
83
84 const result = await queryRepository.getUrlCardBasic(
85 noteCard.cardId.getStringValue(),
86 );
87
88 expect(result).toBeNull();
89 });
90
91 it('should return URL card with basic metadata', async () => {
92 // Create URL card with metadata
93 const url = URL.create('https://example.com/article').unwrap();
94 const urlMetadata = UrlMetadata.create({
95 url: url.value,
96 title: 'Test Article',
97 description: 'A test article description',
98 author: 'John Doe',
99 imageUrl: 'https://example.com/image.jpg',
100 }).unwrap();
101
102 const urlCard = new CardBuilder()
103 .withCuratorId(curatorId.value)
104 .withUrlCard(url, urlMetadata)
105 .buildOrThrow();
106
107 await cardRepository.save(urlCard);
108
109 const result = await queryRepository.getUrlCardBasic(
110 urlCard.cardId.getStringValue(),
111 );
112
113 expect(result).toBeDefined();
114 expect(result?.id).toBe(urlCard.cardId.getStringValue());
115 expect(result?.type).toBe(CardTypeEnum.URL);
116 expect(result?.url).toBe(url.value);
117 expect(result?.cardContent.title).toBe('Test Article');
118 expect(result?.cardContent.description).toBe(
119 'A test article description',
120 );
121 expect(result?.cardContent.author).toBe('John Doe');
122 expect(result?.cardContent.thumbnailUrl).toBe(
123 'https://example.com/image.jpg',
124 );
125 expect(result?.note).toBeUndefined();
126 });
127
128 it('should include connected note card by the same author', async () => {
129 // Create URL card
130 const url = URL.create('https://example.com/article-with-note').unwrap();
131 const urlCard = new CardBuilder()
132 .withCuratorId(curatorId.value)
133 .withUrlCard(url)
134 .buildOrThrow();
135
136 await cardRepository.save(urlCard);
137
138 // Create connected note card by the same author
139 const noteCard = new CardBuilder()
140 .withCuratorId(curatorId.value)
141 .withNoteCard('This is my note about the article')
142 .withParentCard(urlCard.cardId)
143 .buildOrThrow();
144
145 await cardRepository.save(noteCard);
146
147 const result = await queryRepository.getUrlCardBasic(
148 urlCard.cardId.getStringValue(),
149 );
150
151 expect(result).toBeDefined();
152 expect(result?.note).toBeDefined();
153 expect(result?.note?.id).toBe(noteCard.cardId.getStringValue());
154 expect(result?.note?.text).toBe('This is my note about the article');
155 });
156
157 it('should NOT include note card by a different author', async () => {
158 // Create URL card by first author
159 const url = URL.create(
160 'https://example.com/article-different-author',
161 ).unwrap();
162 const urlCard = new CardBuilder()
163 .withCuratorId(curatorId.value)
164 .withUrlCard(url)
165 .buildOrThrow();
166
167 await cardRepository.save(urlCard);
168
169 // Create connected note card by a DIFFERENT author
170 const noteCard = new CardBuilder()
171 .withCuratorId(otherCuratorId.value) // Different author
172 .withNoteCard('This is someone elses note')
173 .withParentCard(urlCard.cardId)
174 .buildOrThrow();
175
176 await cardRepository.save(noteCard);
177
178 const result = await queryRepository.getUrlCardBasic(
179 urlCard.cardId.getStringValue(),
180 );
181
182 expect(result).toBeDefined();
183 expect(result?.note).toBeUndefined(); // Should not include note from different author
184 });
185
186 it('should handle URL card without metadata', async () => {
187 // Create URL card without metadata
188 const url = URL.create('https://example.com/minimal').unwrap();
189 const urlCard = new CardBuilder()
190 .withCuratorId(curatorId.value)
191 .withUrlCard(url)
192 .buildOrThrow();
193
194 await cardRepository.save(urlCard);
195
196 const result = await queryRepository.getUrlCardBasic(
197 urlCard.cardId.getStringValue(),
198 );
199
200 expect(result).toBeDefined();
201 expect(result?.id).toBe(urlCard.cardId.getStringValue());
202 expect(result?.type).toBe(CardTypeEnum.URL);
203 expect(result?.url).toBe(url.value);
204 expect(result?.cardContent.title).toBeUndefined();
205 expect(result?.cardContent.description).toBeUndefined();
206 expect(result?.cardContent.author).toBeUndefined();
207 expect(result?.cardContent.thumbnailUrl).toBeUndefined();
208 expect(result?.note).toBeUndefined();
209 });
210 });
211});