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 - z 0 - 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