Skip to content
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ kubeconfig*

# husky managed hooks
.husky/user-config

# Claude Code local settings
.claude/settings.local.json
2 changes: 2 additions & 0 deletions backend/__tests__/acceptance/api.members.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
beforeEach,
} from 'vitest'
import request from '@gardener-dashboard/request'
import { seedProjectNamespaceIndex } from '../helpers/cache.js'

const { mockRequest } = request

Expand All @@ -21,6 +22,7 @@ describe('api', function () {

beforeAll(async () => {
agent = await createAgent()
seedProjectNamespaceIndex()
})

afterAll(() => {
Expand Down
163 changes: 163 additions & 0 deletions backend/__tests__/acceptance/api.seedstats.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

import {
describe,
it,
expect,
beforeAll,
afterAll,
beforeEach,
} from 'vitest'
import request from '@gardener-dashboard/request'
import { Store } from '@gardener-dashboard/kube-client'
import cache from '../../lib/cache/index.js'

const { mockRequest } = request

function createStore (items) {
const store = new Store()
store.replace(items)
return store
}

function seedShootsBySeedNameIndex (shoots = fixtures.shoots.list()) {
const handlers = new Map()
cache.indexShootsBySeedName({
on (event, handler) {
handlers.set(event, handler)
},
})
const add = handlers.get('add')
for (const shoot of shoots) {
add(shoot)
}
}

describe('api', function () {
let agent

beforeAll(async () => {
agent = await createAgent()

const shoots = fixtures.shoots.list()

cache.initialize({
seeds: {
store: createStore(fixtures.seeds.list()),
},
shoots: {
store: createStore(shoots),
},
})
seedShootsBySeedNameIndex(shoots)
})

afterAll(() => {
return agent.close()
})

beforeEach(() => {
mockRequest.mockReset()
})

describe('seedstats', function () {
const user = fixtures.auth.createUser({ id: 'john.doe@example.org' })

it('should return stats for all seeds', async function () {
mockRequest
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())

const res = await agent
.get('/api/seedstats?unhealthyFilterMask=0')
.set('cookie', await user.cookie)
.expect('content-type', /json/)
.expect(200)

expect(mockRequest).toHaveBeenCalledTimes(2)
expect(res.body).toEqual(expect.arrayContaining([
expect.objectContaining({
apiVersion: 'dashboard.gardener.cloud/v1alpha1',
kind: 'SeedStat',
metadata: expect.objectContaining({ name: 'infra1-seed' }),
counts: {
shootCount: 3,
unhealthyShoots: {
total: 0,
matching: 0,
},
},
}),
]))
})

it('should return stats for one seed', async function () {
mockRequest
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())

const res = await agent
.get('/api/seedstats/infra1-seed?unhealthyFilterMask=0')
.set('cookie', await user.cookie)
.expect('content-type', /json/)
.expect(200)

expect(mockRequest).toHaveBeenCalledTimes(2)
expect(res.body).toEqual(expect.objectContaining({
metadata: {
name: 'infra1-seed',
uid: 'seed--infra1-seed',
},
counts: {
shootCount: 3,
unhealthyShoots: {
total: 0,
matching: 0,
},
},
}))
})

it('should forbid listing seed stats without full access', async function () {
mockRequest
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess({ allowed: false }))

const res = await agent
.get('/api/seedstats?unhealthyFilterMask=0')
.set('cookie', await user.cookie)
.expect('content-type', /json/)
.expect(403)

expect(mockRequest).toHaveBeenCalledTimes(2)
expect(res.body).toEqual(expect.objectContaining({
code: 403,
reason: 'Forbidden',
message: 'You are not allowed to list seed stats',
}))
})

it('should reject requests without unhealthyFilterMask', async function () {
mockRequest
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())

const res = await agent
.get('/api/seedstats')
.set('cookie', await user.cookie)
.expect('content-type', /json/)
.expect(422)

expect(mockRequest).toHaveBeenCalledTimes(2)
expect(res.body).toEqual(expect.objectContaining({
code: 422,
reason: 'Unprocessable Entity',
message: "The 'unhealthyFilterMask' query parameter must be a non-negative integer with no bits set outside the known flags (0–7)",
}))
})
})
})
2 changes: 2 additions & 0 deletions backend/__tests__/acceptance/api.terminals.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'vitest'
import { padStart } from 'lodash-es'
import request from '@gardener-dashboard/request'
import { seedProjectNamespaceIndex } from '../helpers/cache.js'
import { converter } from '../../lib/services/terminals/index.js'

const { mockRequest } = request
Expand All @@ -32,6 +33,7 @@ describe('api', function () {

beforeAll(async () => {
agent = await createAgent()
seedProjectNamespaceIndex()
})

afterAll(() => {
Expand Down
2 changes: 2 additions & 0 deletions backend/__tests__/acceptance/api.tickets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mockListIssues,
mockListComments,
} from '@octokit/core'
import { seedProjectNamespaceIndex } from '../helpers/cache.js'
import * as tickets from '../../lib/services/tickets.js'

const { mockRequest } = request
Expand All @@ -28,6 +29,7 @@ describe('api', function () {

beforeAll(async () => {
agent = await createAgent()
seedProjectNamespaceIndex()
})

afterAll(() => {
Expand Down
5 changes: 5 additions & 0 deletions backend/__tests__/acceptance/io.cors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('cors', () => {
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
await expect(createAgent('io', cache))
.rejects.toThrow('WebSocket allowed origins configuration is required')
})
Expand All @@ -54,6 +55,7 @@ describe('cors', () => {
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
agent = await createAgent('io', cache)
const cookie = await user.cookie
await expect(
Expand All @@ -68,6 +70,7 @@ describe('cors', () => {
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
agent = await createAgent('io', cache)
const cookie = await user.cookie
await expect(
Expand All @@ -82,6 +85,7 @@ describe('cors', () => {
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
agent = await createAgent('io', cache)
const cookie = await user.cookie
socket = await agent.connect({ cookie, originHeader: 'https://allowed.example.org' })
Expand All @@ -95,6 +99,7 @@ describe('cors', () => {
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
.mockImplementationOnce(fixtures.auth.mocks.reviewSelfSubjectAccess())
agent = await createAgent('io', cache)
const cookie = await user.cookie
socket = await agent.connect({ cookie, originHeader: 'https://any.example.org' })
Expand Down
Loading
Loading