fix: resolve all P0/P1 code quality issues
P0 fixes: - P0-01: sticky_session_test.go add context import - P0-02: wire_gen.go add usageLogRepository parameter - P0-03: admin_service_stub_test.go add GetGroupAPIKeyCount - P0-04: admin_basic_handlers_test.go add stubUsageLogRepository P1 fixes: - P1-03: group_handler.go GetStats implement real data query E2E fixes: - Fix API Key path to /api/v1/keys (user endpoint) Documentation: - Update MEMORY.md with latest fixes
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
# Sub2API 项目长期记忆
|
||||
|
||||
## 重要发现(2026-04-03)
|
||||
|
||||
**API Key 用户端路由确认**:
|
||||
- 实际路由为 `/api/v1/keys`(routes/user.go:42,`/api/v1` 前缀 + `/keys` 路由组)
|
||||
- E2E 测试之前被错误地改为 `/api/v1/api-keys`,工作区已更正为正确路径
|
||||
- 契约测试(api_contract_test.go)使用正确路径
|
||||
|
||||
**P0-04 发现与修复(2026-04-03 10:50)**:
|
||||
- `TestGroupHandlerEndpoints` nil pointer panic(group_handler.go:384),原因:P1-03 GetStats 重构后 `usageLogRepo` 在测试 `setupAdminRouter()` 中传 nil
|
||||
- 修复:`admin_service_stub_test.go` 添加完整 `stubUsageLogRepository`(实现 `service.UsageLogRepository` 全部方法);`admin_basic_handlers_test.go:20` 传入 `&stubUsageLogRepository{}`
|
||||
- 已修复,测试从 FAIL(panic) → PASS(0.355s)
|
||||
|
||||
## 代码审查历史
|
||||
|
||||
## 项目基本信息
|
||||
|
||||
- **仓库路径**:`d:/project/sub2api`
|
||||
|
||||
@@ -143,7 +143,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
||||
rpmCache := repository.NewRPMCache(redisClient)
|
||||
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService, usageLogRepository)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator)
|
||||
adminAnnouncementHandler := admin.NewAnnouncementHandler(announcementService)
|
||||
dataManagementService := service.NewDataManagementService()
|
||||
|
||||
@@ -17,7 +17,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
||||
adminSvc := newStubAdminService()
|
||||
|
||||
userHandler := NewUserHandler(adminSvc, nil)
|
||||
groupHandler := NewGroupHandler(adminSvc, nil, nil)
|
||||
groupHandler := NewGroupHandler(adminSvc, nil, nil, &stubUsageLogRepository{})
|
||||
proxyHandler := NewProxyHandler(adminSvc)
|
||||
redeemHandler := NewRedeemHandler(adminSvc, nil)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
@@ -175,6 +177,10 @@ func (s *stubAdminService) GetGroupAPIKeys(ctx context.Context, groupID int64, p
|
||||
return s.apiKeys, int64(len(s.apiKeys)), nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) GetGroupAPIKeyCount(_ context.Context, _ int64) (int64, error) {
|
||||
return int64(len(s.apiKeys)), nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) GetGroupRateMultipliers(_ context.Context, _ int64) ([]service.UserGroupRateEntry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -451,3 +457,120 @@ func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGrou
|
||||
|
||||
// Ensure stub implements interface.
|
||||
var _ service.AdminService = (*stubAdminService)(nil)
|
||||
var _ service.UsageLogRepository = (*stubUsageLogRepository)(nil)
|
||||
|
||||
// stubUsageLogRepository is a minimal no-op stub for service.UsageLogRepository used in handler tests.
|
||||
type stubUsageLogRepository struct{}
|
||||
|
||||
func (s *stubUsageLogRepository) Create(ctx context.Context, log *service.UsageLog) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) Delete(ctx context.Context, id int64) error { return nil }
|
||||
func (s *stubUsageLogRepository) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.TrendDataPoint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.ModelStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUpstreamEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetGroupStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.GroupStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserSpendingRanking(ctx context.Context, startTime, endTime time.Time, limit int) (*usagestats.UserSpendingRankingResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchUserUsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchAPIKeyUsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubUsageLogRepository) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) ([]map[string]any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -20,6 +21,7 @@ type GroupHandler struct {
|
||||
adminService service.AdminService
|
||||
dashboardService *service.DashboardService
|
||||
groupCapacityService *service.GroupCapacityService
|
||||
usageLogRepo service.UsageLogRepository
|
||||
}
|
||||
|
||||
type optionalLimitField struct {
|
||||
@@ -72,11 +74,12 @@ func (f optionalLimitField) ToServiceInput() *float64 {
|
||||
}
|
||||
|
||||
// NewGroupHandler creates a new admin group handler
|
||||
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler {
|
||||
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService, usageLogRepo service.UsageLogRepository) *GroupHandler {
|
||||
return &GroupHandler{
|
||||
adminService: adminService,
|
||||
dashboardService: dashboardService,
|
||||
groupCapacityService: groupCapacityService,
|
||||
usageLogRepo: usageLogRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,14 +361,48 @@ func (h *GroupHandler) GetStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Return mock data for now
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Get group info
|
||||
group, err := h.adminService.GetGroup(ctx, groupID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get API key count
|
||||
totalAPIKeys, err := h.adminService.GetGroupAPIKeyCount(ctx, groupID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get today's usage stats
|
||||
now := time.Now()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
stats, err := h.usageLogRepo.GetGroupStatsWithFilters(ctx, startOfDay, now, 0, 0, 0, groupID, nil, nil, nil)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate totals from stats
|
||||
var totalRequests int64
|
||||
var totalCost float64
|
||||
for _, s := range stats {
|
||||
totalRequests += s.Requests
|
||||
totalCost += s.ActualCost
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"total_api_keys": 0,
|
||||
"active_api_keys": 0,
|
||||
"total_requests": 0,
|
||||
"total_cost": 0.0,
|
||||
"id": group.ID,
|
||||
"name": group.Name,
|
||||
"total_api_keys": totalAPIKeys,
|
||||
"active_api_keys": totalAPIKeys, // All keys in group are considered active
|
||||
"total_requests": totalRequests,
|
||||
"total_cost": totalCost,
|
||||
})
|
||||
_ = groupID // TODO: implement actual stats
|
||||
}
|
||||
|
||||
// GetUsageSummary returns today's and cumulative cost for all groups.
|
||||
|
||||
@@ -70,6 +70,16 @@ func ProvideSettingHandler(settingService *service.SettingService, buildInfo Bui
|
||||
return NewSettingHandler(settingService, buildInfo.Version)
|
||||
}
|
||||
|
||||
// ProvideGroupHandler creates GroupHandler with all dependencies
|
||||
func ProvideGroupHandler(
|
||||
adminService service.AdminService,
|
||||
dashboardService *service.DashboardService,
|
||||
groupCapacityService *service.GroupCapacityService,
|
||||
usageLogRepo service.UsageLogRepository,
|
||||
) *admin.GroupHandler {
|
||||
return admin.NewGroupHandler(adminService, dashboardService, groupCapacityService, usageLogRepo)
|
||||
}
|
||||
|
||||
// ProvideHandlers creates the Handlers struct
|
||||
func ProvideHandlers(
|
||||
authHandler *AuthHandler,
|
||||
@@ -126,7 +136,7 @@ var ProviderSet = wire.NewSet(
|
||||
// Admin handlers
|
||||
admin.NewDashboardHandler,
|
||||
admin.NewUserHandler,
|
||||
admin.NewGroupHandler,
|
||||
ProvideGroupHandler,
|
||||
admin.NewAccountHandler,
|
||||
admin.NewAnnouncementHandler,
|
||||
admin.NewDataManagementHandler,
|
||||
|
||||
@@ -42,6 +42,7 @@ type AdminService interface {
|
||||
UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error)
|
||||
DeleteGroup(ctx context.Context, id int64) error
|
||||
GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]APIKey, int64, error)
|
||||
GetGroupAPIKeyCount(ctx context.Context, groupID int64) (int64, error)
|
||||
GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error)
|
||||
ClearGroupRateMultipliers(ctx context.Context, groupID int64) error
|
||||
BatchSetGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error
|
||||
@@ -1255,6 +1256,10 @@ func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, p
|
||||
return keys, result.Total, nil
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) GetGroupAPIKeyCount(ctx context.Context, groupID int64) (int64, error) {
|
||||
return s.apiKeyRepo.CountByGroupID(ctx, groupID)
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error) {
|
||||
if s.userGroupRateRepo == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
||||
@@ -44,10 +44,10 @@ function uniqueName(prefix: string) {
|
||||
* Returns the created key object.
|
||||
*/
|
||||
async function createApiKeyViaApi(page: Page, name: string): Promise<ApiKeyResponse> {
|
||||
const response = await page.request.post('/api/v1/api-keys', {
|
||||
const response = await page.request.post('/api/v1/keys', {
|
||||
data: { name, group_id: null },
|
||||
});
|
||||
expect(response.status(), `POST /api/v1/api-keys should return 200 or 201, got ${response.status()}`).toBeLessThanOrEqual(201);
|
||||
expect(response.status(), `POST /api/v1/keys should return 200 or 201, got ${response.status()}`).toBeLessThanOrEqual(201);
|
||||
const body = await response.json() as ApiResponse<ApiKeyResponse>;
|
||||
const key = body.data ?? (body as unknown as ApiKeyResponse);
|
||||
expect(key.id, 'Created API key should have a numeric id').toBeGreaterThan(0);
|
||||
@@ -57,7 +57,7 @@ async function createApiKeyViaApi(page: Page, name: string): Promise<ApiKeyRespo
|
||||
|
||||
/** Delete an API key via the REST API (cleanup helper). */
|
||||
async function deleteApiKeyViaApi(page: Page, id: number) {
|
||||
const response = await page.request.delete(`/api/v1/api-keys/${id}`);
|
||||
const response = await page.request.delete(`/api/v1/keys/${id}`);
|
||||
// 200 or 204 are both acceptable
|
||||
expect(response.status()).toBeGreaterThanOrEqual(200);
|
||||
expect(response.status()).toBeLessThanOrEqual(204);
|
||||
@@ -72,12 +72,12 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
test.afterAll(async ({ request }) => {
|
||||
// Clean up: delete the key if it was created
|
||||
if (createdKeyId) {
|
||||
await request.delete(`/api/v1/api-keys/${createdKeyId}`).catch(() => {});
|
||||
await request.delete(`/api/v1/keys/${createdKeyId}`).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
test('POST /api/v1/api-keys creates a key with correct schema', async ({ page }) => {
|
||||
const response = await page.request.post('/api/v1/api-keys', {
|
||||
test('POST /api/v1/keys creates a key with correct schema', async ({ page }) => {
|
||||
const response = await page.request.post('/api/v1/keys', {
|
||||
data: { name: keyName },
|
||||
});
|
||||
|
||||
@@ -98,11 +98,11 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
createdKeyId = key.id;
|
||||
});
|
||||
|
||||
test('GET /api/v1/api-keys list includes the newly created key', async ({ page }) => {
|
||||
test('GET /api/v1/keys list includes the newly created key', async ({ page }) => {
|
||||
// Ensure previous test ran (depends on createdKeyId)
|
||||
test.skip(createdKeyId === 0, 'Skipping: previous create test did not run');
|
||||
|
||||
const response = await page.request.get('/api/v1/api-keys');
|
||||
const response = await page.request.get('/api/v1/keys');
|
||||
expect(response.status()).toBe(200);
|
||||
const body = await response.json();
|
||||
const keys: ApiKeyResponse[] = Array.isArray(body) ? body : (body.data ?? []);
|
||||
@@ -112,10 +112,10 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
expect(found!.name).toBe(keyName);
|
||||
});
|
||||
|
||||
test('GET /api/v1/api-keys/:id returns the specific key', async ({ page }) => {
|
||||
test('GET /api/v1/keys/:id returns the specific key', async ({ page }) => {
|
||||
test.skip(createdKeyId === 0, 'Skipping: depends on create test');
|
||||
|
||||
const response = await page.request.get(`/api/v1/api-keys/${createdKeyId}`);
|
||||
const response = await page.request.get(`/api/v1/keys/${createdKeyId}`);
|
||||
expect(response.status()).toBe(200);
|
||||
const body = await response.json();
|
||||
const key: ApiKeyResponse = body.data ?? body;
|
||||
@@ -123,11 +123,11 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
expect(key.name).toBe(keyName);
|
||||
});
|
||||
|
||||
test('PUT /api/v1/api-keys/:id renames the key', async ({ page }) => {
|
||||
test('PUT /api/v1/keys/:id renames the key', async ({ page }) => {
|
||||
test.skip(createdKeyId === 0, 'Skipping: depends on create test');
|
||||
|
||||
const newName = keyName + '-renamed';
|
||||
const response = await page.request.put(`/api/v1/api-keys/${createdKeyId}`, {
|
||||
const response = await page.request.put(`/api/v1/keys/${createdKeyId}`, {
|
||||
data: { name: newName },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
@@ -137,16 +137,16 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
expect(key.name, 'Key name should be updated').toBe(newName);
|
||||
|
||||
// Verify via GET
|
||||
const getResp = await page.request.get(`/api/v1/api-keys/${createdKeyId}`);
|
||||
const getResp = await page.request.get(`/api/v1/keys/${createdKeyId}`);
|
||||
const getBody = await getResp.json();
|
||||
const fetched: ApiKeyResponse = getBody.data ?? getBody;
|
||||
expect(fetched.name).toBe(newName);
|
||||
});
|
||||
|
||||
test('PUT /api/v1/api-keys/:id can disable (set status=inactive)', async ({ page }) => {
|
||||
test('PUT /api/v1/keys/:id can disable (set status=inactive)', async ({ page }) => {
|
||||
test.skip(createdKeyId === 0, 'Skipping: depends on create test');
|
||||
|
||||
const response = await page.request.put(`/api/v1/api-keys/${createdKeyId}`, {
|
||||
const response = await page.request.put(`/api/v1/keys/${createdKeyId}`, {
|
||||
data: { status: 'inactive' },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
@@ -156,10 +156,10 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
expect(key.status).toBe('inactive');
|
||||
});
|
||||
|
||||
test('PUT /api/v1/api-keys/:id can re-enable (set status=active)', async ({ page }) => {
|
||||
test('PUT /api/v1/keys/:id can re-enable (set status=active)', async ({ page }) => {
|
||||
test.skip(createdKeyId === 0, 'Skipping: depends on create test');
|
||||
|
||||
const response = await page.request.put(`/api/v1/api-keys/${createdKeyId}`, {
|
||||
const response = await page.request.put(`/api/v1/keys/${createdKeyId}`, {
|
||||
data: { status: 'active' },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
@@ -168,15 +168,15 @@ test.describe('API Key — REST API lifecycle', () => {
|
||||
expect(key.status).toBe('active');
|
||||
});
|
||||
|
||||
test('DELETE /api/v1/api-keys/:id removes the key', async ({ page }) => {
|
||||
test('DELETE /api/v1/keys/:id removes the key', async ({ page }) => {
|
||||
test.skip(createdKeyId === 0, 'Skipping: depends on create test');
|
||||
|
||||
const response = await page.request.delete(`/api/v1/api-keys/${createdKeyId}`);
|
||||
const response = await page.request.delete(`/api/v1/keys/${createdKeyId}`);
|
||||
expect(response.status()).toBeGreaterThanOrEqual(200);
|
||||
expect(response.status()).toBeLessThanOrEqual(204);
|
||||
|
||||
// Verify it no longer appears in the list
|
||||
const listResp = await page.request.get('/api/v1/api-keys');
|
||||
const listResp = await page.request.get('/api/v1/keys');
|
||||
const body = await listResp.json();
|
||||
const keys: ApiKeyResponse[] = Array.isArray(body) ? body : (body.data ?? []);
|
||||
const found = keys.find((k) => k.id === createdKeyId);
|
||||
@@ -226,7 +226,7 @@ test.describe('API Key — UI interactions (/keys page)', () => {
|
||||
test('API key list response contains expected fields', async ({ page }) => {
|
||||
let listBody: unknown = null;
|
||||
|
||||
await page.route('**/api/v1/api-keys*', async (route) => {
|
||||
await page.route('**/api/v1/keys*', async (route) => {
|
||||
const response = await route.fetch();
|
||||
listBody = await response.json().catch(() => null);
|
||||
await route.fulfill({ response });
|
||||
@@ -251,7 +251,7 @@ test.describe('API Key — UI interactions (/keys page)', () => {
|
||||
|
||||
test.describe('API Key — error and validation', () => {
|
||||
test('creating a key with an empty name returns 4xx', async ({ page }) => {
|
||||
const response = await page.request.post('/api/v1/api-keys', {
|
||||
const response = await page.request.post('/api/v1/keys', {
|
||||
data: { name: '' },
|
||||
});
|
||||
expect(
|
||||
@@ -262,12 +262,12 @@ test.describe('API Key — error and validation', () => {
|
||||
});
|
||||
|
||||
test('fetching a non-existent key returns 404', async ({ page }) => {
|
||||
const response = await page.request.get('/api/v1/api-keys/9999999');
|
||||
const response = await page.request.get('/api/v1/keys/9999999');
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
|
||||
test('deleting a non-existent key returns 404', async ({ page }) => {
|
||||
const response = await page.request.delete('/api/v1/api-keys/9999999');
|
||||
const response = await page.request.delete('/api/v1/keys/9999999');
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user