@ucptools/validator 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/.claude/settings.local.json +60 -0
  2. package/.vercel/README.txt +11 -0
  3. package/.vercel/project.json +1 -0
  4. package/dist/cli/index.d.ts +6 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +279 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/compliance/compliance-generator.d.ts +34 -0
  9. package/dist/compliance/compliance-generator.d.ts.map +1 -0
  10. package/dist/compliance/compliance-generator.js +320 -0
  11. package/dist/compliance/compliance-generator.js.map +1 -0
  12. package/dist/compliance/index.d.ts +8 -0
  13. package/dist/compliance/index.d.ts.map +1 -0
  14. package/dist/compliance/index.js +17 -0
  15. package/dist/compliance/index.js.map +1 -0
  16. package/dist/compliance/templates.d.ts +34 -0
  17. package/dist/compliance/templates.d.ts.map +1 -0
  18. package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
  19. package/dist/compliance/templates.js.map +1 -0
  20. package/dist/compliance/types.d.ts +64 -0
  21. package/dist/compliance/types.d.ts.map +1 -0
  22. package/dist/compliance/types.js +64 -0
  23. package/dist/compliance/types.js.map +1 -0
  24. package/dist/db/index.d.ts +11 -0
  25. package/dist/db/index.d.ts.map +1 -0
  26. package/dist/db/index.js +63 -0
  27. package/dist/db/index.js.map +1 -0
  28. package/dist/db/schema.d.ts +444 -0
  29. package/dist/db/schema.d.ts.map +1 -0
  30. package/dist/db/schema.js +65 -0
  31. package/dist/db/schema.js.map +1 -0
  32. package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
  33. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
  34. package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +642 -726
  35. package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
  36. package/dist/feed-analyzer/index.d.ts +8 -0
  37. package/dist/feed-analyzer/index.d.ts.map +1 -0
  38. package/dist/feed-analyzer/index.js +19 -0
  39. package/dist/feed-analyzer/index.js.map +1 -0
  40. package/dist/feed-analyzer/types.d.ts +204 -0
  41. package/dist/feed-analyzer/types.d.ts.map +1 -0
  42. package/dist/feed-analyzer/types.js +162 -0
  43. package/dist/feed-analyzer/types.js.map +1 -0
  44. package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
  45. package/dist/generator/index.d.ts.map +1 -0
  46. package/dist/generator/index.js +13 -0
  47. package/dist/generator/index.js.map +1 -0
  48. package/dist/generator/key-generator.d.ts +24 -0
  49. package/dist/generator/key-generator.d.ts.map +1 -0
  50. package/dist/generator/key-generator.js +144 -0
  51. package/dist/generator/key-generator.js.map +1 -0
  52. package/dist/generator/profile-builder.d.ts +15 -0
  53. package/dist/generator/profile-builder.d.ts.map +1 -0
  54. package/dist/generator/profile-builder.js +338 -0
  55. package/dist/generator/profile-builder.js.map +1 -0
  56. package/dist/hosting/artifacts-generator.d.ts +10 -0
  57. package/dist/hosting/artifacts-generator.d.ts.map +1 -0
  58. package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
  59. package/dist/hosting/artifacts-generator.js.map +1 -0
  60. package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
  61. package/dist/hosting/index.d.ts.map +1 -0
  62. package/dist/hosting/index.js +10 -0
  63. package/dist/hosting/index.js.map +1 -0
  64. package/dist/index.d.ts +18 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +78 -0
  67. package/dist/index.js.map +1 -0
  68. package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
  69. package/dist/security/index.d.ts.map +1 -0
  70. package/dist/security/index.js +12 -0
  71. package/dist/security/index.js.map +1 -0
  72. package/dist/security/security-scanner.d.ts +10 -0
  73. package/dist/security/security-scanner.d.ts.map +1 -0
  74. package/dist/security/security-scanner.js +541 -0
  75. package/dist/security/security-scanner.js.map +1 -0
  76. package/dist/security/types.d.ts +48 -0
  77. package/dist/security/types.d.ts.map +1 -0
  78. package/dist/security/types.js +21 -0
  79. package/dist/security/types.js.map +1 -0
  80. package/dist/services/directory.d.ts +104 -0
  81. package/dist/services/directory.d.ts.map +1 -0
  82. package/dist/services/directory.js +333 -0
  83. package/dist/services/directory.js.map +1 -0
  84. package/dist/simulator/agent-simulator.d.ts +69 -0
  85. package/dist/simulator/agent-simulator.d.ts.map +1 -0
  86. package/{src/simulator/agent-simulator.ts → dist/simulator/agent-simulator.js} +650 -941
  87. package/dist/simulator/agent-simulator.js.map +1 -0
  88. package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
  89. package/dist/simulator/index.d.ts.map +1 -0
  90. package/dist/simulator/index.js +23 -0
  91. package/dist/simulator/index.js.map +1 -0
  92. package/{src/simulator/types.ts → dist/simulator/types.d.ts} +145 -170
  93. package/dist/simulator/types.d.ts.map +1 -0
  94. package/dist/simulator/types.js +18 -0
  95. package/dist/simulator/types.js.map +1 -0
  96. package/dist/types/generator.d.ts +106 -0
  97. package/dist/types/generator.d.ts.map +1 -0
  98. package/dist/types/generator.js +6 -0
  99. package/dist/types/generator.js.map +1 -0
  100. package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
  101. package/dist/types/index.d.ts.map +1 -0
  102. package/dist/types/index.js +23 -0
  103. package/dist/types/index.js.map +1 -0
  104. package/dist/types/ucp-profile.d.ts +103 -0
  105. package/dist/types/ucp-profile.d.ts.map +1 -0
  106. package/dist/types/ucp-profile.js +45 -0
  107. package/dist/types/ucp-profile.js.map +1 -0
  108. package/dist/types/validation.d.ts +68 -0
  109. package/dist/types/validation.d.ts.map +1 -0
  110. package/dist/types/validation.js +32 -0
  111. package/dist/types/validation.js.map +1 -0
  112. package/dist/validator/index.d.ts +26 -0
  113. package/dist/validator/index.d.ts.map +1 -0
  114. package/dist/validator/index.js +161 -0
  115. package/dist/validator/index.js.map +1 -0
  116. package/dist/validator/network-validator.d.ts +28 -0
  117. package/dist/validator/network-validator.d.ts.map +1 -0
  118. package/dist/validator/network-validator.js +319 -0
  119. package/dist/validator/network-validator.js.map +1 -0
  120. package/dist/validator/rules-validator.d.ts +11 -0
  121. package/dist/validator/rules-validator.d.ts.map +1 -0
  122. package/dist/validator/rules-validator.js +257 -0
  123. package/dist/validator/rules-validator.js.map +1 -0
  124. package/dist/validator/sdk-validator.d.ts +58 -0
  125. package/dist/validator/sdk-validator.d.ts.map +1 -0
  126. package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
  127. package/dist/validator/sdk-validator.js.map +1 -0
  128. package/dist/validator/structural-validator.d.ts +11 -0
  129. package/dist/validator/structural-validator.d.ts.map +1 -0
  130. package/dist/validator/structural-validator.js +415 -0
  131. package/dist/validator/structural-validator.js.map +1 -0
  132. package/package.json +1 -1
  133. package/publish-output.txt +0 -0
  134. package/CLAUDE.md +0 -109
  135. package/api/analyze-feed.js +0 -140
  136. package/api/badge.js +0 -185
  137. package/api/benchmark.js +0 -177
  138. package/api/directory-stats.ts +0 -29
  139. package/api/directory.ts +0 -73
  140. package/api/generate-compliance.js +0 -143
  141. package/api/generate-schema.js +0 -457
  142. package/api/generate.js +0 -132
  143. package/api/security-scan.js +0 -133
  144. package/api/simulate.js +0 -187
  145. package/api/tsconfig.json +0 -10
  146. package/api/validate.js +0 -1351
  147. package/apify-actor/.actor/actor.json +0 -68
  148. package/apify-actor/.actor/input_schema.json +0 -32
  149. package/apify-actor/APIFY-STORE-LISTING.md +0 -412
  150. package/apify-actor/Dockerfile +0 -8
  151. package/apify-actor/README.md +0 -166
  152. package/apify-actor/main.ts +0 -111
  153. package/apify-actor/package.json +0 -17
  154. package/apify-actor/src/main.js +0 -199
  155. package/docs/BRAND-IDENTITY.md +0 -238
  156. package/docs/BRAND-STYLE-GUIDE.md +0 -356
  157. package/drizzle/0000_black_king_cobra.sql +0 -39
  158. package/drizzle/meta/0000_snapshot.json +0 -309
  159. package/drizzle/meta/_journal.json +0 -13
  160. package/drizzle.config.ts +0 -10
  161. package/public/.well-known/ucp +0 -25
  162. package/public/android-chrome-192x192.png +0 -0
  163. package/public/android-chrome-512x512.png +0 -0
  164. package/public/apple-touch-icon.png +0 -0
  165. package/public/brand.css +0 -321
  166. package/public/directory.html +0 -701
  167. package/public/favicon-16x16.png +0 -0
  168. package/public/favicon-32x32.png +0 -0
  169. package/public/favicon.ico +0 -0
  170. package/public/guides/bigcommerce.html +0 -743
  171. package/public/guides/fastucp.html +0 -838
  172. package/public/guides/magento.html +0 -779
  173. package/public/guides/shopify.html +0 -726
  174. package/public/guides/squarespace.html +0 -749
  175. package/public/guides/wix.html +0 -747
  176. package/public/guides/woocommerce.html +0 -733
  177. package/public/index.html +0 -3835
  178. package/public/learn.html +0 -396
  179. package/public/logo.jpeg +0 -0
  180. package/public/og-image-icon.png +0 -0
  181. package/public/og-image.png +0 -0
  182. package/public/robots.txt +0 -6
  183. package/public/site.webmanifest +0 -31
  184. package/public/sitemap.xml +0 -69
  185. package/public/social/linkedin-banner-1128x191.png +0 -0
  186. package/public/social/temp.PNG +0 -0
  187. package/public/social/x-header-1500x500.png +0 -0
  188. package/public/verify.html +0 -410
  189. package/scripts/generate-favicons.js +0 -44
  190. package/scripts/generate-ico.js +0 -23
  191. package/scripts/generate-og-image.js +0 -45
  192. package/scripts/reset-db.ts +0 -77
  193. package/scripts/seed-db.ts +0 -71
  194. package/scripts/setup-benchmark-db.js +0 -70
  195. package/src/api/server.ts +0 -266
  196. package/src/cli/index.ts +0 -302
  197. package/src/compliance/compliance-generator.ts +0 -452
  198. package/src/compliance/index.ts +0 -28
  199. package/src/compliance/types.ts +0 -170
  200. package/src/db/index.ts +0 -28
  201. package/src/db/schema.ts +0 -84
  202. package/src/feed-analyzer/index.ts +0 -34
  203. package/src/feed-analyzer/types.ts +0 -354
  204. package/src/generator/key-generator.ts +0 -124
  205. package/src/generator/profile-builder.ts +0 -402
  206. package/src/index.ts +0 -105
  207. package/src/security/security-scanner.ts +0 -604
  208. package/src/security/types.ts +0 -55
  209. package/src/services/directory.ts +0 -434
  210. package/src/types/generator.ts +0 -140
  211. package/src/types/ucp-profile.ts +0 -140
  212. package/src/types/validation.ts +0 -89
  213. package/src/validator/index.ts +0 -194
  214. package/src/validator/network-validator.ts +0 -417
  215. package/src/validator/rules-validator.ts +0 -297
  216. package/src/validator/structural-validator.ts +0 -476
  217. package/tests/fixtures/non-compliant-profile.json +0 -25
  218. package/tests/fixtures/official-sample-profile.json +0 -75
  219. package/tests/integration/benchmark.test.ts +0 -207
  220. package/tests/integration/database.test.ts +0 -163
  221. package/tests/integration/directory-api.test.ts +0 -268
  222. package/tests/integration/simulate-api.test.ts +0 -230
  223. package/tests/integration/validate-api.test.ts +0 -269
  224. package/tests/setup.ts +0 -15
  225. package/tests/unit/agent-simulator.test.ts +0 -575
  226. package/tests/unit/compliance-generator.test.ts +0 -374
  227. package/tests/unit/directory-service.test.ts +0 -272
  228. package/tests/unit/feed-analyzer.test.ts +0 -517
  229. package/tests/unit/lint-suggestions.test.ts +0 -423
  230. package/tests/unit/official-samples.test.ts +0 -211
  231. package/tests/unit/pdf-report.test.ts +0 -390
  232. package/tests/unit/sdk-validator.test.ts +0 -531
  233. package/tests/unit/security-scanner.test.ts +0 -410
  234. package/tests/unit/validation.test.ts +0 -390
  235. package/vercel.json +0 -34
  236. package/vitest.config.ts +0 -22
@@ -1,207 +0,0 @@
1
- /**
2
- * Integration Tests for Benchmark Functionality
3
- * Tests Issue #2: Industry Benchmark Comparison
4
- */
5
-
6
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
7
- import { getDb, benchmarkStats, benchmarkSummary } from '../../src/db/index.js';
8
- import { eq, sql } from 'drizzle-orm';
9
-
10
- // Skip tests if DATABASE_URL is not set
11
- const skipIfNoDb = !process.env.DATABASE_URL;
12
-
13
- describe.skipIf(skipIfNoDb)('Benchmark Integration (Issue #2)', () => {
14
- describe('Benchmark Stats Table Structure', () => {
15
- it('should have all required score buckets (0-100 in increments of 10)', async () => {
16
- const db = getDb();
17
- const stats = await db.select().from(benchmarkStats);
18
-
19
- const buckets = stats.map((s) => s.scoreBucket).sort((a, b) => a - b);
20
- const expectedBuckets = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
21
-
22
- expect(buckets).toEqual(expectedBuckets);
23
- });
24
-
25
- it('should have non-negative counts for all buckets', async () => {
26
- const db = getDb();
27
- const stats = await db.select().from(benchmarkStats);
28
-
29
- for (const stat of stats) {
30
- expect(stat.count).toBeGreaterThanOrEqual(0);
31
- }
32
- });
33
- });
34
-
35
- describe('Benchmark Summary Table', () => {
36
- it('should have exactly one summary row', async () => {
37
- const db = getDb();
38
- const summary = await db.select().from(benchmarkSummary);
39
-
40
- expect(summary).toHaveLength(1);
41
- expect(summary[0].id).toBe(1);
42
- });
43
-
44
- it('should have valid totalValidations count', async () => {
45
- const db = getDb();
46
- const summary = await db.select().from(benchmarkSummary);
47
-
48
- expect(summary[0].totalValidations).toBeGreaterThanOrEqual(0);
49
- });
50
-
51
- it('should have valid avgScore', async () => {
52
- const db = getDb();
53
- const summary = await db.select().from(benchmarkSummary);
54
-
55
- if (summary[0].avgScore !== null) {
56
- const avgScore = parseFloat(summary[0].avgScore);
57
- expect(avgScore).toBeGreaterThanOrEqual(0);
58
- expect(avgScore).toBeLessThanOrEqual(100);
59
- }
60
- });
61
-
62
- it('should have updatedAt timestamp', async () => {
63
- const db = getDb();
64
- const summary = await db.select().from(benchmarkSummary);
65
-
66
- expect(summary[0].updatedAt).toBeInstanceOf(Date);
67
- });
68
- });
69
-
70
- describe('Benchmark Recording Logic', () => {
71
- // Save original values to restore after tests
72
- let originalBucketCount: number;
73
- let originalTotalValidations: number;
74
- const testBucket = 70;
75
-
76
- beforeAll(async () => {
77
- const db = getDb();
78
-
79
- // Get original values
80
- const bucketResult = await db
81
- .select()
82
- .from(benchmarkStats)
83
- .where(eq(benchmarkStats.scoreBucket, testBucket));
84
- originalBucketCount = bucketResult[0]?.count || 0;
85
-
86
- const summaryResult = await db.select().from(benchmarkSummary);
87
- originalTotalValidations = summaryResult[0]?.totalValidations || 0;
88
- });
89
-
90
- afterAll(async () => {
91
- // Restore original values
92
- const db = getDb();
93
-
94
- await db
95
- .update(benchmarkStats)
96
- .set({ count: originalBucketCount })
97
- .where(eq(benchmarkStats.scoreBucket, testBucket));
98
-
99
- await db
100
- .update(benchmarkSummary)
101
- .set({ totalValidations: originalTotalValidations })
102
- .where(eq(benchmarkSummary.id, 1));
103
- });
104
-
105
- it('should increment bucket count when score is recorded', async () => {
106
- const db = getDb();
107
-
108
- // Get current count
109
- const beforeResult = await db
110
- .select()
111
- .from(benchmarkStats)
112
- .where(eq(benchmarkStats.scoreBucket, testBucket));
113
- const beforeCount = beforeResult[0]?.count || 0;
114
-
115
- // Increment (simulating what validate.js does)
116
- await db
117
- .update(benchmarkStats)
118
- .set({ count: sql`${benchmarkStats.count} + 1` })
119
- .where(eq(benchmarkStats.scoreBucket, testBucket));
120
-
121
- // Verify increment
122
- const afterResult = await db
123
- .select()
124
- .from(benchmarkStats)
125
- .where(eq(benchmarkStats.scoreBucket, testBucket));
126
-
127
- expect(afterResult[0].count).toBe(beforeCount + 1);
128
- });
129
-
130
- it('should increment total validations in summary', async () => {
131
- const db = getDb();
132
-
133
- // Get current total
134
- const beforeResult = await db.select().from(benchmarkSummary);
135
- const beforeTotal = beforeResult[0]?.totalValidations || 0;
136
-
137
- // Increment
138
- await db
139
- .update(benchmarkSummary)
140
- .set({ totalValidations: sql`${benchmarkSummary.totalValidations} + 1` })
141
- .where(eq(benchmarkSummary.id, 1));
142
-
143
- // Verify
144
- const afterResult = await db.select().from(benchmarkSummary);
145
- expect(afterResult[0].totalValidations).toBe(beforeTotal + 1);
146
- });
147
- });
148
-
149
- describe('Percentile Calculation', () => {
150
- it('should be calculable from bucket data', async () => {
151
- const db = getDb();
152
-
153
- // Get all buckets with cumulative counts
154
- const stats = await db
155
- .select()
156
- .from(benchmarkStats)
157
- .orderBy(benchmarkStats.scoreBucket);
158
-
159
- // Calculate cumulative
160
- let cumulative = 0;
161
- const withCumulative = stats.map((s) => {
162
- cumulative += s.count;
163
- return { ...s, cumulative };
164
- });
165
-
166
- // Total is the last cumulative
167
- const total = cumulative;
168
-
169
- if (total > 0) {
170
- // For a score of 70, calculate percentile
171
- const bucket70 = withCumulative.find((s) => s.scoreBucket === 70);
172
-
173
- if (bucket70) {
174
- const belowCount = withCumulative
175
- .filter((s) => s.scoreBucket < 70)
176
- .reduce((sum, s) => sum + s.count, 0);
177
-
178
- const percentile = Math.round((belowCount / total) * 100);
179
-
180
- expect(percentile).toBeGreaterThanOrEqual(0);
181
- expect(percentile).toBeLessThanOrEqual(100);
182
- }
183
- }
184
- });
185
- });
186
- });
187
-
188
- describe.skipIf(skipIfNoDb)('Validate API Benchmark Response', () => {
189
- it('should include benchmark data in validation response format', () => {
190
- // This tests the expected response structure from api/validate.js
191
- const mockBenchmarkResponse = {
192
- percentile: 75,
193
- comparison: 'Your site scores better than 75% of sites analyzed',
194
- total_sites_analyzed: 100,
195
- average_score: 65,
196
- };
197
-
198
- expect(mockBenchmarkResponse).toHaveProperty('percentile');
199
- expect(mockBenchmarkResponse).toHaveProperty('comparison');
200
- expect(mockBenchmarkResponse).toHaveProperty('total_sites_analyzed');
201
- expect(mockBenchmarkResponse).toHaveProperty('average_score');
202
-
203
- expect(typeof mockBenchmarkResponse.percentile).toBe('number');
204
- expect(mockBenchmarkResponse.percentile).toBeGreaterThanOrEqual(0);
205
- expect(mockBenchmarkResponse.percentile).toBeLessThanOrEqual(100);
206
- });
207
- });
@@ -1,163 +0,0 @@
1
- /**
2
- * Integration Tests for Database
3
- * Tests database connectivity and schema
4
- */
5
-
6
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
7
- import { getDb, merchants, benchmarkStats, benchmarkSummary } from '../../src/db/index.js';
8
- import { eq, sql } from 'drizzle-orm';
9
-
10
- // Skip tests if DATABASE_URL is not set
11
- const skipIfNoDb = !process.env.DATABASE_URL;
12
-
13
- describe.skipIf(skipIfNoDb)('Database Integration', () => {
14
- describe('Connection', () => {
15
- it('should connect to the database successfully', async () => {
16
- const db = getDb();
17
- expect(db).toBeDefined();
18
-
19
- // Simple query to verify connection
20
- const result = await db.execute(sql`SELECT 1 as test`);
21
- expect(result).toBeDefined();
22
- });
23
-
24
- it('should use lazy initialization', () => {
25
- // getDb should return the same instance
26
- const db1 = getDb();
27
- const db2 = getDb();
28
- expect(db1).toBe(db2);
29
- });
30
- });
31
-
32
- describe('Merchants Table', () => {
33
- const testDomain = `test-${Date.now()}.example.com`;
34
-
35
- afterAll(async () => {
36
- // Clean up test data
37
- const db = getDb();
38
- await db.delete(merchants).where(eq(merchants.domain, testDomain));
39
- });
40
-
41
- it('should insert a merchant', async () => {
42
- const db = getDb();
43
-
44
- const result = await db
45
- .insert(merchants)
46
- .values({
47
- domain: testDomain,
48
- displayName: 'Test Merchant',
49
- description: 'A test merchant for integration testing',
50
- category: 'test',
51
- countryCode: 'US',
52
- ucpScore: 75,
53
- ucpGrade: 'C',
54
- transports: 'REST',
55
- isPublic: true,
56
- isVerified: false,
57
- })
58
- .returning();
59
-
60
- expect(result).toHaveLength(1);
61
- expect(result[0].domain).toBe(testDomain);
62
- expect(result[0].id).toBeDefined();
63
- });
64
-
65
- it('should query merchants', async () => {
66
- const db = getDb();
67
-
68
- const result = await db
69
- .select()
70
- .from(merchants)
71
- .where(eq(merchants.domain, testDomain));
72
-
73
- expect(result).toHaveLength(1);
74
- expect(result[0].displayName).toBe('Test Merchant');
75
- expect(result[0].ucpScore).toBe(75);
76
- });
77
-
78
- it('should update a merchant', async () => {
79
- const db = getDb();
80
-
81
- await db
82
- .update(merchants)
83
- .set({ ucpScore: 85, ucpGrade: 'B' })
84
- .where(eq(merchants.domain, testDomain));
85
-
86
- const result = await db
87
- .select()
88
- .from(merchants)
89
- .where(eq(merchants.domain, testDomain));
90
-
91
- expect(result[0].ucpScore).toBe(85);
92
- expect(result[0].ucpGrade).toBe('B');
93
- });
94
-
95
- it('should enforce unique domain constraint', async () => {
96
- const db = getDb();
97
-
98
- await expect(
99
- db.insert(merchants).values({
100
- domain: testDomain,
101
- displayName: 'Duplicate Merchant',
102
- })
103
- ).rejects.toThrow();
104
- });
105
- });
106
-
107
- describe('Benchmark Stats Table (Issue #2)', () => {
108
- it('should have score buckets from 0 to 100', async () => {
109
- const db = getDb();
110
-
111
- const result = await db.select().from(benchmarkStats);
112
-
113
- expect(result.length).toBe(11); // 0, 10, 20, ..., 100
114
-
115
- const buckets = result.map((r) => r.scoreBucket).sort((a, b) => a - b);
116
- expect(buckets).toEqual([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]);
117
- });
118
-
119
- it('should have count field for each bucket', async () => {
120
- const db = getDb();
121
-
122
- const result = await db.select().from(benchmarkStats);
123
-
124
- for (const row of result) {
125
- expect(typeof row.count).toBe('number');
126
- expect(row.count).toBeGreaterThanOrEqual(0);
127
- }
128
- });
129
- });
130
-
131
- describe('Benchmark Summary Table (Issue #2)', () => {
132
- it('should have a summary row', async () => {
133
- const db = getDb();
134
-
135
- const result = await db.select().from(benchmarkSummary);
136
-
137
- expect(result.length).toBe(1);
138
- expect(result[0].id).toBe(1);
139
- });
140
-
141
- it('should track total validations', async () => {
142
- const db = getDb();
143
-
144
- const result = await db.select().from(benchmarkSummary);
145
-
146
- expect(typeof result[0].totalValidations).toBe('number');
147
- expect(result[0].totalValidations).toBeGreaterThanOrEqual(0);
148
- });
149
-
150
- it('should track average score', async () => {
151
- const db = getDb();
152
-
153
- const result = await db.select().from(benchmarkSummary);
154
-
155
- // avgScore can be null or a string (decimal type)
156
- if (result[0].avgScore !== null) {
157
- const avgScore = parseFloat(result[0].avgScore);
158
- expect(avgScore).toBeGreaterThanOrEqual(0);
159
- expect(avgScore).toBeLessThanOrEqual(100);
160
- }
161
- });
162
- });
163
- });
@@ -1,268 +0,0 @@
1
- /**
2
- * Integration Tests for Directory API Endpoints
3
- * Tests Issue #1: UCP Merchant Directory
4
- */
5
-
6
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
7
- import { getDb, merchants } from '../../src/db/index.js';
8
- import { eq } from 'drizzle-orm';
9
- import {
10
- listMerchants,
11
- submitMerchant,
12
- getMerchantByDomain,
13
- getDirectoryStats,
14
- revalidateMerchant,
15
- } from '../../src/services/directory.js';
16
-
17
- // Skip tests if DATABASE_URL is not set
18
- const skipIfNoDb = !process.env.DATABASE_URL;
19
-
20
- describe.skipIf(skipIfNoDb)('Directory API Integration', () => {
21
- const testDomains: string[] = [];
22
-
23
- // Helper to create a unique test domain
24
- const createTestDomain = () => {
25
- const domain = `api-test-${Date.now()}-${Math.random().toString(36).slice(2)}.example.com`;
26
- testDomains.push(domain);
27
- return domain;
28
- };
29
-
30
- afterAll(async () => {
31
- // Clean up all test data
32
- const db = getDb();
33
- for (const domain of testDomains) {
34
- await db.delete(merchants).where(eq(merchants.domain, domain));
35
- }
36
- });
37
-
38
- describe('listMerchants', () => {
39
- it('should return empty list when no merchants exist', async () => {
40
- const result = await listMerchants({ search: 'nonexistent-unique-query-xyz' });
41
-
42
- expect(result.merchants).toHaveLength(0);
43
- expect(result.pagination.total).toBe(0);
44
- });
45
-
46
- it('should return correct pagination structure', async () => {
47
- const result = await listMerchants({ page: 1, limit: 10 });
48
-
49
- expect(result.pagination).toHaveProperty('page');
50
- expect(result.pagination).toHaveProperty('limit');
51
- expect(result.pagination).toHaveProperty('total');
52
- expect(result.pagination).toHaveProperty('totalPages');
53
- });
54
-
55
- it('should return filter options', async () => {
56
- const result = await listMerchants({});
57
-
58
- expect(result.filters).toHaveProperty('categories');
59
- expect(result.filters).toHaveProperty('countries');
60
- expect(Array.isArray(result.filters.categories)).toBe(true);
61
- expect(Array.isArray(result.filters.countries)).toBe(true);
62
- });
63
-
64
- it('should filter by category', async () => {
65
- const db = getDb();
66
- const domain1 = createTestDomain();
67
- const domain2 = createTestDomain();
68
-
69
- // Create test merchants with different categories
70
- await db.insert(merchants).values([
71
- { domain: domain1, displayName: 'Fashion Store', category: 'fashion', isPublic: true },
72
- { domain: domain2, displayName: 'Tech Store', category: 'electronics', isPublic: true },
73
- ]);
74
-
75
- const fashionResult = await listMerchants({ category: 'fashion' });
76
- const electronicsResult = await listMerchants({ category: 'electronics' });
77
-
78
- const fashionDomains = fashionResult.merchants.map((m) => m.domain);
79
- const electronicsDomains = electronicsResult.merchants.map((m) => m.domain);
80
-
81
- expect(fashionDomains).toContain(domain1);
82
- expect(fashionDomains).not.toContain(domain2);
83
- expect(electronicsDomains).toContain(domain2);
84
- expect(electronicsDomains).not.toContain(domain1);
85
- });
86
-
87
- it('should search by domain and display name', async () => {
88
- const db = getDb();
89
- const domain = createTestDomain();
90
-
91
- await db.insert(merchants).values({
92
- domain,
93
- displayName: 'Unique Searchable Store XYZ123',
94
- isPublic: true,
95
- });
96
-
97
- // Search by display name
98
- const nameResult = await listMerchants({ search: 'XYZ123' });
99
- expect(nameResult.merchants.some((m) => m.domain === domain)).toBe(true);
100
-
101
- // Search by domain
102
- const domainResult = await listMerchants({ search: domain.split('.')[0] });
103
- expect(domainResult.merchants.some((m) => m.domain === domain)).toBe(true);
104
- });
105
-
106
- it('should only return public merchants', async () => {
107
- const db = getDb();
108
- const publicDomain = createTestDomain();
109
- const privateDomain = createTestDomain();
110
-
111
- await db.insert(merchants).values([
112
- { domain: publicDomain, displayName: 'Public Store', isPublic: true },
113
- { domain: privateDomain, displayName: 'Private Store', isPublic: false },
114
- ]);
115
-
116
- const result = await listMerchants({});
117
- const domains = result.merchants.map((m) => m.domain);
118
-
119
- expect(domains).toContain(publicDomain);
120
- expect(domains).not.toContain(privateDomain);
121
- });
122
-
123
- it('should sort by score descending by default', async () => {
124
- const db = getDb();
125
- const domain1 = createTestDomain();
126
- const domain2 = createTestDomain();
127
- const domain3 = createTestDomain();
128
-
129
- await db.insert(merchants).values([
130
- { domain: domain1, displayName: 'Low Score', ucpScore: 30, isPublic: true },
131
- { domain: domain2, displayName: 'High Score', ucpScore: 90, isPublic: true },
132
- { domain: domain3, displayName: 'Mid Score', ucpScore: 60, isPublic: true },
133
- ]);
134
-
135
- const result = await listMerchants({});
136
-
137
- // Find positions of our test merchants
138
- const positions = {
139
- high: result.merchants.findIndex((m) => m.domain === domain2),
140
- mid: result.merchants.findIndex((m) => m.domain === domain3),
141
- low: result.merchants.findIndex((m) => m.domain === domain1),
142
- };
143
-
144
- // High score should come before mid, mid before low
145
- if (positions.high !== -1 && positions.mid !== -1) {
146
- expect(positions.high).toBeLessThan(positions.mid);
147
- }
148
- if (positions.mid !== -1 && positions.low !== -1) {
149
- expect(positions.mid).toBeLessThan(positions.low);
150
- }
151
- });
152
- });
153
-
154
- describe('getMerchantByDomain', () => {
155
- it('should return null for non-existent domain', async () => {
156
- const result = await getMerchantByDomain('nonexistent-domain-xyz.com');
157
- expect(result).toBeNull();
158
- });
159
-
160
- it('should find merchant by domain', async () => {
161
- const db = getDb();
162
- const domain = createTestDomain();
163
-
164
- await db.insert(merchants).values({
165
- domain,
166
- displayName: 'Test Store',
167
- ucpScore: 75,
168
- });
169
-
170
- const result = await getMerchantByDomain(domain);
171
-
172
- expect(result).not.toBeNull();
173
- expect(result?.domain).toBe(domain);
174
- expect(result?.displayName).toBe('Test Store');
175
- });
176
-
177
- it('should be case-insensitive', async () => {
178
- const db = getDb();
179
- const domain = createTestDomain();
180
-
181
- await db.insert(merchants).values({
182
- domain: domain.toLowerCase(),
183
- displayName: 'Case Test Store',
184
- });
185
-
186
- const result = await getMerchantByDomain(domain.toUpperCase());
187
-
188
- expect(result).not.toBeNull();
189
- });
190
- });
191
-
192
- describe('getDirectoryStats', () => {
193
- it('should return stats structure', async () => {
194
- const stats = await getDirectoryStats();
195
-
196
- expect(stats).toHaveProperty('totalMerchants');
197
- expect(stats).toHaveProperty('verifiedMerchants');
198
- expect(stats).toHaveProperty('avgScore');
199
- expect(stats).toHaveProperty('totalCategories');
200
- expect(stats).toHaveProperty('totalCountries');
201
- expect(stats).toHaveProperty('gradeDistribution');
202
- expect(stats).toHaveProperty('topCategories');
203
- expect(stats).toHaveProperty('recentAdditions');
204
- });
205
-
206
- it('should return valid numeric values', async () => {
207
- const stats = await getDirectoryStats();
208
-
209
- expect(typeof stats.totalMerchants).toBe('number');
210
- expect(stats.totalMerchants).toBeGreaterThanOrEqual(0);
211
-
212
- // verifiedMerchants may be string from SQL aggregate, coerce to number
213
- const verifiedCount = Number(stats.verifiedMerchants);
214
- expect(verifiedCount).toBeGreaterThanOrEqual(0);
215
- expect(verifiedCount).toBeLessThanOrEqual(stats.totalMerchants);
216
-
217
- expect(typeof stats.avgScore).toBe('number');
218
- expect(stats.avgScore).toBeGreaterThanOrEqual(0);
219
- expect(stats.avgScore).toBeLessThanOrEqual(100);
220
- });
221
-
222
- it('should return arrays for distributions', async () => {
223
- const stats = await getDirectoryStats();
224
-
225
- expect(Array.isArray(stats.gradeDistribution)).toBe(true);
226
- expect(Array.isArray(stats.topCategories)).toBe(true);
227
- expect(Array.isArray(stats.recentAdditions)).toBe(true);
228
- });
229
- });
230
-
231
- describe('submitMerchant', () => {
232
- it('should reject domain without UCP profile', async () => {
233
- const result = await submitMerchant({
234
- domain: 'example.com', // example.com doesn't have UCP
235
- });
236
-
237
- expect(result.success).toBe(false);
238
- expect(result.error).toBeDefined();
239
- });
240
-
241
- it('should reject duplicate domain', async () => {
242
- const db = getDb();
243
- const domain = createTestDomain();
244
-
245
- // First, insert directly
246
- await db.insert(merchants).values({
247
- domain,
248
- displayName: 'Original Store',
249
- });
250
-
251
- // Then try to submit via service
252
- const result = await submitMerchant({ domain });
253
-
254
- expect(result.success).toBe(false);
255
- expect(result.error).toContain('already registered');
256
- });
257
-
258
- it('should clean domain before validation', async () => {
259
- // Test that https://, trailing slash, and uppercase are handled
260
- const result = await submitMerchant({
261
- domain: 'HTTPS://EXAMPLE.COM/',
262
- });
263
-
264
- // Should fail for UCP validation, not domain parsing
265
- expect(result.error).not.toContain('parse');
266
- });
267
- });
268
- });