Skip to content

Commit f68ee0b

Browse files
claude[bot]Custard7
andcommitted
Add comprehensive test coverage for App DIDs functionality
- Brain service unit tests covering slug generation, DID resolution, and security validation - E2E tests for complete App DIDs workflow including credential issuance - Security tests for directory traversal and XSS prevention - Edge case handling for missing signing authorities and collision detection - Tests support both development (draft) and production (listed) app statuses 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Jacksón Smith <Custard7@users.noreply.github.com>
1 parent 1b93118 commit f68ee0b

2 files changed

Lines changed: 721 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { describe, it, beforeEach, afterAll, expect } from 'vitest';
2+
3+
import { AppStoreListing, Integration, Profile, SigningAuthority } from '@models';
4+
5+
import { createAppStoreListing } from '@accesslayer/app-store-listing/create';
6+
import { readAppStoreListingById, readAppStoreListingBySlug } from '@accesslayer/app-store-listing/read';
7+
import { updateAppStoreListing } from '@accesslayer/app-store-listing/update';
8+
import { createIntegration } from '@accesslayer/integration/create';
9+
import { createSigningAuthority } from '@accesslayer/signing-authority/create';
10+
import { getAppDidWeb } from '@helpers/did.helpers';
11+
12+
// Test helpers
13+
const makeListingInput = (overrides?: Record<string, any>) => ({
14+
display_name: 'Test App',
15+
tagline: 'A test application',
16+
full_description: 'This is a comprehensive test application for the app store',
17+
icon_url: 'https://example.com/icon.png',
18+
app_listing_status: 'DRAFT' as const,
19+
launch_type: 'EMBEDDED_IFRAME' as const,
20+
launch_config_json: JSON.stringify({ iframeUrl: 'https://app.example.com' }),
21+
category: 'Learning',
22+
promotion_level: 'STANDARD' as const,
23+
...overrides,
24+
});
25+
26+
// Simple slug generator for testing (mirrors the one in routes/app-store.ts)
27+
const generateSlugFromName = (name: string): string => {
28+
return name
29+
.toLowerCase()
30+
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
31+
.replace(/\s+/g, '-') // Replace spaces with hyphens
32+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
33+
.replace(/-+/g, '-'); // Replace multiple hyphens with single
34+
};
35+
36+
describe('App DIDs Access Layer', () => {
37+
beforeEach(async () => {
38+
// Clean up before each test
39+
await AppStoreListing.delete({ detach: true, where: {} });
40+
await Integration.delete({ detach: true, where: {} });
41+
await Profile.delete({ detach: true, where: {} });
42+
await SigningAuthority.delete({ detach: true, where: {} });
43+
});
44+
45+
afterAll(async () => {
46+
// Clean up after all tests
47+
await AppStoreListing.delete({ detach: true, where: {} });
48+
await Integration.delete({ detach: true, where: {} });
49+
await Profile.delete({ detach: true, where: {} });
50+
await SigningAuthority.delete({ detach: true, where: {} });
51+
});
52+
53+
describe('Slug Generation and Management', () => {
54+
it('creates listing with slug field', async () => {
55+
const slug = 'test-app-slug';
56+
const listing = await createAppStoreListing(
57+
makeListingInput({
58+
slug,
59+
display_name: 'Test App With Slug'
60+
})
61+
);
62+
63+
expect(listing.slug).toBe(slug);
64+
expect(listing.display_name).toBe('Test App With Slug');
65+
});
66+
67+
it('allows creating listing without explicit slug', async () => {
68+
const listing = await createAppStoreListing(
69+
makeListingInput({ display_name: 'Auto Slug App' })
70+
);
71+
72+
expect(listing.listing_id).toBeTruthy();
73+
expect(listing.display_name).toBe('Auto Slug App');
74+
// Slug may be undefined/null if not set by route logic
75+
});
76+
77+
it('supports reading listing by slug', async () => {
78+
const slug = 'readable-slug';
79+
const created = await createAppStoreListing(
80+
makeListingInput({
81+
slug,
82+
display_name: 'Readable App'
83+
})
84+
);
85+
86+
const bySlug = await readAppStoreListingBySlug(slug);
87+
88+
expect(bySlug).toBeTruthy();
89+
expect(bySlug?.listing_id).toBe(created.listing_id);
90+
expect(bySlug?.display_name).toBe('Readable App');
91+
});
92+
93+
it('returns null for non-existent slug', async () => {
94+
const result = await readAppStoreListingBySlug('non-existent-slug');
95+
expect(result).toBeNull();
96+
});
97+
98+
it('handles multiple listings with different slugs', async () => {
99+
const listing1 = await createAppStoreListing(
100+
makeListingInput({
101+
slug: 'first-app',
102+
display_name: 'First App'
103+
})
104+
);
105+
106+
const listing2 = await createAppStoreListing(
107+
makeListingInput({
108+
slug: 'second-app',
109+
display_name: 'Second App'
110+
})
111+
);
112+
113+
const firstBySlug = await readAppStoreListingBySlug('first-app');
114+
const secondBySlug = await readAppStoreListingBySlug('second-app');
115+
116+
expect(firstBySlug?.listing_id).toBe(listing1.listing_id);
117+
expect(secondBySlug?.listing_id).toBe(listing2.listing_id);
118+
});
119+
120+
it('updates listing slug', async () => {
121+
const listing = await createAppStoreListing(
122+
makeListingInput({
123+
slug: 'original-slug',
124+
display_name: 'Original App'
125+
})
126+
);
127+
128+
await updateAppStoreListing(listing, {
129+
slug: 'updated-slug',
130+
display_name: 'Updated App'
131+
});
132+
133+
const originalSlugResult = await readAppStoreListingBySlug('original-slug');
134+
const updatedSlugResult = await readAppStoreListingBySlug('updated-slug');
135+
136+
expect(originalSlugResult).toBeNull();
137+
expect(updatedSlugResult?.listing_id).toBe(listing.listing_id);
138+
expect(updatedSlugResult?.display_name).toBe('Updated App');
139+
});
140+
});
141+
142+
describe('App DID Helper Functions', () => {
143+
it('constructs proper app DID format', () => {
144+
const domain = 'localhost%3A4000';
145+
const slug = 'test-app';
146+
const expectedDid = 'did:web:localhost%3A4000:app:test-app';
147+
148+
const appDid = getAppDidWeb(domain, slug);
149+
expect(appDid).toBe(expectedDid);
150+
});
151+
152+
it('handles different domain formats', () => {
153+
const testCases = [
154+
{ domain: 'example.com', slug: 'app', expected: 'did:web:example.com:app:app' },
155+
{ domain: 'sub.domain.com', slug: 'my-app', expected: 'did:web:sub.domain.com:app:my-app' },
156+
{ domain: 'localhost%3A8080', slug: 'dev-app', expected: 'did:web:localhost%3A8080:app:dev-app' },
157+
];
158+
159+
for (const { domain, slug, expected } of testCases) {
160+
const result = getAppDidWeb(domain, slug);
161+
expect(result).toBe(expected);
162+
}
163+
});
164+
165+
it('handles edge cases in DID construction', () => {
166+
// Test that the helper doesn't throw on edge cases
167+
expect(() => getAppDidWeb('', '')).not.toThrow();
168+
expect(() => getAppDidWeb('domain.com', '')).not.toThrow();
169+
expect(() => getAppDidWeb('', 'slug')).not.toThrow();
170+
});
171+
});
172+
173+
describe('Slug Validation and Security', () => {
174+
it('demonstrates slug sanitization requirements', () => {
175+
// These tests show what the route layer should do for slug generation
176+
const testCases = [
177+
{ input: 'Simple App', expected: 'simple-app' },
178+
{ input: 'App with @#$%^&*()! chars', expected: 'app-with-chars' },
179+
{ input: ' Whitespace App ', expected: 'whitespace-app' },
180+
{ input: 'Multiple---Hyphens', expected: 'multiple-hyphens' },
181+
{ input: 'UPPERCASE APP', expected: 'uppercase-app' },
182+
{ input: '123 Number App', expected: '123-number-app' },
183+
];
184+
185+
for (const { input, expected } of testCases) {
186+
const result = generateSlugFromName(input);
187+
expect(result).toBe(expected);
188+
}
189+
});
190+
191+
it('handles very long names', () => {
192+
const longName = 'This is an extremely long app name that should be handled gracefully by the slug generation system';
193+
const result = generateSlugFromName(longName);
194+
195+
expect(result).toBeTruthy();
196+
expect(result.length).toBeGreaterThan(0);
197+
expect(result).not.toContain(' '); // Should not contain spaces
198+
});
199+
});
200+
201+
describe('App Listing Status Handling', () => {
202+
it('creates listings with different statuses', async () => {
203+
const statuses = ['DRAFT', 'PENDING_REVIEW', 'LISTED', 'ARCHIVED'] as const;
204+
205+
const createdListings = [];
206+
207+
for (const status of statuses) {
208+
const listing = await createAppStoreListing(
209+
makeListingInput({
210+
slug: `${status.toLowerCase().replace('_', '-')}-app`,
211+
display_name: `${status} App`,
212+
app_listing_status: status
213+
})
214+
);
215+
createdListings.push({ listing, status });
216+
}
217+
218+
// Verify all listings were created with correct status
219+
for (const { listing, status } of createdListings) {
220+
const retrieved = await readAppStoreListingById(listing.listing_id);
221+
expect(retrieved?.app_listing_status).toBe(status);
222+
}
223+
});
224+
225+
it('updates listing status', async () => {
226+
const listing = await createAppStoreListing(
227+
makeListingInput({
228+
slug: 'status-test-app',
229+
app_listing_status: 'DRAFT'
230+
})
231+
);
232+
233+
expect(listing.app_listing_status).toBe('DRAFT');
234+
235+
await updateAppStoreListing(listing, { app_listing_status: 'LISTED' });
236+
237+
const updated = await readAppStoreListingById(listing.listing_id);
238+
expect(updated?.app_listing_status).toBe('LISTED');
239+
});
240+
241+
it('supports slug lookup for all statuses', async () => {
242+
// App DIDs should be resolvable for all statuses to support dev/test apps
243+
const statuses = ['DRAFT', 'LISTED', 'ARCHIVED'] as const;
244+
245+
for (const status of statuses) {
246+
const listing = await createAppStoreListing(
247+
makeListingInput({
248+
slug: `status-${status.toLowerCase()}-app`,
249+
app_listing_status: status
250+
})
251+
);
252+
253+
const bySlug = await readAppStoreListingBySlug(listing.slug!);
254+
expect(bySlug?.app_listing_status).toBe(status);
255+
}
256+
});
257+
});
258+
259+
describe('Integration with Signing Authorities', () => {
260+
it('creates signing authority for app DID usage', async () => {
261+
const sa = await createSigningAuthority({
262+
name: 'lca-sa',
263+
did: `did:key:test${Math.random()}`,
264+
endpoint: 'https://example.com/sign',
265+
isDefault: false,
266+
});
267+
268+
expect(sa.name).toBe('lca-sa');
269+
expect(sa.did).toContain('did:key:');
270+
expect(sa.endpoint).toBe('https://example.com/sign');
271+
});
272+
273+
it('creates integration for app listing association', async () => {
274+
const integration = await createIntegration({
275+
name: 'Test App Integration',
276+
description: 'Integration for app DID testing',
277+
whitelistedDomains: ['example.com', 'localhost'],
278+
});
279+
280+
expect(integration.name).toBe('Test App Integration');
281+
expect(integration.whitelistedDomains).toContain('example.com');
282+
expect(integration.whitelistedDomains).toContain('localhost');
283+
});
284+
});
285+
286+
describe('Error Handling', () => {
287+
it('handles database constraints for unique listing_id', async () => {
288+
const listingId = 'duplicate-listing-id';
289+
290+
await createAppStoreListing(
291+
makeListingInput({ listing_id: listingId })
292+
);
293+
294+
// Second creation with same listing_id should fail
295+
await expect(
296+
createAppStoreListing(
297+
makeListingInput({ listing_id: listingId })
298+
)
299+
).rejects.toThrow();
300+
});
301+
302+
it('handles database constraints for unique slug', async () => {
303+
const slug = 'duplicate-slug';
304+
305+
await createAppStoreListing(
306+
makeListingInput({ slug })
307+
);
308+
309+
// Second creation with same slug should fail due to unique constraint
310+
await expect(
311+
createAppStoreListing(
312+
makeListingInput({ slug })
313+
)
314+
).rejects.toThrow();
315+
});
316+
317+
it('handles missing required fields', async () => {
318+
await expect(
319+
createAppStoreListing({} as any)
320+
).rejects.toThrow();
321+
});
322+
});
323+
});

0 commit comments

Comments
 (0)