@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.
- package/.claude/settings.local.json +60 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +279 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/compliance/compliance-generator.d.ts +34 -0
- package/dist/compliance/compliance-generator.d.ts.map +1 -0
- package/dist/compliance/compliance-generator.js +320 -0
- package/dist/compliance/compliance-generator.js.map +1 -0
- package/dist/compliance/index.d.ts +8 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +17 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/compliance/templates.d.ts +34 -0
- package/dist/compliance/templates.d.ts.map +1 -0
- package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
- package/dist/compliance/templates.js.map +1 -0
- package/dist/compliance/types.d.ts +64 -0
- package/dist/compliance/types.d.ts.map +1 -0
- package/dist/compliance/types.js +64 -0
- package/dist/compliance/types.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +63 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +444 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
- package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +642 -726
- package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
- package/dist/feed-analyzer/index.d.ts +8 -0
- package/dist/feed-analyzer/index.d.ts.map +1 -0
- package/dist/feed-analyzer/index.js +19 -0
- package/dist/feed-analyzer/index.js.map +1 -0
- package/dist/feed-analyzer/types.d.ts +204 -0
- package/dist/feed-analyzer/types.d.ts.map +1 -0
- package/dist/feed-analyzer/types.js +162 -0
- package/dist/feed-analyzer/types.js.map +1 -0
- package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +13 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/key-generator.d.ts +24 -0
- package/dist/generator/key-generator.d.ts.map +1 -0
- package/dist/generator/key-generator.js +144 -0
- package/dist/generator/key-generator.js.map +1 -0
- package/dist/generator/profile-builder.d.ts +15 -0
- package/dist/generator/profile-builder.d.ts.map +1 -0
- package/dist/generator/profile-builder.js +338 -0
- package/dist/generator/profile-builder.js.map +1 -0
- package/dist/hosting/artifacts-generator.d.ts +10 -0
- package/dist/hosting/artifacts-generator.d.ts.map +1 -0
- package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
- package/dist/hosting/artifacts-generator.js.map +1 -0
- package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
- package/dist/hosting/index.d.ts.map +1 -0
- package/dist/hosting/index.js +10 -0
- package/dist/hosting/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +12 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security-scanner.d.ts +10 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +541 -0
- package/dist/security/security-scanner.js.map +1 -0
- package/dist/security/types.d.ts +48 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +21 -0
- package/dist/security/types.js.map +1 -0
- package/dist/services/directory.d.ts +104 -0
- package/dist/services/directory.d.ts.map +1 -0
- package/dist/services/directory.js +333 -0
- package/dist/services/directory.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts +69 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -0
- package/{src/simulator/agent-simulator.ts → dist/simulator/agent-simulator.js} +650 -941
- package/dist/simulator/agent-simulator.js.map +1 -0
- package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
- package/dist/simulator/index.d.ts.map +1 -0
- package/dist/simulator/index.js +23 -0
- package/dist/simulator/index.js.map +1 -0
- package/{src/simulator/types.ts → dist/simulator/types.d.ts} +145 -170
- package/dist/simulator/types.d.ts.map +1 -0
- package/dist/simulator/types.js +18 -0
- package/dist/simulator/types.js.map +1 -0
- package/dist/types/generator.d.ts +106 -0
- package/dist/types/generator.d.ts.map +1 -0
- package/dist/types/generator.js +6 -0
- package/dist/types/generator.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/ucp-profile.d.ts +103 -0
- package/dist/types/ucp-profile.d.ts.map +1 -0
- package/dist/types/ucp-profile.js +45 -0
- package/dist/types/ucp-profile.js.map +1 -0
- package/dist/types/validation.d.ts +68 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +32 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/validator/index.d.ts +26 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +161 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/network-validator.d.ts +28 -0
- package/dist/validator/network-validator.d.ts.map +1 -0
- package/dist/validator/network-validator.js +319 -0
- package/dist/validator/network-validator.js.map +1 -0
- package/dist/validator/rules-validator.d.ts +11 -0
- package/dist/validator/rules-validator.d.ts.map +1 -0
- package/dist/validator/rules-validator.js +257 -0
- package/dist/validator/rules-validator.js.map +1 -0
- package/dist/validator/sdk-validator.d.ts +58 -0
- package/dist/validator/sdk-validator.d.ts.map +1 -0
- package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
- package/dist/validator/sdk-validator.js.map +1 -0
- package/dist/validator/structural-validator.d.ts +11 -0
- package/dist/validator/structural-validator.d.ts.map +1 -0
- package/dist/validator/structural-validator.js +415 -0
- package/dist/validator/structural-validator.js.map +1 -0
- package/package.json +1 -1
- package/publish-output.txt +0 -0
- package/CLAUDE.md +0 -109
- package/api/analyze-feed.js +0 -140
- package/api/badge.js +0 -185
- package/api/benchmark.js +0 -177
- package/api/directory-stats.ts +0 -29
- package/api/directory.ts +0 -73
- package/api/generate-compliance.js +0 -143
- package/api/generate-schema.js +0 -457
- package/api/generate.js +0 -132
- package/api/security-scan.js +0 -133
- package/api/simulate.js +0 -187
- package/api/tsconfig.json +0 -10
- package/api/validate.js +0 -1351
- package/apify-actor/.actor/actor.json +0 -68
- package/apify-actor/.actor/input_schema.json +0 -32
- package/apify-actor/APIFY-STORE-LISTING.md +0 -412
- package/apify-actor/Dockerfile +0 -8
- package/apify-actor/README.md +0 -166
- package/apify-actor/main.ts +0 -111
- package/apify-actor/package.json +0 -17
- package/apify-actor/src/main.js +0 -199
- package/docs/BRAND-IDENTITY.md +0 -238
- package/docs/BRAND-STYLE-GUIDE.md +0 -356
- package/drizzle/0000_black_king_cobra.sql +0 -39
- package/drizzle/meta/0000_snapshot.json +0 -309
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/public/.well-known/ucp +0 -25
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/brand.css +0 -321
- package/public/directory.html +0 -701
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/guides/bigcommerce.html +0 -743
- package/public/guides/fastucp.html +0 -838
- package/public/guides/magento.html +0 -779
- package/public/guides/shopify.html +0 -726
- package/public/guides/squarespace.html +0 -749
- package/public/guides/wix.html +0 -747
- package/public/guides/woocommerce.html +0 -733
- package/public/index.html +0 -3835
- package/public/learn.html +0 -396
- package/public/logo.jpeg +0 -0
- package/public/og-image-icon.png +0 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +0 -6
- package/public/site.webmanifest +0 -31
- package/public/sitemap.xml +0 -69
- package/public/social/linkedin-banner-1128x191.png +0 -0
- package/public/social/temp.PNG +0 -0
- package/public/social/x-header-1500x500.png +0 -0
- package/public/verify.html +0 -410
- package/scripts/generate-favicons.js +0 -44
- package/scripts/generate-ico.js +0 -23
- package/scripts/generate-og-image.js +0 -45
- package/scripts/reset-db.ts +0 -77
- package/scripts/seed-db.ts +0 -71
- package/scripts/setup-benchmark-db.js +0 -70
- package/src/api/server.ts +0 -266
- package/src/cli/index.ts +0 -302
- package/src/compliance/compliance-generator.ts +0 -452
- package/src/compliance/index.ts +0 -28
- package/src/compliance/types.ts +0 -170
- package/src/db/index.ts +0 -28
- package/src/db/schema.ts +0 -84
- package/src/feed-analyzer/index.ts +0 -34
- package/src/feed-analyzer/types.ts +0 -354
- package/src/generator/key-generator.ts +0 -124
- package/src/generator/profile-builder.ts +0 -402
- package/src/index.ts +0 -105
- package/src/security/security-scanner.ts +0 -604
- package/src/security/types.ts +0 -55
- package/src/services/directory.ts +0 -434
- package/src/types/generator.ts +0 -140
- package/src/types/ucp-profile.ts +0 -140
- package/src/types/validation.ts +0 -89
- package/src/validator/index.ts +0 -194
- package/src/validator/network-validator.ts +0 -417
- package/src/validator/rules-validator.ts +0 -297
- package/src/validator/structural-validator.ts +0 -476
- package/tests/fixtures/non-compliant-profile.json +0 -25
- package/tests/fixtures/official-sample-profile.json +0 -75
- package/tests/integration/benchmark.test.ts +0 -207
- package/tests/integration/database.test.ts +0 -163
- package/tests/integration/directory-api.test.ts +0 -268
- package/tests/integration/simulate-api.test.ts +0 -230
- package/tests/integration/validate-api.test.ts +0 -269
- package/tests/setup.ts +0 -15
- package/tests/unit/agent-simulator.test.ts +0 -575
- package/tests/unit/compliance-generator.test.ts +0 -374
- package/tests/unit/directory-service.test.ts +0 -272
- package/tests/unit/feed-analyzer.test.ts +0 -517
- package/tests/unit/lint-suggestions.test.ts +0 -423
- package/tests/unit/official-samples.test.ts +0 -211
- package/tests/unit/pdf-report.test.ts +0 -390
- package/tests/unit/sdk-validator.test.ts +0 -531
- package/tests/unit/security-scanner.test.ts +0 -410
- package/tests/unit/validation.test.ts +0 -390
- package/vercel.json +0 -34
- 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
|
-
});
|