mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-06 02:48:28 +00:00
fix(namespace): preserve my namespace compatibility
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
123d0d5f53
commit
daba212271
15 changed files with 295 additions and 39 deletions
|
|
@ -131,12 +131,18 @@ import sys
|
|||
slug = sys.argv[1]
|
||||
data = json.loads(os.environ["JSON_INPUT"])
|
||||
items = data["data"]
|
||||
raise SystemExit(0 if all(item["slug"] != slug for item in items) else 1)
|
||||
match = next((item for item in items if item["slug"] == slug), None)
|
||||
if not match:
|
||||
raise SystemExit(1)
|
||||
if match.get("currentUserRole") is not None:
|
||||
raise SystemExit(2)
|
||||
if match["status"] != "ACTIVE":
|
||||
raise SystemExit(3)
|
||||
PY
|
||||
then
|
||||
pass "Namespace is not visible to unrelated users in my namespaces"
|
||||
pass "SUPER_ADMIN can see namespace without namespace membership"
|
||||
else
|
||||
fail "Unrelated user should not see team namespace in my namespaces"
|
||||
fail "SUPER_ADMIN should see team namespace in my namespaces without namespace role"
|
||||
fi
|
||||
|
||||
FREEZE_FORBIDDEN_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" \
|
||||
|
|
|
|||
|
|
@ -68,7 +68,16 @@ public class NamespaceController extends BaseApiController {
|
|||
}
|
||||
|
||||
@GetMapping("/me/namespaces")
|
||||
public ApiResponse<PageResponse<MyNamespaceResponse>> listMyNamespaces(
|
||||
public ApiResponse<List<MyNamespaceResponse>> listMyNamespaces(
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles) {
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.listMyNamespaces(userNsRoles, normalizePlatformRoles(platformRoles)));
|
||||
}
|
||||
|
||||
@GetMapping("/me/namespaces/page")
|
||||
public ApiResponse<PageResponse<MyNamespaceResponse>> listMyNamespacesPage(
|
||||
Pageable pageable,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.iflytek.skillhub.dto.MemberResponse;
|
|||
import com.iflytek.skillhub.dto.MyNamespaceResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -115,11 +116,20 @@ public class NamespacePortalQueryAppService {
|
|||
@Transactional(readOnly = true)
|
||||
public List<MyNamespaceResponse> listMyNamespaces(Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
return listMyNamespaces(
|
||||
PageRequest.of(0, MAX_MY_NAMESPACE_PAGE_SIZE),
|
||||
userNamespaceRoles,
|
||||
platformRoles
|
||||
).items();
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
if (namespaceRoles.isEmpty() && !isSuperAdmin(platformRoles)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Namespace> visibleNamespaces = isSuperAdmin(platformRoles)
|
||||
? listAllNamespacesByPage()
|
||||
: namespaceRepository.findByIdIn(namespaceRoles.keySet().stream().toList()).stream()
|
||||
.sorted(Comparator.comparing(Namespace::getSlug))
|
||||
.toList();
|
||||
|
||||
return visibleNamespaces.stream()
|
||||
.map(namespace -> myNamespaceResponse(namespace, namespaceRoles))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
|
@ -213,6 +223,25 @@ public class NamespacePortalQueryAppService {
|
|||
namespaceService.canDelete(namespace, currentUserRole));
|
||||
}
|
||||
|
||||
private List<Namespace> listAllNamespacesByPage() {
|
||||
List<Namespace> namespaces = new ArrayList<>();
|
||||
int pageNumber = 0;
|
||||
Page<Namespace> page;
|
||||
do {
|
||||
page = namespaceRepository.findAll(PageRequest.of(
|
||||
pageNumber,
|
||||
MAX_MY_NAMESPACE_PAGE_SIZE,
|
||||
Sort.by(NAMESPACE_SLUG_SORT).ascending()
|
||||
));
|
||||
namespaces.addAll(page.getContent());
|
||||
pageNumber++;
|
||||
if (page.getContent().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
} while (!page.isLast() && namespaces.size() < page.getTotalElements());
|
||||
return namespaces;
|
||||
}
|
||||
|
||||
private Pageable normalizeMyNamespacesPageable(Pageable pageable) {
|
||||
int page = pageable != null && pageable.isPaged()
|
||||
? Math.max(pageable.getPageNumber(), 0)
|
||||
|
|
|
|||
|
|
@ -100,9 +100,33 @@ class NamespacePortalControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminReturnsPagedNamespacesWithoutMembership() throws Exception {
|
||||
void listMyNamespaces_superAdminKeepsLegacyArrayContract() throws Exception {
|
||||
Namespace active = namespace(1L, "active", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
Namespace archived = namespace(3L, "archived", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
given(namespaceRepository.findAll(any()))
|
||||
.willReturn(new org.springframework.data.domain.PageImpl<>(
|
||||
List.of(active, archived),
|
||||
org.springframework.data.domain.PageRequest.of(0, 2),
|
||||
2
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/me/namespaces")
|
||||
.param("page", "0")
|
||||
.param("size", "2")
|
||||
.with(auth("super-1", Set.of("SUPER_ADMIN")))
|
||||
.requestAttr("userId", "super-1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data[0].slug").value("active"))
|
||||
.andExpect(jsonPath("$.data[0].currentUserRole").doesNotExist())
|
||||
.andExpect(jsonPath("$.data[1].slug").value("archived"))
|
||||
.andExpect(jsonPath("$.data.items").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespacesPage_superAdminReturnsPagedNamespacesWithoutMembership() throws Exception {
|
||||
Namespace active = namespace(1L, "active", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
Namespace frozen = namespace(2L, "frozen", NamespaceStatus.FROZEN, NamespaceType.TEAM);
|
||||
Namespace archived = namespace(3L, "archived", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
given(namespaceRepository.findAll(any()))
|
||||
|
|
@ -112,7 +136,7 @@ class NamespacePortalControllerTest {
|
|||
3
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/me/namespaces")
|
||||
mockMvc.perform(get("/api/web/me/namespaces/page")
|
||||
.param("page", "0")
|
||||
.param("size", "2")
|
||||
.with(auth("super-1", Set.of("SUPER_ADMIN")))
|
||||
|
|
@ -139,10 +163,10 @@ class NamespacePortalControllerTest {
|
|||
.requestAttr("userId", "owner-1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items[0].slug").value("team-a"))
|
||||
.andExpect(jsonPath("$.data.items[0].status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.items[0].currentUserRole").value("OWNER"))
|
||||
.andExpect(jsonPath("$.data.items[0].canDelete").value(false));
|
||||
.andExpect(jsonPath("$.data[0].slug").value("team-a"))
|
||||
.andExpect(jsonPath("$.data[0].status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data[0].currentUserRole").value("OWNER"))
|
||||
.andExpect(jsonPath("$.data[0].canDelete").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -145,6 +145,22 @@ class NamespacePortalQueryAppServiceTest {
|
|||
assertThat(response.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminCompatibilityCollectsAllRepositoryPages() {
|
||||
Namespace first = namespace(1L, "first");
|
||||
Namespace second = namespace(2L, "second");
|
||||
Namespace third = namespace(3L, "third");
|
||||
|
||||
when(namespaceRepository.findAll(any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(first, second), PageRequest.of(0, 2), 3))
|
||||
.thenReturn(new PageImpl<>(List.of(third), PageRequest.of(1, 2), 3));
|
||||
|
||||
var response = service.listMyNamespaces(Map.of(), Set.of("SUPER_ADMIN"));
|
||||
|
||||
assertThat(response).extracting("slug").containsExactly("first", "second", "third");
|
||||
assertThat(response).extracting("currentUserRole").containsOnlyNulls();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespace_throwsWhenCurrentUserIsNotNamespaceMember() {
|
||||
Namespace namespace = namespace(1L, "team-a");
|
||||
|
|
|
|||
|
|
@ -44,13 +44,6 @@ interface NamespaceCandidate {
|
|||
status: string
|
||||
}
|
||||
|
||||
interface PageResponse<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
interface ApiEnvelope<T> {
|
||||
code: number
|
||||
msg: string
|
||||
|
|
@ -256,10 +249,9 @@ export class E2eTestDataBuilder {
|
|||
}
|
||||
|
||||
async listMyNamespaces(): Promise<SeededNamespace[]> {
|
||||
const page = await parseEnvelope<PageResponse<SeededNamespace>>(
|
||||
await this.request.get('/api/web/me/namespaces?page=0&size=100'),
|
||||
return parseEnvelope<SeededNamespace[]>(
|
||||
await this.request.get('/api/web/me/namespaces'),
|
||||
)
|
||||
return page.items
|
||||
}
|
||||
|
||||
private isTeamNamespace(namespace: SeededNamespace): boolean {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ test.describe('My Namespaces super admin actions', () => {
|
|||
|
||||
await page.route('**/api/web/skills/*/star', (route) => fulfillJson(route, false))
|
||||
|
||||
await page.route('**/api/web/me/namespaces?**', (route) => fulfillJson(route, {
|
||||
await page.route('**/api/web/me/namespaces/page?**', (route) => fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
id: 101,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ test.describe('Promotion review dashboard', () => {
|
|||
}),
|
||||
})
|
||||
})
|
||||
await page.route('**/api/web/me/namespaces?**', async (route) => {
|
||||
await page.route('**/api/web/me/namespaces/page?**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
|
|
|
|||
|
|
@ -164,6 +164,46 @@ describe('namespaceApi.delete', () => {
|
|||
})
|
||||
|
||||
describe('namespaceApi.listMine', () => {
|
||||
it('keeps the compatibility endpoint as a current user namespace array', async () => {
|
||||
window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com' }
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
code: 0,
|
||||
msg: 'ok',
|
||||
data: [{
|
||||
id: 1,
|
||||
slug: 'team-a',
|
||||
displayName: 'Team A',
|
||||
type: 'TEAM',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-05-07T00:00:00Z',
|
||||
immutable: false,
|
||||
canFreeze: false,
|
||||
canUnfreeze: false,
|
||||
canArchive: false,
|
||||
canRestore: false,
|
||||
canDelete: false,
|
||||
}],
|
||||
timestamp: '2026-05-07T00:00:00Z',
|
||||
requestId: 'req-test',
|
||||
}),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const namespaces = await namespaceApi.listMine()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://api.example.com/api/web/me/namespaces',
|
||||
expect.objectContaining({
|
||||
headers: expect.any(Headers),
|
||||
}),
|
||||
)
|
||||
expect(namespaces).toEqual([expect.objectContaining({ slug: 'team-a' })])
|
||||
})
|
||||
})
|
||||
|
||||
describe('namespaceApi.listMinePage', () => {
|
||||
it('requests a bounded page of current user namespaces', async () => {
|
||||
window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com' }
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
|
|
@ -183,10 +223,10 @@ describe('namespaceApi.listMine', () => {
|
|||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const page = await namespaceApi.listMine({ page: 2, size: 25 })
|
||||
const page = await namespaceApi.listMinePage({ page: 2, size: 25 })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://api.example.com/api/web/me/namespaces?page=2&size=25',
|
||||
'https://api.example.com/api/web/me/namespaces/page?page=2&size=25',
|
||||
expect.objectContaining({
|
||||
headers: expect.any(Headers),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -639,10 +639,14 @@ export const namespaceApi = {
|
|||
return namespace
|
||||
},
|
||||
|
||||
async listMine(params: { page?: number; size?: number } = {}): Promise<PagedResponse<ManagedNamespace>> {
|
||||
async listMine(): Promise<ManagedNamespace[]> {
|
||||
return fetchJson<ManagedNamespace[]>(`${WEB_API_PREFIX}/me/namespaces`)
|
||||
},
|
||||
|
||||
async listMinePage(params: { page?: number; size?: number } = {}): Promise<PagedResponse<ManagedNamespace>> {
|
||||
const page = params.page ?? 0
|
||||
const size = params.size ?? 20
|
||||
return fetchJson<PagedResponse<ManagedNamespace>>(`${WEB_API_PREFIX}/me/namespaces?page=${page}&size=${size}`)
|
||||
return fetchJson<PagedResponse<ManagedNamespace>>(`${WEB_API_PREFIX}/me/namespaces/page?page=${page}&size=${size}`)
|
||||
},
|
||||
|
||||
async getDetail(slug: string): Promise<Namespace> {
|
||||
|
|
|
|||
85
web/src/api/generated/schema.d.ts
vendored
85
web/src/api/generated/schema.d.ts
vendored
|
|
@ -2660,6 +2660,38 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/me/namespaces/page": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listMyNamespacesPage"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/web/me/namespaces/page": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listMyNamespacesPage_1"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/me/namespaces": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -4557,6 +4589,15 @@ export interface components {
|
|||
/** Format: int32 */
|
||||
size?: number;
|
||||
};
|
||||
ApiResponseListMyNamespaceResponse: {
|
||||
/** Format: int32 */
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: components["schemas"]["MyNamespaceResponse"][];
|
||||
/** Format: date-time */
|
||||
timestamp?: string;
|
||||
requestId?: string;
|
||||
};
|
||||
ApiResponseGovernanceSummaryResponse: {
|
||||
/** Format: int32 */
|
||||
code?: number;
|
||||
|
|
@ -10008,7 +10049,7 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
listMyNamespaces: {
|
||||
listMyNamespacesPage: {
|
||||
parameters: {
|
||||
query: {
|
||||
pageable: components["schemas"]["Pageable"];
|
||||
|
|
@ -10030,7 +10071,7 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
listMyNamespaces_1: {
|
||||
listMyNamespacesPage_1: {
|
||||
parameters: {
|
||||
query: {
|
||||
pageable: components["schemas"]["Pageable"];
|
||||
|
|
@ -10052,6 +10093,46 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
listMyNamespaces: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseListMyNamespaceResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
listMyNamespaces_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ApiResponseListMyNamespaceResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
listVisibleLabels: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ vi.mock('@/shared/lib/toast', () => ({
|
|||
}))
|
||||
|
||||
import { MyNamespacesPage } from './my-namespaces'
|
||||
import { executeNamespaceAction, resolveNamespaceActionCopy } from './my-namespaces'
|
||||
import { executeNamespaceAction, resolveNamespaceActionCopy, resolveValidNamespacePage } from './my-namespaces'
|
||||
|
||||
function buildNamespace(overrides: Partial<ManagedNamespace> = {}): ManagedNamespace {
|
||||
return {
|
||||
|
|
@ -205,6 +205,12 @@ describe('MyNamespacesPage', () => {
|
|||
expect(html).toContain('pagination.next')
|
||||
})
|
||||
|
||||
it('backs up to the last valid page when a delete empties the current page', () => {
|
||||
expect(resolveValidNamespacePage(2, 40, 20)).toBe(1)
|
||||
expect(resolveValidNamespacePage(1, 40, 20)).toBe(1)
|
||||
expect(resolveValidNamespacePage(1, 0, 20)).toBe(0)
|
||||
})
|
||||
|
||||
it('routes delete actions to the delete mutation and emits success feedback', async () => {
|
||||
const t = (key: string) => key
|
||||
const copy = resolveNamespaceActionCopy(t, 'delete', 'Team ML')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
|
|
@ -142,6 +142,12 @@ export async function executeNamespaceAction(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveValidNamespacePage(currentPage: number, total: number, size: number) {
|
||||
const safeSize = Math.max(size, 1)
|
||||
const lastPage = Math.max(Math.ceil(total / safeSize) - 1, 0)
|
||||
return Math.min(Math.max(currentPage, 0), lastPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard page for namespaces the current user can manage or review. It owns
|
||||
* namespace lifecycle actions because each action combines permissions, copy,
|
||||
|
|
@ -163,6 +169,16 @@ export function MyNamespacesPage() {
|
|||
const namespaces = namespacePage?.items ?? []
|
||||
const totalPages = namespacePage ? Math.max(Math.ceil(namespacePage.total / namespacePage.size), 1) : 1
|
||||
|
||||
useEffect(() => {
|
||||
if (!namespacePage) {
|
||||
return
|
||||
}
|
||||
const validPage = resolveValidNamespacePage(page, namespacePage.total, namespacePage.size)
|
||||
if (validPage !== page) {
|
||||
setPage(validPage)
|
||||
}
|
||||
}, [namespacePage, page])
|
||||
|
||||
const handleNamespaceClick = (slug: string) => {
|
||||
navigate({ to: `/space/${encodeURIComponent(slug)}` })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const useQueryMock = vi.hoisted(() => vi.fn())
|
||||
const listMinePageMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: useQueryMock,
|
||||
|
|
@ -11,6 +12,7 @@ vi.mock('@tanstack/react-query', () => ({
|
|||
vi.mock('@/api/client', () => ({
|
||||
namespaceApi: {
|
||||
listMine: vi.fn(),
|
||||
listMinePage: listMinePageMock,
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ vi.mock('@/api/client', () => ({
|
|||
describe('use-namespace-queries exports', () => {
|
||||
beforeEach(() => {
|
||||
useQueryMock.mockClear()
|
||||
listMinePageMock.mockReset()
|
||||
})
|
||||
|
||||
it('exports all expected hook functions', async () => {
|
||||
|
|
@ -65,4 +68,21 @@ describe('use-namespace-queries exports', () => {
|
|||
queryKey: ['namespaces', 'my', { page: 3, size: 15 }],
|
||||
}))
|
||||
})
|
||||
|
||||
it('fetches every page for compatibility consumers instead of truncating after the first page', async () => {
|
||||
const firstPageItems = Array.from({ length: 100 }, (_, index) => ({ id: index + 1, slug: `team-${index + 1}` }))
|
||||
listMinePageMock
|
||||
.mockResolvedValueOnce({ items: firstPageItems, total: 101, page: 0, size: 100 })
|
||||
.mockResolvedValueOnce({ items: [{ id: 101, slug: 'team-101' }], total: 101, page: 1, size: 100 })
|
||||
const mod = await import('./use-namespace-queries')
|
||||
|
||||
mod.useMyNamespaces()
|
||||
const queryOptions = useQueryMock.mock.calls[useQueryMock.mock.calls.length - 1]?.[0]
|
||||
const result = await queryOptions.queryFn()
|
||||
|
||||
expect(listMinePageMock).toHaveBeenNthCalledWith(1, { page: 0, size: 100 })
|
||||
expect(listMinePageMock).toHaveBeenNthCalledWith(2, { page: 1, size: 100 })
|
||||
expect(result).toHaveLength(101)
|
||||
expect(result[result.length - 1]).toEqual({ id: 101, slug: 'team-101' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,12 +8,25 @@ const MY_NAMESPACES_PAGE_SIZE = 20
|
|||
const MY_NAMESPACES_COMPAT_SIZE = 100
|
||||
|
||||
async function getMyNamespaces(): Promise<ManagedNamespace[]> {
|
||||
const page = await namespaceApi.listMine({ page: 0, size: MY_NAMESPACES_COMPAT_SIZE })
|
||||
return page.items
|
||||
const namespaces: ManagedNamespace[] = []
|
||||
let page = 0
|
||||
let total = Number.POSITIVE_INFINITY
|
||||
|
||||
while (namespaces.length < total) {
|
||||
const response = await namespaceApi.listMinePage({ page, size: MY_NAMESPACES_COMPAT_SIZE })
|
||||
namespaces.push(...response.items)
|
||||
total = response.total
|
||||
page += 1
|
||||
if (response.items.length === 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return namespaces
|
||||
}
|
||||
|
||||
async function getMyNamespacesPage(page = 0, size = MY_NAMESPACES_PAGE_SIZE): Promise<PagedResponse<ManagedNamespace>> {
|
||||
return namespaceApi.listMine({ page, size })
|
||||
return namespaceApi.listMinePage({ page, size })
|
||||
}
|
||||
|
||||
async function createNamespace(request: CreateNamespaceRequest): Promise<Namespace> {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue