1
0
mirror of https://github.com/quay/quay.git synced 2026-01-27 18:42:52 +03:00
Files
quay/web/playwright/utils/api/client.ts
jbpratt c6eb6b573d test(web): migrate more tests to playwright (#4773)
* test(web): migrate superuser-org-actions.cy.ts to Playwright

Migrate Cypress E2E tests for superuser organization actions to Playwright
following the project's MIGRATION.md guidelines.

Changes:
- Add new test file: playwright/e2e/superuser/org-actions.spec.ts
- Consolidate 12 Cypress tests into 5 focused Playwright tests
- Use real API data instead of mocked fixtures
- Auto-cleanup via TestApi fixture

Test coverage:
- Superuser sees actions column and options menu for organizations
- Regular user does not see organization options menu
- Superuser can rename organization
- Superuser can delete organization
- Superuser can take ownership of organization

Skipped from migration:
- Quota menu tests (already covered by quota.spec.ts)
- Fresh login requirement tests (low value, complex to mock)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(web): set superuser feature tag

Signed-off-by: Brady Pratt <bpratt@redhat.com>

* test(web): migrate superuser-messages.cy.ts to Playwright

Migrate superuser messages tests from Cypress to Playwright, consolidating
14 original tests into 6 focused, value-add tests.

Tests cover:
- Non-superuser redirect to organization page
- Full CRUD flow: create, view, and delete messages via UI
- Error state when API fails to load messages
- Loading spinner during message fetch
- Read-only superuser can access and view messages
- Read-only superuser sees disabled create/delete actions

Infrastructure additions:
- Add message() method to TestApi with auto-cleanup
- Add CreatedMessage interface for type safety
- Add SUPERUSERS_FULL_ACCESS feature tag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(web): migrate superuser-user-management.cy.ts to Playwright

Consolidates 29 Cypress tests into 11 Playwright tests covering
superuser user management functionality.

Changes:
- Add CreatedUser interface and user() method to TestApi for
  user creation with auto-cleanup
- Add createUserAsSuperuser() to API client using superuser endpoint
- Add QuayAuthType and skipUnlessAuthType() helper for auth-type
  conditional tests
- Create user-management.spec.ts with consolidated tests

Tests cover:
- Create user via UI (Database/AppToken auth only)
- User access control based on user type
- Change email and password (Database auth only)
- Toggle user status (disable/enable)
- Delete user
- Take ownership (convert user to org)
- Fresh login error handling with mocked responses
- Send recovery email (MAILING feature)
- Auth type visibility

Key patterns:
- Uses search to find users in paginated list
- page.route() only for error scenarios per MIGRATION.md
- skipUnlessAuthType() for auth-dependent tests
- @feature:SUPERUSERS_FULL_ACCESS tag for all tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(web): delete more migrated cypress tests

Signed-off-by: Brady Pratt <bpratt@redhat.com>

* test(web): migrate superuser-framework Cypress test to Playwright

Consolidates 7 Cypress tests into 4 Playwright tests covering:
- Superuser navigation to all superuser pages
- Navigation section visibility and expansion
- Organizations table Settings column and actions menu
- Regular user restrictions and redirects

Uses real superuserPage/authenticatedPage fixtures instead of mocking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(web): migrate superuser-service-keys Cypress test to Playwright

Consolidates 17 Cypress tests into 5 Playwright tests:
- non-superuser redirect to organization page
- superuser CRUD lifecycle (create, view, search, update, delete)
- error handling when create fails
- read-only superuser permission restrictions
- bulk select and delete operations

Adds service key API methods to Playwright test utilities:
- getServiceKeys, createServiceKey, updateServiceKey, deleteServiceKey
- TestApi.serviceKey() with auto-cleanup support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(web): migrate superuser-change-log Cypress test to Playwright

Migrate superuser-change-log.cy.ts to Playwright with test consolidation:
- 7 original tests reduced to 2 focused tests
- Access control tests already covered by framework.spec.ts
- Loading spinner and empty state tests skipped (low value)
- Uses real API calls except for error state (acceptable mock)
- No PatternFly class dependencies, uses role-based selectors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(web): migrate superuser-usage-logs Cypress test to Playwright

- Consolidate 7 Cypress tests into 2 Playwright tests
- Access control tests already covered by framework.spec.ts
- Add data-testid="usage-logs-table" to UsageLogsTable component
- Tests verify: page header, date pickers, chart toggle, table loading,
  and filter functionality
- Use structural assertions for parallel test safety

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Brady Pratt <bpratt@redhat.com>

* test(web): remove unneeded comments

Signed-off-by: Brady Pratt <bpratt@redhat.com>

---------

Signed-off-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 17:54:25 -06:00

1169 lines
28 KiB
TypeScript

/**
* API client for Playwright e2e tests
*
* Provides API interactions with CSRF token caching to reduce redundant requests.
*/
import {APIRequestContext} from '@playwright/test';
import {API_URL} from '../config';
export type RepositoryVisibility = 'public' | 'private';
export type RepositoryState = 'NORMAL' | 'MIRROR' | 'READ_ONLY';
export type TeamRole = 'member' | 'creator' | 'admin';
export type PrototypeRole = 'read' | 'write' | 'admin';
export type MessageSeverity = 'info' | 'warning' | 'error';
export type MessageMediaType = 'text/plain' | 'text/markdown';
export interface MirrorConfig {
external_reference: string;
sync_interval: number;
sync_start_date: string;
root_rule: {
rule_kind: 'tag_glob_csv';
rule_value: string[];
};
robot_username: string;
skopeo_timeout_interval?: number;
is_enabled?: boolean;
external_registry_username?: string | null;
external_registry_password?: string | null;
external_registry_config?: {
verify_tls?: boolean;
unsigned_images?: boolean;
proxy?: {
http_proxy?: string | null;
https_proxy?: string | null;
no_proxy?: string | null;
};
};
}
export interface MirrorConfigResponse extends MirrorConfig {
sync_status: string;
sync_retries_remaining: number;
sync_expiration_date: string | null;
mirror_type: string;
}
export interface CreateUserResponse {
username: string;
awaiting_verification?: boolean;
}
export interface CreateRobotResponse {
name: string;
token: string;
}
export interface PrototypeDelegate {
name: string;
kind: 'user' | 'team';
}
export interface PrototypeActivatingUser {
name: string;
}
export interface Prototype {
id: string;
role: string;
activating_user: {
name: string;
is_robot: boolean;
kind: string;
is_org_member: boolean;
} | null;
delegate: {
name: string;
kind: string;
};
}
export interface GetPrototypesResponse {
prototypes: Prototype[];
}
// Global message types
export interface GlobalMessage {
uuid: string;
content: string;
media_type: MessageMediaType;
severity: MessageSeverity;
}
export interface GlobalMessagesResponse {
messages: GlobalMessage[];
}
// Service key types
export interface ServiceKeyApproval {
approval_type: string;
approver?: {
name: string;
username: string;
kind: string;
};
notes?: string;
}
export interface ServiceKey {
kid: string;
name?: string;
service: string;
created_date: string | number;
expiration_date?: string | number;
approval?: ServiceKeyApproval;
metadata?: Record<string, unknown>;
}
export interface ServiceKeysResponse {
keys: ServiceKey[];
}
export class ApiClient {
private request: APIRequestContext;
private csrfToken: string | null = null;
constructor(request: APIRequestContext) {
this.request = request;
}
private async fetchToken(): Promise<string> {
if (!this.csrfToken) {
const response = await this.request.get(`${API_URL}/csrf_token`, {
timeout: 5000,
});
if (!response.ok()) {
throw new Error(`Failed to get CSRF token: ${response.status()}`);
}
const data = await response.json();
this.csrfToken = data.csrf_token;
}
return this.csrfToken;
}
/**
* Get the CSRF token (fetches if not cached)
* Primarily for use by test fixtures that need the raw token.
*/
async getToken(): Promise<string> {
return this.fetchToken();
}
// Organization methods
async createOrganization(
name: string,
email?: string,
): Promise<{name: string}> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/organization/`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
name,
email: email || `${name}@example.com`,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create organization ${name}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteOrganization(name: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/organization/${name}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete organization ${name}: ${response.status()} - ${body}`,
);
}
}
// Repository methods
async createRepository(
namespace: string,
name: string,
visibility: RepositoryVisibility = 'private',
description = '',
): Promise<{namespace: string; name: string; kind: string}> {
const token = await this.fetchToken();
const response = await this.request.post(`${API_URL}/api/v1/repository`, {
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
namespace,
repository: name,
visibility,
description,
repo_kind: 'image',
},
});
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create repository ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteRepository(namespace: string, name: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/repository/${namespace}/${name}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete repository ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
}
async changeRepositoryState(
namespace: string,
name: string,
state: RepositoryState,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/repository/${namespace}/${name}/changestate`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
state,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to change repository state ${namespace}/${name} to ${state}: ${response.status()} - ${body}`,
);
}
}
// Repository mirroring methods
async createMirrorConfig(
namespace: string,
name: string,
config: MirrorConfig,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${name}/mirror`,
{
timeout: 10000,
headers: {
'X-CSRF-Token': token,
},
data: {
external_reference: config.external_reference,
sync_interval: config.sync_interval,
sync_start_date: config.sync_start_date,
root_rule: config.root_rule,
robot_username: config.robot_username,
skopeo_timeout_interval: config.skopeo_timeout_interval ?? 300,
is_enabled: config.is_enabled ?? true,
external_registry_username: config.external_registry_username ?? null,
external_registry_password: config.external_registry_password ?? null,
external_registry_config: config.external_registry_config ?? {
verify_tls: true,
unsigned_images: false,
proxy: {
http_proxy: null,
https_proxy: null,
no_proxy: null,
},
},
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create mirror config for ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
}
async getMirrorConfig(
namespace: string,
name: string,
): Promise<MirrorConfigResponse | null> {
const response = await this.request.get(
`${API_URL}/api/v1/repository/${namespace}/${name}/mirror`,
{
timeout: 5000,
},
);
if (response.status() === 404) {
return null;
}
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to get mirror config for ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async updateMirrorConfig(
namespace: string,
name: string,
updates: Partial<MirrorConfig>,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/repository/${namespace}/${name}/mirror`,
{
timeout: 10000,
headers: {
'X-CSRF-Token': token,
},
data: updates,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to update mirror config for ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
}
async triggerMirrorSync(namespace: string, name: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${name}/mirror/sync-now`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to trigger mirror sync for ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
}
async cancelMirrorSync(namespace: string, name: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${name}/mirror/sync-cancel`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to cancel mirror sync for ${namespace}/${name}: ${response.status()} - ${body}`,
);
}
}
// Repository notification methods
async createRepositoryNotification(
namespace: string,
repo: string,
event: string,
method: string,
config: Record<string, unknown>,
eventConfig: Record<string, unknown> = {},
title?: string,
): Promise<{uuid: string}> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${repo}/notification/`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
event,
method,
config,
eventConfig,
title,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create repository notification: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteRepositoryNotification(
namespace: string,
repo: string,
uuid: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/repository/${namespace}/${repo}/notification/${uuid}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete repository notification ${uuid}: ${response.status()} - ${body}`,
);
}
}
async enableRepositoryNotification(
namespace: string,
repo: string,
uuid: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${repo}/notification/${uuid}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to enable repository notification ${uuid}: ${response.status()} - ${body}`,
);
}
}
async testRepositoryNotification(
namespace: string,
repo: string,
uuid: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/repository/${namespace}/${repo}/notification/${uuid}/test`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to test repository notification ${uuid}: ${response.status()} - ${body}`,
);
}
}
async getRepositoryNotifications(
namespace: string,
repo: string,
): Promise<{
notifications: Array<{
uuid: string;
title: string;
event: string;
method: string;
number_of_failures: number;
}>;
}> {
const response = await this.request.get(
`${API_URL}/api/v1/repository/${namespace}/${repo}/notification/`,
{
timeout: 5000,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to get repository notifications: ${response.status()} - ${body}`,
);
}
return response.json();
}
// Team methods
async createTeam(
orgName: string,
teamName: string,
role: TeamRole = 'member',
): Promise<{name: string; role: string}> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/organization/${orgName}/team/${teamName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
role,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create team ${teamName} in ${orgName}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteTeam(orgName: string, teamName: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/organization/${orgName}/team/${teamName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete team ${teamName} from ${orgName}: ${response.status()} - ${body}`,
);
}
}
// Robot account methods
async createRobot(
orgName: string,
robotShortname: string,
description = '',
): Promise<CreateRobotResponse> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/organization/${orgName}/robots/${robotShortname}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
description,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create robot ${robotShortname} in ${orgName}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteRobot(orgName: string, robotShortname: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/organization/${orgName}/robots/${robotShortname}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete robot ${robotShortname} from ${orgName}: ${response.status()} - ${body}`,
);
}
}
// Prototype (default permission) methods
async getPrototypes(orgName: string): Promise<GetPrototypesResponse> {
const response = await this.request.get(
`${API_URL}/api/v1/organization/${orgName}/prototypes`,
{
timeout: 5000,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to get prototypes for ${orgName}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async createPrototype(
orgName: string,
role: PrototypeRole,
delegate: PrototypeDelegate,
activatingUser?: PrototypeActivatingUser,
): Promise<{id: string}> {
const token = await this.fetchToken();
const data: Record<string, unknown> = {
role,
delegate,
};
if (activatingUser) {
data.activating_user = activatingUser;
}
const response = await this.request.post(
`${API_URL}/api/v1/organization/${orgName}/prototypes`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create prototype in ${orgName}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deletePrototype(orgName: string, prototypeId: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/organization/${orgName}/prototypes/${prototypeId}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete prototype ${prototypeId} from ${orgName}: ${response.status()} - ${body}`,
);
}
}
// User methods
async createUser(
username: string,
password: string,
email: string,
): Promise<CreateUserResponse> {
const token = await this.fetchToken();
const response = await this.request.post(`${API_URL}/api/v1/user/`, {
timeout: 10000,
headers: {
'X-CSRF-Token': token,
},
data: {
username,
password,
email,
},
});
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create user ${username}: ${response.status()} - ${body}`,
);
}
const result = await response.json();
return {
username: result.username || username,
awaiting_verification: result.awaiting_verification,
};
}
/**
* Create a user as superuser (requires superuser API context).
* Returns the generated temporary password.
*/
async createUserAsSuperuser(
username: string,
email?: string,
): Promise<{username: string; email?: string; password: string}> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/superuser/users/`,
{
timeout: 10000,
headers: {
'X-CSRF-Token': token,
},
data: {
username,
email,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create user as superuser ${username}: ${response.status()} - ${body}`,
);
}
const result = await response.json();
return {
username: result.username,
email: result.email,
password: result.password,
};
}
async deleteUser(username: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/superuser/users/${username}`,
{
timeout: 10000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete user ${username}: ${response.status()} - ${body}`,
);
}
}
async userExists(username: string): Promise<boolean> {
const response = await this.request.get(
`${API_URL}/api/v1/users/${username}`,
{
timeout: 5000,
},
);
return response.ok();
}
// Auth methods
async signIn(username: string, password: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.post(`${API_URL}/api/v1/signin`, {
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
username,
password,
},
});
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to sign in as ${username}: ${response.status()} - ${body}`,
);
}
}
// User notification methods
async getUserNotifications(): Promise<{
notifications: Array<{
id: string;
kind: string;
metadata: {name: string; repository: string};
dismissed: boolean;
}>;
additional: boolean;
}> {
const response = await this.request.get(
`${API_URL}/api/v1/user/notifications`,
{
timeout: 5000,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to get user notifications: ${response.status()} - ${body}`,
);
}
return response.json();
}
// Team member methods (for test setup)
async addTeamMember(
orgName: string,
teamName: string,
memberName: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/organization/${orgName}/team/${teamName}/members/${memberName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to add member ${memberName} to team ${teamName}: ${response.status()} - ${body}`,
);
}
}
async removeTeamMember(
orgName: string,
teamName: string,
memberName: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/organization/${orgName}/team/${teamName}/members/${memberName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to remove member ${memberName} from team ${teamName}: ${response.status()} - ${body}`,
);
}
}
// Repository permission methods
async addRepositoryPermission(
namespace: string,
repo: string,
entityType: 'user' | 'team',
entityName: string,
role: PrototypeRole,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/repository/${namespace}/${repo}/permissions/${entityType}/${entityName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
role,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to add ${entityType} permission for ${entityName} on ${namespace}/${repo}: ${response.status()} - ${body}`,
);
}
}
async deleteRepositoryPermission(
namespace: string,
repo: string,
entityType: 'user' | 'team',
entityName: string,
): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/repository/${namespace}/${repo}/permissions/${entityType}/${entityName}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete ${entityType} permission for ${entityName} on ${namespace}/${repo}: ${response.status()} - ${body}`,
);
}
}
// Global message methods (superuser only)
async getMessages(): Promise<GlobalMessage[]> {
const response = await this.request.get(`${API_URL}/api/v1/messages`, {
timeout: 5000,
});
if (!response.ok()) {
const body = await response.text();
throw new Error(`Failed to get messages: ${response.status()} - ${body}`);
}
const data: GlobalMessagesResponse = await response.json();
return data.messages || [];
}
async createMessage(
content: string,
severity: MessageSeverity = 'info',
mediaType: MessageMediaType = 'text/markdown',
): Promise<GlobalMessage> {
const token = await this.fetchToken();
const response = await this.request.post(`${API_URL}/api/v1/messages`, {
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
message: {
content,
media_type: mediaType,
severity,
},
},
});
if (response.status() !== 201) {
const body = await response.text();
throw new Error(
`Failed to create message: ${response.status()} - ${body}`,
);
}
// API doesn't return the created message, so fetch to get the UUID
const messages = await this.getMessages();
const created = messages.find((m) => m.content === content);
if (!created) {
throw new Error('Created message not found after creation');
}
return created;
}
async deleteMessage(uuid: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/message/${uuid}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete message ${uuid}: ${response.status()} - ${body}`,
);
}
}
// Service key methods (superuser only)
async getServiceKeys(): Promise<ServiceKey[]> {
const response = await this.request.get(
`${API_URL}/api/v1/superuser/keys`,
{
timeout: 5000,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to get service keys: ${response.status()} - ${body}`,
);
}
const data: ServiceKeysResponse = await response.json();
return data.keys || [];
}
async createServiceKey(
service: string,
name?: string,
expiration?: number,
): Promise<ServiceKey> {
const token = await this.fetchToken();
const response = await this.request.post(
`${API_URL}/api/v1/superuser/keys`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: {
service,
name,
expiration: expiration ?? null,
},
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to create service key: ${response.status()} - ${body}`,
);
}
return response.json();
}
async updateServiceKey(
kid: string,
updates: {name?: string; expiration?: number},
): Promise<ServiceKey> {
const token = await this.fetchToken();
const response = await this.request.put(
`${API_URL}/api/v1/superuser/keys/${kid}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
data: updates,
},
);
if (!response.ok()) {
const body = await response.text();
throw new Error(
`Failed to update service key ${kid}: ${response.status()} - ${body}`,
);
}
return response.json();
}
async deleteServiceKey(kid: string): Promise<void> {
const token = await this.fetchToken();
const response = await this.request.delete(
`${API_URL}/api/v1/superuser/keys/${kid}`,
{
timeout: 5000,
headers: {
'X-CSRF-Token': token,
},
},
);
if (!response.ok() && response.status() !== 404) {
const body = await response.text();
throw new Error(
`Failed to delete service key ${kid}: ${response.status()} - ${body}`,
);
}
}
}