@igstack/app-catalog-test-kit 0.18.6
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/README.md +51 -0
- package/dist/esm/harness/MockBackendVerifier.d.ts +8 -0
- package/dist/esm/harness/MockBackendVerifier.js +15 -0
- package/dist/esm/harness/MockBackendVerifier.js.map +1 -0
- package/dist/esm/harness/given.d.ts +23 -0
- package/dist/esm/harness/given.js +144 -0
- package/dist/esm/harness/given.js.map +1 -0
- package/dist/esm/index.d.ts +38 -0
- package/dist/esm/index.js +45 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mock-backend/BrowserStateCfg.d.ts +18 -0
- package/dist/esm/mock-backend/BrowserStateCfg.js +33 -0
- package/dist/esm/mock-backend/BrowserStateCfg.js.map +1 -0
- package/dist/esm/mock-backend/MockBackendConfigurer.d.ts +17 -0
- package/dist/esm/mock-backend/MockBackendConfigurer.js +80 -0
- package/dist/esm/mock-backend/MockBackendConfigurer.js.map +1 -0
- package/dist/esm/mock-backend/MockDb.d.ts +18 -0
- package/dist/esm/mock-backend/MockDb.js +56 -0
- package/dist/esm/mock-backend/MockDb.js.map +1 -0
- package/dist/esm/mock-backend/MockService.d.ts +10 -0
- package/dist/esm/mock-backend/MockService.js +16 -0
- package/dist/esm/mock-backend/MockService.js.map +1 -0
- package/dist/esm/mock-backend/MockUserContext.d.ts +11 -0
- package/dist/esm/mock-backend/MockUserContext.js +38 -0
- package/dist/esm/mock-backend/MockUserContext.js.map +1 -0
- package/dist/esm/mock-backend/magazines.d.ts +22 -0
- package/dist/esm/mock-backend/magazines.js +186 -0
- package/dist/esm/mock-backend/magazines.js.map +1 -0
- package/dist/esm/mock-network/NetworkCatalog.d.ts +12 -0
- package/dist/esm/mock-network/NetworkCatalog.js +24 -0
- package/dist/esm/mock-network/NetworkCatalog.js.map +1 -0
- package/dist/esm/mock-network/NetworkConfigurerCfg.d.ts +13 -0
- package/dist/esm/mock-network/NetworkConfigurerCfg.js +16 -0
- package/dist/esm/mock-network/NetworkConfigurerCfg.js.map +1 -0
- package/dist/esm/mock-network/SharedNetwork.d.ts +13 -0
- package/dist/esm/mock-network/SharedNetwork.js +149 -0
- package/dist/esm/mock-network/SharedNetwork.js.map +1 -0
- package/dist/esm/mock-network/errorFactories.d.ts +11 -0
- package/dist/esm/mock-network/errorFactories.js +22 -0
- package/dist/esm/mock-network/errorFactories.js.map +1 -0
- package/dist/esm/mock-network/makeNetworkReplyWithCatalog.d.ts +3 -0
- package/dist/esm/mock-network/makeNetworkReplyWithCatalog.js +17 -0
- package/dist/esm/mock-network/makeNetworkReplyWithCatalog.js.map +1 -0
- package/dist/esm/setup/polyfillLocalStorage.d.ts +0 -0
- package/dist/esm/setup/polyfillLocalStorage.js +25 -0
- package/dist/esm/setup/polyfillLocalStorage.js.map +1 -0
- package/dist/esm/setup/svgMock.d.ts +1 -0
- package/dist/esm/setup/testSetup.d.ts +0 -0
- package/dist/esm/setup/testSetup.js +58 -0
- package/dist/esm/setup/testSetup.js.map +1 -0
- package/dist/esm/tools/AppDetailTools.d.ts +63 -0
- package/dist/esm/tools/AppDetailTools.js +180 -0
- package/dist/esm/tools/AppDetailTools.js.map +1 -0
- package/dist/esm/tools/BrowserState.d.ts +25 -0
- package/dist/esm/tools/BrowserState.js +30 -0
- package/dist/esm/tools/BrowserState.js.map +1 -0
- package/dist/esm/tools/CatalogTools.d.ts +67 -0
- package/dist/esm/tools/CatalogTools.js +202 -0
- package/dist/esm/tools/CatalogTools.js.map +1 -0
- package/dist/esm/tools/ErrorTools.d.ts +15 -0
- package/dist/esm/tools/ErrorTools.js +34 -0
- package/dist/esm/tools/ErrorTools.js.map +1 -0
- package/dist/esm/tools/GalleryTools.d.ts +6 -0
- package/dist/esm/tools/GalleryTools.js +22 -0
- package/dist/esm/tools/GalleryTools.js.map +1 -0
- package/dist/esm/tools/suppressConsole.d.ts +19 -0
- package/dist/esm/tools/suppressConsole.js +44 -0
- package/dist/esm/tools/suppressConsole.js.map +1 -0
- package/package.json +87 -0
- package/src/harness/MockBackendVerifier.ts +14 -0
- package/src/harness/given.tsx +212 -0
- package/src/index.ts +44 -0
- package/src/mock-backend/BrowserStateCfg.ts +35 -0
- package/src/mock-backend/MockBackendConfigurer.ts +97 -0
- package/src/mock-backend/MockDb.ts +66 -0
- package/src/mock-backend/MockService.ts +18 -0
- package/src/mock-backend/MockUserContext.ts +42 -0
- package/src/mock-backend/magazines.ts +245 -0
- package/src/mock-network/NetworkCatalog.ts +30 -0
- package/src/mock-network/NetworkConfigurerCfg.ts +24 -0
- package/src/mock-network/SharedNetwork.ts +100 -0
- package/src/mock-network/errorFactories.ts +28 -0
- package/src/mock-network/makeNetworkReplyWithCatalog.ts +19 -0
- package/src/setup/polyfillLocalStorage.ts +34 -0
- package/src/setup/svgMock.tsx +3 -0
- package/src/setup/testSetup.ts +58 -0
- package/src/tools/AppDetailTools.ts +240 -0
- package/src/tools/BrowserState.ts +41 -0
- package/src/tools/CatalogTools.ts +254 -0
- package/src/tools/ErrorTools.ts +49 -0
- package/src/tools/GalleryTools.ts +26 -0
- package/src/tools/suppressConsole.ts +66 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppApprovalMethod,
|
|
3
|
+
GroupingTagDefinition,
|
|
4
|
+
Resource,
|
|
5
|
+
} from '@igstack/app-catalog-backend-core'
|
|
6
|
+
import type { MockDb } from './MockDb'
|
|
7
|
+
import type { MockUserContext, UserConfig } from './MockUserContext'
|
|
8
|
+
|
|
9
|
+
let counter = 0
|
|
10
|
+
|
|
11
|
+
function nextId(): string {
|
|
12
|
+
counter++
|
|
13
|
+
return `test-id-${counter}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nextSlug(): string {
|
|
17
|
+
return `app-${counter}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class MockBackendConfigurer {
|
|
21
|
+
constructor(
|
|
22
|
+
readonly db: MockDb,
|
|
23
|
+
readonly userContext: MockUserContext,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
withApp(overrides?: Partial<Resource>): Resource {
|
|
27
|
+
const id = overrides?.id ?? nextId()
|
|
28
|
+
const app: Resource = {
|
|
29
|
+
id,
|
|
30
|
+
slug: overrides?.slug ?? nextSlug(),
|
|
31
|
+
displayName: overrides?.displayName ?? `App ${counter}`,
|
|
32
|
+
description: overrides?.description,
|
|
33
|
+
tags: overrides?.tags ?? [],
|
|
34
|
+
screenshotIds: overrides?.screenshotIds ?? [],
|
|
35
|
+
...overrides,
|
|
36
|
+
}
|
|
37
|
+
this.db.upsertResource(app)
|
|
38
|
+
return app
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
withTag(overrides?: Partial<GroupingTagDefinition>): GroupingTagDefinition {
|
|
42
|
+
const def: GroupingTagDefinition = {
|
|
43
|
+
prefix: overrides?.prefix ?? `tag-${nextId()}`,
|
|
44
|
+
displayName: overrides?.displayName ?? 'Tag',
|
|
45
|
+
description: overrides?.description ?? '',
|
|
46
|
+
values: overrides?.values ?? [],
|
|
47
|
+
...overrides,
|
|
48
|
+
}
|
|
49
|
+
this.db.setTagDefinitions([...this.db.tagsDefinitions, def])
|
|
50
|
+
return def
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
withApprovalMethod(
|
|
54
|
+
overrides?: Partial<AppApprovalMethod>,
|
|
55
|
+
): AppApprovalMethod {
|
|
56
|
+
const slug = overrides?.slug ?? `method-${nextId()}`
|
|
57
|
+
const method = {
|
|
58
|
+
slug,
|
|
59
|
+
type: 'service' as const,
|
|
60
|
+
displayName: overrides?.displayName ?? 'Approval Method',
|
|
61
|
+
config: {},
|
|
62
|
+
...overrides,
|
|
63
|
+
} as AppApprovalMethod
|
|
64
|
+
this.db.setApprovalMethods([...this.db.approvalMethods, method])
|
|
65
|
+
return method
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
withSubResource(
|
|
69
|
+
overrides: Partial<Resource> & { appSlug: string },
|
|
70
|
+
): Resource {
|
|
71
|
+
const appSlug = overrides.appSlug
|
|
72
|
+
const id = overrides.id ?? nextId()
|
|
73
|
+
const resource: Resource = {
|
|
74
|
+
id,
|
|
75
|
+
slug: overrides.slug ?? `sr-${id}`,
|
|
76
|
+
displayName: overrides.displayName ?? `Sub Resource ${counter}`,
|
|
77
|
+
aliases: overrides.aliases ?? [],
|
|
78
|
+
approverSlugs: overrides.approverSlugs ?? [],
|
|
79
|
+
parentSlug: appSlug,
|
|
80
|
+
tier: overrides.tier,
|
|
81
|
+
ownerPersonSlug: overrides.ownerPersonSlug,
|
|
82
|
+
accessRequest: overrides.accessRequest,
|
|
83
|
+
description: overrides.description,
|
|
84
|
+
}
|
|
85
|
+
this.db.upsertResource(resource)
|
|
86
|
+
return resource
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
withUser(overrides: Partial<UserConfig>): void {
|
|
90
|
+
this.userContext.setUser(overrides)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Reset the counter between tests */
|
|
95
|
+
export function resetConfigurerCounter(): void {
|
|
96
|
+
counter = 0
|
|
97
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppApprovalMethod,
|
|
3
|
+
AppCatalogData,
|
|
4
|
+
GroupingTagDefinition,
|
|
5
|
+
Resource,
|
|
6
|
+
} from '@igstack/app-catalog-backend-core'
|
|
7
|
+
|
|
8
|
+
export class MockDb {
|
|
9
|
+
resources: Resource[] = []
|
|
10
|
+
tagsDefinitions: GroupingTagDefinition[] = []
|
|
11
|
+
approvalMethods: AppApprovalMethod[] = []
|
|
12
|
+
|
|
13
|
+
upsertResource(resource: Resource): void {
|
|
14
|
+
this.resources = [
|
|
15
|
+
...this.resources.filter((r) => r.id !== resource.id),
|
|
16
|
+
resource,
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @deprecated Use upsertResource */
|
|
21
|
+
upsertApp(app: Resource): void {
|
|
22
|
+
this.upsertResource(app)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getResources(): Resource[] {
|
|
26
|
+
return this.resources
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @deprecated Use getResources */
|
|
30
|
+
getApps(): Resource[] {
|
|
31
|
+
return this.getResources()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getResource(slug: string): Resource {
|
|
35
|
+
const resource = this.resources.find((r) => r.slug === slug)
|
|
36
|
+
if (!resource) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`MockDb: resource with slug "${slug}" not found. Available: ${this.resources.map((r) => r.slug).join(', ')}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return resource
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @deprecated Use getResource */
|
|
45
|
+
getApp(slug: string): Resource {
|
|
46
|
+
return this.getResource(slug)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setTagDefinitions(defs: GroupingTagDefinition[]): void {
|
|
50
|
+
this.tagsDefinitions = defs
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setApprovalMethods(methods: AppApprovalMethod[]): void {
|
|
54
|
+
this.approvalMethods = methods
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getAppCatalogData(): AppCatalogData {
|
|
58
|
+
return {
|
|
59
|
+
resources: this.resources,
|
|
60
|
+
tagsDefinitions: this.tagsDefinitions,
|
|
61
|
+
approvalMethods: this.approvalMethods,
|
|
62
|
+
persons: [],
|
|
63
|
+
groups: [],
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AppCatalogData } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import type { MockDb } from './MockDb'
|
|
3
|
+
import type { MockUserContext } from './MockUserContext'
|
|
4
|
+
|
|
5
|
+
export class MockService {
|
|
6
|
+
constructor(
|
|
7
|
+
readonly db: MockDb,
|
|
8
|
+
readonly userContext: MockUserContext,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
getAppCatalogData(): AppCatalogData {
|
|
12
|
+
return this.db.getAppCatalogData()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getSessionResponse(): object | null {
|
|
16
|
+
return this.userContext.getSessionResponse()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface UserConfig {
|
|
2
|
+
name: string
|
|
3
|
+
isAdmin: boolean
|
|
4
|
+
isAuthenticated: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const defaultUser: UserConfig = {
|
|
8
|
+
name: 'Test User',
|
|
9
|
+
isAdmin: false,
|
|
10
|
+
isAuthenticated: true,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class MockUserContext {
|
|
14
|
+
private user: UserConfig = { ...defaultUser }
|
|
15
|
+
|
|
16
|
+
setUser(overrides: Partial<UserConfig>): void {
|
|
17
|
+
this.user = { ...this.user, ...overrides }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getUser(): UserConfig {
|
|
21
|
+
return this.user
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getSessionResponse(): object | null {
|
|
25
|
+
if (!this.user.isAuthenticated) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
user: {
|
|
30
|
+
id: 'test-user-id',
|
|
31
|
+
email: `${this.user.name.toLowerCase().replace(/\s/g, '.')}@test.com`,
|
|
32
|
+
name: this.user.name,
|
|
33
|
+
isAdmin: this.user.isAdmin,
|
|
34
|
+
},
|
|
35
|
+
session: {
|
|
36
|
+
id: 'test-session-id',
|
|
37
|
+
userId: 'test-user-id',
|
|
38
|
+
expiresAt: new Date(Date.now() + 86400000).toISOString(),
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { MockBackendConfigurer } from './MockBackendConfigurer'
|
|
2
|
+
import type { BrowserStateCfg } from './BrowserStateCfg'
|
|
3
|
+
import type { NetworkConfigurerCfg } from '../mock-network/NetworkConfigurerCfg'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Core types
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export interface ConfigurerContext {
|
|
10
|
+
backendCfg: MockBackendConfigurer
|
|
11
|
+
browserStateCfg: BrowserStateCfg
|
|
12
|
+
networkCfg: NetworkConfigurerCfg
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A magazine is a function that configures the context */
|
|
16
|
+
export type Magazine = (ctx: ConfigurerContext) => void
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Full magazine features
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
interface FullFeatures {
|
|
23
|
+
prepopulateCache?: boolean
|
|
24
|
+
dismissOnboarding?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fullMagazine(
|
|
28
|
+
features: FullFeatures,
|
|
29
|
+
postConfigure?: Magazine,
|
|
30
|
+
): Magazine {
|
|
31
|
+
return (ctx) => {
|
|
32
|
+
const { backendCfg, browserStateCfg } = ctx
|
|
33
|
+
|
|
34
|
+
// Approval methods — all 3 types
|
|
35
|
+
const supportPortal = backendCfg.withApprovalMethod({
|
|
36
|
+
slug: 'it-support',
|
|
37
|
+
type: 'service',
|
|
38
|
+
displayName: 'Support Portal',
|
|
39
|
+
config: {
|
|
40
|
+
url: 'https://support.example.com',
|
|
41
|
+
description: 'General IT support desk',
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
backendCfg.withApprovalMethod({
|
|
45
|
+
slug: 'ops-support',
|
|
46
|
+
type: 'service',
|
|
47
|
+
displayName: 'Ops Portal',
|
|
48
|
+
config: { url: 'https://opsdesk.example.com' },
|
|
49
|
+
})
|
|
50
|
+
const managerApproval = backendCfg.withApprovalMethod({
|
|
51
|
+
slug: 'manager-approval',
|
|
52
|
+
type: 'custom',
|
|
53
|
+
displayName: 'Manager Approval',
|
|
54
|
+
config: {},
|
|
55
|
+
})
|
|
56
|
+
backendCfg.withApprovalMethod({
|
|
57
|
+
slug: 'self-service',
|
|
58
|
+
type: 'custom',
|
|
59
|
+
displayName: 'Self-Service',
|
|
60
|
+
config: {},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Tag definitions
|
|
64
|
+
backendCfg.withTag({
|
|
65
|
+
prefix: 'category',
|
|
66
|
+
displayName: 'Category',
|
|
67
|
+
description: 'Application category',
|
|
68
|
+
values: [
|
|
69
|
+
{
|
|
70
|
+
value: 'project-management',
|
|
71
|
+
displayName: 'Project Management',
|
|
72
|
+
description: '',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
value: 'communication',
|
|
76
|
+
displayName: 'Communication',
|
|
77
|
+
description: '',
|
|
78
|
+
},
|
|
79
|
+
{ value: 'internal', displayName: 'Internal Tools', description: '' },
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
backendCfg.withTag({
|
|
83
|
+
prefix: 'team',
|
|
84
|
+
displayName: 'Team',
|
|
85
|
+
description: 'Owning team',
|
|
86
|
+
values: [
|
|
87
|
+
{ value: 'engineering', displayName: 'Engineering', description: '' },
|
|
88
|
+
{ value: 'platform', displayName: 'Platform', description: '' },
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Apps
|
|
93
|
+
backendCfg.withApp({
|
|
94
|
+
slug: 'taskflow',
|
|
95
|
+
displayName: 'TaskFlow',
|
|
96
|
+
description: 'Project tracking and issue management',
|
|
97
|
+
tags: ['category:project-management', 'team:engineering'],
|
|
98
|
+
screenshotIds: ['ss-taskflow-1', 'ss-taskflow-2', 'ss-taskflow-3'],
|
|
99
|
+
appUrl: 'https://taskflow.example.com',
|
|
100
|
+
accessRequest: {
|
|
101
|
+
approvalMethodSlug: supportPortal.slug,
|
|
102
|
+
comments: 'Submit a ticket',
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
backendCfg.withApp({
|
|
107
|
+
slug: 'teamchat',
|
|
108
|
+
displayName: 'TeamChat',
|
|
109
|
+
description: 'Team messaging and collaboration',
|
|
110
|
+
tags: ['category:communication', 'team:platform'],
|
|
111
|
+
screenshotIds: ['ss-teamchat-1'],
|
|
112
|
+
appUrl: 'https://teamchat.example.com',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const newTool = backendCfg.withApp({
|
|
116
|
+
slug: 'new-tool',
|
|
117
|
+
displayName: 'New Tool',
|
|
118
|
+
description: 'The modern replacement',
|
|
119
|
+
tags: ['category:internal'],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
backendCfg.withApp({
|
|
123
|
+
slug: 'old-tool',
|
|
124
|
+
displayName: 'Old Tool',
|
|
125
|
+
description: 'Legacy project tracker',
|
|
126
|
+
tags: ['category:project-management'],
|
|
127
|
+
deprecated: {
|
|
128
|
+
type: 'deprecated',
|
|
129
|
+
comment: 'Replaced by New Tool',
|
|
130
|
+
replacementSlug: newTool.slug,
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
backendCfg.withApp({
|
|
135
|
+
slug: 'legacy-app',
|
|
136
|
+
displayName: 'Legacy App',
|
|
137
|
+
description: 'Still works but discouraged',
|
|
138
|
+
tags: ['category:internal'],
|
|
139
|
+
deprecated: {
|
|
140
|
+
type: 'discouraged',
|
|
141
|
+
comment: 'Consider using alternatives',
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
backendCfg.withApp({
|
|
146
|
+
slug: 'internal-portal',
|
|
147
|
+
displayName: 'Internal Portal',
|
|
148
|
+
description: 'Company intranet portal',
|
|
149
|
+
tags: ['category:internal', 'team:platform'],
|
|
150
|
+
screenshotIds: ['ss-portal-1'],
|
|
151
|
+
accessRequest: {
|
|
152
|
+
approvalMethodSlug: managerApproval.slug,
|
|
153
|
+
comments: 'Requires manager approval',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
backendCfg.withApp({
|
|
158
|
+
slug: 'admin-console',
|
|
159
|
+
displayName: 'Admin Console',
|
|
160
|
+
description: 'System administration dashboard',
|
|
161
|
+
tags: ['category:internal', 'team:engineering'],
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Apply features
|
|
165
|
+
if (features.dismissOnboarding) {
|
|
166
|
+
browserStateCfg.dismissOnboarding()
|
|
167
|
+
}
|
|
168
|
+
if (features.prepopulateCache) {
|
|
169
|
+
browserStateCfg.withOfflineData()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Apply post-configurer (for test-specific overrides)
|
|
173
|
+
postConfigure?.(ctx)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Single magazine
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function singleMagazine(postConfigure?: Magazine): Magazine {
|
|
182
|
+
return (ctx) => {
|
|
183
|
+
const { backendCfg } = ctx
|
|
184
|
+
|
|
185
|
+
const method = backendCfg.withApprovalMethod({
|
|
186
|
+
slug: 'it-support',
|
|
187
|
+
type: 'service',
|
|
188
|
+
displayName: 'Support Portal',
|
|
189
|
+
config: { url: 'https://support.example.com' },
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
backendCfg.withTag({
|
|
193
|
+
prefix: 'category',
|
|
194
|
+
displayName: 'Category',
|
|
195
|
+
description: 'Application category',
|
|
196
|
+
values: [
|
|
197
|
+
{
|
|
198
|
+
value: 'project-management',
|
|
199
|
+
displayName: 'Project Management',
|
|
200
|
+
description: '',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
backendCfg.withApp({
|
|
206
|
+
slug: 'taskflow',
|
|
207
|
+
displayName: 'TaskFlow',
|
|
208
|
+
description: 'Project tracking and issue management',
|
|
209
|
+
tags: ['category:project-management'],
|
|
210
|
+
screenshotIds: ['screenshot-taskflow-1', 'screenshot-taskflow-2'],
|
|
211
|
+
appUrl: 'https://taskflow.example.com',
|
|
212
|
+
accessRequest: {
|
|
213
|
+
approvalMethodSlug: method.slug,
|
|
214
|
+
comments: 'Submit a ticket to IT',
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
postConfigure?.(ctx)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Magazine entries
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
export const magazine = {
|
|
227
|
+
/** No apps, no tags, no approval methods */
|
|
228
|
+
empty: (() => {}) satisfies Magazine,
|
|
229
|
+
|
|
230
|
+
/** 1 app (TaskFlow), 1 tag, 1 approval method — accepts optional post-configurer */
|
|
231
|
+
single: (postConfigure?: Magazine) => singleMagazine(postConfigure),
|
|
232
|
+
|
|
233
|
+
/** Full catalog, first-time user — accepts optional post-configurer */
|
|
234
|
+
full: (postConfigure?: Magazine) => fullMagazine({}, postConfigure),
|
|
235
|
+
|
|
236
|
+
/** Full catalog, returning user (cache + onboarding dismissed) — accepts optional post-configurer */
|
|
237
|
+
fullReturningUser: (postConfigure?: Magazine) =>
|
|
238
|
+
fullMagazine(
|
|
239
|
+
{ prepopulateCache: true, dismissOnboarding: true },
|
|
240
|
+
postConfigure,
|
|
241
|
+
),
|
|
242
|
+
|
|
243
|
+
/** Inline custom configurer — pass-through for ad-hoc tests */
|
|
244
|
+
custom: (fn: Magazine): Magazine => fn,
|
|
245
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RequestHandler } from 'msw'
|
|
2
|
+
|
|
3
|
+
export interface NetworkInterceptor {
|
|
4
|
+
scopeKey: string[]
|
|
5
|
+
handler: RequestHandler
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class NetworkCatalog {
|
|
9
|
+
private interceptors: NetworkInterceptor[] = []
|
|
10
|
+
|
|
11
|
+
add(interceptor: NetworkInterceptor): void {
|
|
12
|
+
this.interceptors.push(interceptor)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
replace(scopeKey: string[], handler: RequestHandler): void {
|
|
16
|
+
// Remove existing interceptor with matching key, add new one
|
|
17
|
+
this.interceptors = this.interceptors.filter(
|
|
18
|
+
(i) => !this.keysMatch(i.scopeKey, scopeKey),
|
|
19
|
+
)
|
|
20
|
+
this.interceptors.push({ scopeKey, handler })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getHandlers(): RequestHandler[] {
|
|
24
|
+
return this.interceptors.map((i) => i.handler)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private keysMatch(existing: string[], search: string[]): boolean {
|
|
28
|
+
return search.every((k) => existing.includes(k))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NetworkCatalog } from './NetworkCatalog'
|
|
2
|
+
import type { MockService } from '../mock-backend/MockService'
|
|
3
|
+
|
|
4
|
+
export type NetworkOverrideFn = (
|
|
5
|
+
catalog: NetworkCatalog,
|
|
6
|
+
service: MockService,
|
|
7
|
+
) => void
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Data-carrier that collects network override callbacks.
|
|
11
|
+
* Does not execute anything — `given()` applies overrides after building the default catalog.
|
|
12
|
+
*/
|
|
13
|
+
export class NetworkConfigurerCfg {
|
|
14
|
+
private _overrides: NetworkOverrideFn[] = []
|
|
15
|
+
|
|
16
|
+
/** Register a callback to modify the network catalog after default handlers are set up */
|
|
17
|
+
overrideConfig(fn: NetworkOverrideFn): void {
|
|
18
|
+
this._overrides.push(fn)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get overrides(): NetworkOverrideFn[] {
|
|
22
|
+
return this._overrides
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw'
|
|
2
|
+
import { createTRPCMsw, httpLink } from 'msw-trpc'
|
|
3
|
+
import type { TRPCRouter } from '@igstack/app-catalog-backend-core'
|
|
4
|
+
import type { MockService } from '../mock-backend/MockService'
|
|
5
|
+
import type { NetworkInterceptor } from './NetworkCatalog'
|
|
6
|
+
|
|
7
|
+
const trpcMsw = createTRPCMsw<TRPCRouter>({
|
|
8
|
+
links: [httpLink({ url: '/api/trpc' })],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// 1x1 transparent PNG placeholder
|
|
12
|
+
const PLACEHOLDER_PNG = new Uint8Array([
|
|
13
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
|
|
14
|
+
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
|
|
15
|
+
0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44,
|
|
16
|
+
0x41, 0x54, 0x78, 0x9c, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27,
|
|
17
|
+
0xde, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
|
|
18
|
+
0x82,
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export const SharedNetwork = {
|
|
22
|
+
appCatalogQuery(service: MockService): NetworkInterceptor {
|
|
23
|
+
return {
|
|
24
|
+
scopeKey: ['appCatalog-query'],
|
|
25
|
+
handler: trpcMsw.appCatalog.getData.query(() => {
|
|
26
|
+
return service.getAppCatalogData()
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
authGetSession(service: MockService): NetworkInterceptor {
|
|
32
|
+
return {
|
|
33
|
+
scopeKey: ['auth-session'],
|
|
34
|
+
handler: http.get(/\/api\/auth\/session/, () => {
|
|
35
|
+
const session = service.getSessionResponse()
|
|
36
|
+
if (!session) {
|
|
37
|
+
return HttpResponse.json(
|
|
38
|
+
{ error: 'Not authenticated' },
|
|
39
|
+
{ status: 401 },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return HttpResponse.json(session)
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
authGetProviders(overrides?: {
|
|
48
|
+
devLoginEnabled?: boolean
|
|
49
|
+
}): NetworkInterceptor {
|
|
50
|
+
return {
|
|
51
|
+
scopeKey: ['auth-providers'],
|
|
52
|
+
handler: trpcMsw.auth.getProviders.query(() => {
|
|
53
|
+
return {
|
|
54
|
+
providers: [] as string[],
|
|
55
|
+
devLoginEnabled: overrides?.devLoginEnabled ?? false,
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
authSignOut(): NetworkInterceptor {
|
|
62
|
+
return {
|
|
63
|
+
scopeKey: ['auth-sign-out'],
|
|
64
|
+
handler: http.post(/\/api\/auth\/sign-out/, () => {
|
|
65
|
+
return HttpResponse.json({ ok: true })
|
|
66
|
+
}),
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
authDevLogout(): NetworkInterceptor {
|
|
71
|
+
return {
|
|
72
|
+
scopeKey: ['auth-dev-logout'],
|
|
73
|
+
handler: http.post(/\/api\/auth\/dev-logout/, () => {
|
|
74
|
+
return HttpResponse.json({ ok: true })
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
screenshotBinary(): NetworkInterceptor {
|
|
80
|
+
return {
|
|
81
|
+
scopeKey: ['screenshot'],
|
|
82
|
+
handler: http.get(/\/api\/screenshots\/.*/, () => {
|
|
83
|
+
return new HttpResponse(PLACEHOLDER_PNG, {
|
|
84
|
+
headers: { 'Content-Type': 'image/png' },
|
|
85
|
+
})
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
iconBinary(): NetworkInterceptor {
|
|
91
|
+
return {
|
|
92
|
+
scopeKey: ['icon'],
|
|
93
|
+
handler: http.get(/\/api\/icons\/.*/, () => {
|
|
94
|
+
return new HttpResponse(PLACEHOLDER_PNG, {
|
|
95
|
+
headers: { 'Content-Type': 'image/png' },
|
|
96
|
+
})
|
|
97
|
+
}),
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw'
|
|
2
|
+
import type { RequestHandler } from 'msw'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Simulates a network-level connection reset error.
|
|
6
|
+
* This is what happens when the backend is completely unreachable.
|
|
7
|
+
*/
|
|
8
|
+
export function makeConnectionResetError(): RequestHandler {
|
|
9
|
+
return http.get(/\/api\/trpc\/appCatalog\.getData/, () => {
|
|
10
|
+
return HttpResponse.error()
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Simulates a reverse proxy returning HTML instead of JSON
|
|
16
|
+
* (e.g., nginx 502 Bad Gateway page).
|
|
17
|
+
*/
|
|
18
|
+
export function makeHtmlResponseError(): RequestHandler {
|
|
19
|
+
return http.get(/\/api\/trpc\/appCatalog\.getData/, () => {
|
|
20
|
+
return new HttpResponse(
|
|
21
|
+
'<html><head><title>502 Bad Gateway</title></head><body><h1>502 Bad Gateway</h1><p>The server is temporarily unable to service your request.</p></body></html>',
|
|
22
|
+
{
|
|
23
|
+
status: 502,
|
|
24
|
+
headers: { 'Content-Type': 'text/html' },
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MockService } from '../mock-backend/MockService'
|
|
2
|
+
import { NetworkCatalog } from './NetworkCatalog'
|
|
3
|
+
import { SharedNetwork } from './SharedNetwork'
|
|
4
|
+
|
|
5
|
+
export function makeNetworkReplyWithCatalog(
|
|
6
|
+
service: MockService,
|
|
7
|
+
): NetworkCatalog {
|
|
8
|
+
const catalog = new NetworkCatalog()
|
|
9
|
+
|
|
10
|
+
catalog.add(SharedNetwork.appCatalogQuery(service))
|
|
11
|
+
catalog.add(SharedNetwork.authGetSession(service))
|
|
12
|
+
catalog.add(SharedNetwork.authGetProviders())
|
|
13
|
+
catalog.add(SharedNetwork.authSignOut())
|
|
14
|
+
catalog.add(SharedNetwork.authDevLogout())
|
|
15
|
+
catalog.add(SharedNetwork.screenshotBinary())
|
|
16
|
+
catalog.add(SharedNetwork.iconBinary())
|
|
17
|
+
|
|
18
|
+
return catalog
|
|
19
|
+
}
|