@skyhook-io/k8s-ui 1.16.1 → 1.16.3
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 +22 -0
- package/package.json +3 -3
- package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
- package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
- package/src/components/cnpg/index.ts +14 -0
- package/src/components/cnpg/primitives.tsx +134 -0
- package/src/components/cnpg/relations.test.ts +304 -0
- package/src/components/cnpg/relations.ts +417 -0
- package/src/components/cnpg/workspace.test.ts +285 -0
- package/src/components/cnpg/workspace.ts +700 -0
- package/src/components/dock/BottomDock.tsx +8 -1
- package/src/components/dock/DockContext.tsx +8 -0
- package/src/components/dock/LocalTerminalTab.test.tsx +189 -0
- package/src/components/dock/LocalTerminalTab.tsx +56 -18
- package/src/components/dock/TerminalTab.tsx +2 -4
- package/src/components/issues/IssuesView.tsx +10 -1
- package/src/components/issues/types.ts +24 -0
- package/src/components/logs/LogCore.theme.test.tsx +77 -0
- package/src/components/logs/LogCore.tsx +23 -2
- package/src/components/logs/LogsViewer.tsx +5 -1
- package/src/components/logs/WorkloadLogsViewer.tsx +16 -4
- package/src/components/logs/useLogBuffer.test.ts +17 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
- package/src/components/resources/ResourcesSidebar.tsx +183 -11
- package/src/components/resources/ResourcesView.tsx +5 -1
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.ts +3 -3
- package/src/components/resources/status-overclaim.test.ts +1 -1
- package/src/components/shared/EditableYamlView.test.tsx +268 -0
- package/src/components/shared/EditableYamlView.tsx +25 -1
- package/src/components/timeline/TimelineList.tsx +185 -23
- package/src/components/timeline/TimelineListFold.test.tsx +56 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-filters.ts +7 -0
- package/src/components/ui/SelectMenu.test.tsx +25 -0
- package/src/components/ui/SelectMenu.tsx +11 -9
- package/src/components/ui/YamlReview.test.tsx +40 -0
- package/src/components/ui/YamlReview.tsx +22 -1
- package/src/components/workload/WorkloadView.tsx +96 -5
- package/src/components/workload/index.ts +1 -0
- package/src/components/workload/workload-logs-availability.test.ts +6 -0
- package/src/index.ts +3 -0
- package/src/utils/log-level.ts +14 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
appliedFact,
|
|
4
|
+
backupDestination,
|
|
5
|
+
backupsForScheduledBackup,
|
|
6
|
+
clustersUsingCatalog,
|
|
7
|
+
databaseForDeclaration,
|
|
8
|
+
gitopsSourceOf,
|
|
9
|
+
inferredObjectStoreHealth,
|
|
10
|
+
isBackupFromSchedule,
|
|
11
|
+
issuesForObject,
|
|
12
|
+
missingManagedRole,
|
|
13
|
+
objectStoreForBackup,
|
|
14
|
+
relationUnavailable,
|
|
15
|
+
replicationForDatabase,
|
|
16
|
+
scheduledBackupOf,
|
|
17
|
+
usersOfObjectStore,
|
|
18
|
+
} from './relations'
|
|
19
|
+
import { CNPG_WORKSPACE_KEYS, type CNPGWorkspaceIssue, type CNPGWorkspaceResponse } from './workspace'
|
|
20
|
+
|
|
21
|
+
const PG = 'postgresql.cnpg.io/v1'
|
|
22
|
+
const BARMAN = 'barmancloud.cnpg.io/v1'
|
|
23
|
+
const VELERO = 'velero.io/v1'
|
|
24
|
+
const PLUGIN = 'barman-cloud.cloudnative-pg.io'
|
|
25
|
+
|
|
26
|
+
function cluster(name: string, ns = 'pg', spec: any = {}, status: any = {}): any {
|
|
27
|
+
return { apiVersion: PG, kind: 'Cluster', metadata: { name, namespace: ns }, spec, status }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pluginCluster(name: string, store: string, serverName?: string, conditions?: any[]): any {
|
|
31
|
+
return cluster(
|
|
32
|
+
name,
|
|
33
|
+
'pg',
|
|
34
|
+
{ plugins: [{ name: PLUGIN, isWALArchiver: true, parameters: { barmanObjectName: store, ...(serverName ? { serverName } : {}) } }] },
|
|
35
|
+
conditions ? { conditions } : {},
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function backup(name: string, extra: any = {}): any {
|
|
40
|
+
return {
|
|
41
|
+
apiVersion: PG,
|
|
42
|
+
kind: 'Backup',
|
|
43
|
+
metadata: { name, namespace: 'pg', ...(extra.metadata ?? {}) },
|
|
44
|
+
spec: { cluster: { name: 'main' }, ...(extra.spec ?? {}) },
|
|
45
|
+
status: extra.status ?? {},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('scheduledBackupOf / backupsForScheduledBackup', () => {
|
|
50
|
+
const sched = { apiVersion: PG, kind: 'ScheduledBackup', metadata: { name: 'nightly', namespace: 'pg', uid: 'uid-new' } }
|
|
51
|
+
const ownedBy = (name: string, uid: string, extra: any = {}) =>
|
|
52
|
+
backup(name, {
|
|
53
|
+
...extra,
|
|
54
|
+
metadata: { ...(extra.metadata ?? {}), ownerReferences: [{ apiVersion: PG, kind: 'ScheduledBackup', name: 'nightly', uid }] },
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('reads the owner reference name', () => {
|
|
58
|
+
expect(scheduledBackupOf(ownedBy('b1', 'uid-new'))).toBe('nightly')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('falls back to the operator label when the owner is the Cluster or unset', () => {
|
|
62
|
+
const b = backup('b1', {
|
|
63
|
+
metadata: {
|
|
64
|
+
labels: { 'cnpg.io/scheduled-backup': 'nightly' },
|
|
65
|
+
ownerReferences: [{ apiVersion: PG, kind: 'Cluster', name: 'main' }],
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
expect(scheduledBackupOf(b)).toBe('nightly')
|
|
69
|
+
expect(isBackupFromSchedule(b, sched)).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns null for an on-demand backup', () => {
|
|
73
|
+
expect(scheduledBackupOf(backup('b1'))).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('ignores an owner reference from another API group', () => {
|
|
77
|
+
const b = backup('b1', { metadata: { ownerReferences: [{ apiVersion: 'example.com/v1', kind: 'ScheduledBackup', name: 'nightly' }] } })
|
|
78
|
+
expect(scheduledBackupOf(b)).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does not attribute a recreated schedule the old schedule\'s Backups', () => {
|
|
82
|
+
const old = ownedBy('old', 'uid-old', { metadata: { labels: { 'cnpg.io/scheduled-backup': 'nightly' } } })
|
|
83
|
+
expect(isBackupFromSchedule(old, sched)).toBe(false)
|
|
84
|
+
expect(isBackupFromSchedule(ownedBy('cur', 'uid-new'), sched)).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('lists owned backups newest first and never a Velero Backup', () => {
|
|
88
|
+
const owned = (name: string, startedAt: string, apiVersion = PG) => ({
|
|
89
|
+
...backup(name, { metadata: { labels: { 'cnpg.io/scheduled-backup': 'nightly' } }, status: { startedAt } }),
|
|
90
|
+
apiVersion,
|
|
91
|
+
})
|
|
92
|
+
const list = [
|
|
93
|
+
owned('old', '2026-09-01T00:00:00Z'),
|
|
94
|
+
owned('new', '2026-09-02T00:00:00Z'),
|
|
95
|
+
owned('velero', '2026-09-03T00:00:00Z', VELERO),
|
|
96
|
+
ownedBy('stale', 'uid-old', { status: { startedAt: '2026-09-04T00:00:00Z' } }),
|
|
97
|
+
backup('other'),
|
|
98
|
+
{ ...owned('elsewhere', '2026-09-03T00:00:00Z'), metadata: { name: 'elsewhere', namespace: 'x', labels: { 'cnpg.io/scheduled-backup': 'nightly' } } },
|
|
99
|
+
]
|
|
100
|
+
expect(backupsForScheduledBackup(sched, list).map((b) => b.metadata.name)).toEqual(['new', 'old'])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('objectStoreForBackup / backupDestination', () => {
|
|
105
|
+
const clusters = [pluginCluster('main', 'store-a')]
|
|
106
|
+
|
|
107
|
+
it('prefers the store the backup recorded', () => {
|
|
108
|
+
const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN, parameters: { barmanObjectName: 'store-b' } } } })
|
|
109
|
+
expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-b', inferred: false })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("marks a store taken from the Cluster's current plugin as inferred", () => {
|
|
113
|
+
const b = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: PLUGIN } } })
|
|
114
|
+
expect(objectStoreForBackup(b, clusters)).toEqual({ name: 'store-a', inferred: true })
|
|
115
|
+
expect(backupDestination(b, clusters)).toEqual({ type: 'objectStore', name: 'store-a', inferred: true })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('checks plugin identity before reading barmanObjectName', () => {
|
|
119
|
+
const other = backup('b', { spec: { method: 'plugin', pluginConfiguration: { name: 'other.example.com', parameters: { barmanObjectName: 'store-b' } } } })
|
|
120
|
+
expect(objectStoreForBackup(other, clusters)).toBeNull()
|
|
121
|
+
const unnamed = backup('b', { spec: { method: 'plugin', pluginConfiguration: { parameters: { barmanObjectName: 'store-b' } } } })
|
|
122
|
+
expect(objectStoreForBackup(unnamed, clusters)).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('reports in-tree paths and volume snapshots', () => {
|
|
126
|
+
expect(backupDestination(backup('b', { status: { method: 'barmanObjectStore', destinationPath: 's3://x' } }), clusters)).toEqual({ type: 'path', path: 's3://x' })
|
|
127
|
+
expect(backupDestination(backup('b', { spec: { method: 'volumeSnapshot' } }), clusters)).toEqual({ type: 'volumeSnapshot' })
|
|
128
|
+
expect(backupDestination(backup('b'), clusters)).toEqual({ type: 'unknown' })
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('ObjectStore users and inferred health', () => {
|
|
133
|
+
const store = {
|
|
134
|
+
apiVersion: BARMAN,
|
|
135
|
+
kind: 'ObjectStore',
|
|
136
|
+
metadata: { name: 'store', namespace: 'pg' },
|
|
137
|
+
status: {
|
|
138
|
+
serverRecoveryWindow: {
|
|
139
|
+
'srv-a': { firstRecoverabilityPoint: '2026-09-01T00:00:00Z', lastSuccessfulBackupTime: '2026-09-02T00:00:00Z' },
|
|
140
|
+
b: { lastSuccessfulBackupTime: '2026-09-02T00:00:00Z', lastFailedBackupTime: '2026-09-03T00:00:00Z' },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
const ok = [{ type: 'ContinuousArchiving', status: 'True' }]
|
|
145
|
+
|
|
146
|
+
it('finds users in the same namespace, keyed by serverName', () => {
|
|
147
|
+
const clusters = [
|
|
148
|
+
pluginCluster('a', 'store', 'srv-a', ok),
|
|
149
|
+
pluginCluster('b', 'store'),
|
|
150
|
+
pluginCluster('c', 'other'),
|
|
151
|
+
{ ...pluginCluster('d', 'store'), metadata: { name: 'd', namespace: 'elsewhere' } },
|
|
152
|
+
{ ...pluginCluster('e', 'store'), apiVersion: 'cluster.x-k8s.io/v1beta1' },
|
|
153
|
+
]
|
|
154
|
+
const users = usersOfObjectStore(store, clusters)
|
|
155
|
+
expect(users.map((u) => [u.cluster.metadata.name, u.serverName])).toEqual([
|
|
156
|
+
['a', 'srv-a'],
|
|
157
|
+
['b', 'b'],
|
|
158
|
+
])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('reports failing when any user has a failure newer than its last success', () => {
|
|
162
|
+
const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', ok), pluginCluster('b', 'store', undefined, ok)])
|
|
163
|
+
const h = inferredObjectStoreHealth(store, users)
|
|
164
|
+
expect(h.summary.tone).toBe('unhealthy')
|
|
165
|
+
expect(h.summary.text).toBe('Uploads failing for 1 of 2 clusters')
|
|
166
|
+
expect(h.summary.at).toBe('2026-09-03T00:00:00Z')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('reports failing from a False ContinuousArchiving condition', () => {
|
|
170
|
+
const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', [{ type: 'ContinuousArchiving', status: 'False', message: 'boom' }])])
|
|
171
|
+
expect(inferredObjectStoreHealth(store, users).summary).toMatchObject({ text: 'Uploads failing', tone: 'unhealthy' })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('is healthy only when every user reports archiving', () => {
|
|
175
|
+
const users = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a', ok)])
|
|
176
|
+
expect(inferredObjectStoreHealth(store, users).summary.tone).toBe('healthy')
|
|
177
|
+
const unreported = usersOfObjectStore(store, [pluginCluster('a', 'store', 'srv-a')])
|
|
178
|
+
expect(inferredObjectStoreHealth(store, unreported).summary).toMatchObject({ text: 'No failures reported', tone: 'unknown' })
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('says so when nothing uses the store', () => {
|
|
182
|
+
expect(inferredObjectStoreHealth(store, []).summary).toMatchObject({ text: 'No visible cluster uses this store', tone: 'unknown' })
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('clustersUsingCatalog', () => {
|
|
187
|
+
const catalog = { apiVersion: PG, kind: 'ImageCatalog', metadata: { name: 'pg', namespace: 'pg' } }
|
|
188
|
+
const clusterCatalog = { apiVersion: PG, kind: 'ClusterImageCatalog', metadata: { name: 'pg' } }
|
|
189
|
+
|
|
190
|
+
it('treats an omitted ref kind as ImageCatalog and stays in the namespace', () => {
|
|
191
|
+
const clusters = [
|
|
192
|
+
cluster('a', 'pg', { imageCatalogRef: { name: 'pg', major: 16 } }),
|
|
193
|
+
cluster('b', 'pg', { imageCatalogRef: { kind: 'ImageCatalog', name: 'pg', major: 17 } }),
|
|
194
|
+
cluster('c', 'other', { imageCatalogRef: { name: 'pg', major: 16 } }),
|
|
195
|
+
cluster('d', 'pg', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg', major: 16 } }),
|
|
196
|
+
]
|
|
197
|
+
expect(clustersUsingCatalog(catalog, clusters).map((u) => [u.cluster.metadata.name, u.major])).toEqual([
|
|
198
|
+
['a', 16],
|
|
199
|
+
['b', 17],
|
|
200
|
+
])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('matches ClusterImageCatalog refs from any namespace, never an omitted kind', () => {
|
|
204
|
+
const clusters = [
|
|
205
|
+
cluster('a', 'x', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg', major: 16 } }),
|
|
206
|
+
cluster('b', 'y', { imageCatalogRef: { kind: 'ClusterImageCatalog', name: 'pg' } }),
|
|
207
|
+
cluster('c', 'pg', { imageCatalogRef: { name: 'pg', major: 16 } }),
|
|
208
|
+
]
|
|
209
|
+
expect(clustersUsingCatalog(clusterCatalog, clusters).map((u) => [u.cluster.metadata.name, u.major])).toEqual([
|
|
210
|
+
['a', 16],
|
|
211
|
+
['b', null],
|
|
212
|
+
])
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('issuesForObject', () => {
|
|
217
|
+
const issue = (over: Partial<CNPGWorkspaceIssue>): CNPGWorkspaceIssue => ({
|
|
218
|
+
id: Math.random().toString(),
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
kind: 'Backup',
|
|
221
|
+
group: 'postgresql.cnpg.io',
|
|
222
|
+
namespace: 'pg',
|
|
223
|
+
name: 'b1',
|
|
224
|
+
reason: 'CNPGBackupFailed',
|
|
225
|
+
...over,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('matches kind, group, namespace and name, critical first', () => {
|
|
229
|
+
const issues = [
|
|
230
|
+
issue({ id: 'w' }),
|
|
231
|
+
issue({ id: 'c', severity: 'critical' }),
|
|
232
|
+
issue({ id: 'velero', group: 'velero.io' }),
|
|
233
|
+
issue({ id: 'ns', namespace: 'other' }),
|
|
234
|
+
issue({ id: 'name', name: 'b2' }),
|
|
235
|
+
issue({ id: 'kind', kind: 'ScheduledBackup' }),
|
|
236
|
+
]
|
|
237
|
+
const got = issuesForObject(issues, { kind: 'Backup', group: 'postgresql.cnpg.io', namespace: 'pg', name: 'b1' })
|
|
238
|
+
expect(got.map((i) => i.id)).toEqual(['c', 'w'])
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('declarations', () => {
|
|
243
|
+
it('distinguishes pending from failed', () => {
|
|
244
|
+
expect(appliedFact({ status: {} }).tone).toBe('unknown')
|
|
245
|
+
expect(appliedFact({ status: { applied: false } }).text).toBe('Not applied')
|
|
246
|
+
expect(appliedFact({ status: { applied: true } }).text).toBe('Applied')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('names a missing role only when it is absent from managed roles', () => {
|
|
250
|
+
const db = { status: { applied: false, message: 'role "app" does not exist' } }
|
|
251
|
+
expect(missingManagedRole(db, cluster('main', 'pg', { managed: { roles: [{ name: 'other' }] } }))).toBe('app')
|
|
252
|
+
expect(missingManagedRole(db, cluster('main', 'pg', { managed: { roles: [{ name: 'app' }] } }))).toBeNull()
|
|
253
|
+
expect(missingManagedRole(db, null)).toBeNull()
|
|
254
|
+
expect(missingManagedRole({ status: { message: 'connection refused' } }, cluster('main'))).toBeNull()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('reads the GitOps owner labels', () => {
|
|
258
|
+
expect(gitopsSourceOf({ metadata: { labels: { 'argocd.argoproj.io/instance': 'app' } } })).toEqual({ tool: 'argocd', name: 'app' })
|
|
259
|
+
expect(gitopsSourceOf({ metadata: { labels: { 'kustomize.toolkit.fluxcd.io/name': 'k', 'kustomize.toolkit.fluxcd.io/namespace': 'flux' } } })).toEqual({
|
|
260
|
+
tool: 'flux',
|
|
261
|
+
name: 'k',
|
|
262
|
+
namespace: 'flux',
|
|
263
|
+
})
|
|
264
|
+
expect(gitopsSourceOf({ metadata: {} })).toBeNull()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const decl = (kind: string, name: string, clusterName: string, dbname: string, ns = 'pg') => ({
|
|
268
|
+
apiVersion: PG,
|
|
269
|
+
kind,
|
|
270
|
+
metadata: { name, namespace: ns },
|
|
271
|
+
spec: { cluster: { name: clusterName }, dbname },
|
|
272
|
+
})
|
|
273
|
+
const database = { apiVersion: PG, kind: 'Database', metadata: { name: 'app-db', namespace: 'pg' }, spec: { cluster: { name: 'main' }, name: 'app' } }
|
|
274
|
+
|
|
275
|
+
it('finds publications and subscriptions on the same cluster and database', () => {
|
|
276
|
+
const pubs = [decl('Publication', 'p1', 'main', 'app'), decl('Publication', 'p2', 'main', 'other'), decl('Publication', 'p3', 'other', 'app')]
|
|
277
|
+
const subs = [decl('Subscription', 's1', 'main', 'app'), decl('Subscription', 's2', 'main', 'app', 'x')]
|
|
278
|
+
const r = replicationForDatabase(database, pubs, subs)
|
|
279
|
+
expect(r.publications.map((p) => p.metadata.name)).toEqual(['p1'])
|
|
280
|
+
expect(r.subscriptions.map((s) => s.metadata.name)).toEqual(['s1'])
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('finds the Database declaring a publication database', () => {
|
|
284
|
+
expect(databaseForDeclaration(decl('Publication', 'p1', 'main', 'app'), [database])?.metadata.name).toBe('app-db')
|
|
285
|
+
expect(databaseForDeclaration(decl('Publication', 'p1', 'other', 'app'), [database])).toBeNull()
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('relationUnavailable', () => {
|
|
290
|
+
const ws = (state: any, deniedNamespaces?: string[]): CNPGWorkspaceResponse => {
|
|
291
|
+
const coverage: CNPGWorkspaceResponse['coverage'] = {}
|
|
292
|
+
for (const k of CNPG_WORKSPACE_KEYS) coverage[k] = { state: 'full' }
|
|
293
|
+
coverage.backups = { state, deniedNamespaces }
|
|
294
|
+
return { installed: true, context: 'c', namespaces: null, coverage, objects: {}, issues: [], audit: [], backupsOmitted: 0 }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
it('answers from coverage', () => {
|
|
298
|
+
expect(relationUnavailable(ws('full'), 'backups', 'pg', 'Backups')).toBeNull()
|
|
299
|
+
expect(relationUnavailable(ws('partial', ['pg']), 'backups', 'pg', 'Backups')).toBe('No access to Backups')
|
|
300
|
+
expect(relationUnavailable(ws('partial', ['other']), 'backups', 'pg', 'Backups')).toBeNull()
|
|
301
|
+
expect(relationUnavailable(ws('denied'), 'backups', 'pg', 'Backups')).toBe('No access to Backups')
|
|
302
|
+
expect(relationUnavailable(null, 'backups', 'pg', 'Backups')).toBe('Backups could not be read')
|
|
303
|
+
})
|
|
304
|
+
})
|