feat: M2-01 — User & Account deepening, membership + deletion + devices
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 33s
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 33s
- UserDevice + AccountDeletionRequest Prisma models - CAPI: membership query, deletion request/cancel, device list/remove - AAPI: membership assign, deletion approve/reject, device view Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
14eaad53c3
commit
292e7e5638
@ -48,6 +48,8 @@ jobs:
|
|||||||
$MYSQL_CMD -e "DROP TABLE IF EXISTS ProviderConfig;" 2>/dev/null || true
|
$MYSQL_CMD -e "DROP TABLE IF EXISTS ProviderConfig;" 2>/dev/null || true
|
||||||
$MYSQL_CMD -e "DROP TABLE IF EXISTS FallbackEvent;" 2>/dev/null || true
|
$MYSQL_CMD -e "DROP TABLE IF EXISTS FallbackEvent;" 2>/dev/null || true
|
||||||
$MYSQL_CMD -e "DROP TABLE IF EXISTS ViolationRecord;" 2>/dev/null || true
|
$MYSQL_CMD -e "DROP TABLE IF EXISTS ViolationRecord;" 2>/dev/null || true
|
||||||
|
$MYSQL_CMD -e "DROP TABLE IF EXISTS UserDevice;" 2>/dev/null || true
|
||||||
|
$MYSQL_CMD -e "DROP TABLE IF EXISTS AccountDeletionRequest;" 2>/dev/null || true
|
||||||
$MYSQL_CMD -e "ALTER TABLE UploadedFile DROP COLUMN objectKey;" 2>/dev/null || true
|
$MYSQL_CMD -e "ALTER TABLE UploadedFile DROP COLUMN objectKey;" 2>/dev/null || true
|
||||||
$MYSQL_CMD -e "ALTER TABLE UploadedFile DROP COLUMN bucket;" 2>/dev/null || true
|
$MYSQL_CMD -e "ALTER TABLE UploadedFile DROP COLUMN bucket;" 2>/dev/null || true
|
||||||
$MYSQL_CMD -e "DROP INDEX UploadedFile_objectKey_idx ON UploadedFile;" 2>/dev/null || true
|
$MYSQL_CMD -e "DROP INDEX UploadedFile_objectKey_idx ON UploadedFile;" 2>/dev/null || true
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `UserDevice` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`userId` VARCHAR(191) NOT NULL,
|
||||||
|
`deviceId` VARCHAR(255) NOT NULL,
|
||||||
|
`deviceName` VARCHAR(100) NULL,
|
||||||
|
`osVersion` VARCHAR(50) NULL,
|
||||||
|
`pushToken` VARCHAR(500) NULL,
|
||||||
|
`lastSeenAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `UserDevice_userId_deviceId_key`(`userId`, `deviceId`),
|
||||||
|
INDEX `UserDevice_userId_idx`(`userId`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `AccountDeletionRequest` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`userId` VARCHAR(191) NOT NULL,
|
||||||
|
`status` VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||||
|
`requestedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`coolingEndsAt` DATETIME(3) NOT NULL,
|
||||||
|
`reviewedBy` VARCHAR(100) NULL,
|
||||||
|
`reviewedAt` DATETIME(3) NULL,
|
||||||
|
`completedAt` DATETIME(3) NULL,
|
||||||
|
`cancelledAt` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
INDEX `AccountDeletionRequest_userId_idx`(`userId`),
|
||||||
|
INDEX `AccountDeletionRequest_status_idx`(`status`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@ -1051,6 +1051,36 @@ model UserMembership {
|
|||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserDevice {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
deviceId String @db.VarChar(255)
|
||||||
|
deviceName String? @db.VarChar(100)
|
||||||
|
osVersion String? @db.VarChar(50)
|
||||||
|
pushToken String? @db.VarChar(500)
|
||||||
|
lastSeenAt DateTime @default(now())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, deviceId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model AccountDeletionRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
status String @default("pending") @db.VarChar(16)
|
||||||
|
requestedAt DateTime @default(now())
|
||||||
|
coolingEndsAt DateTime
|
||||||
|
reviewedBy String? @db.VarChar(100)
|
||||||
|
reviewedAt DateTime?
|
||||||
|
completedAt DateTime?
|
||||||
|
cancelledAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([status])
|
||||||
|
}
|
||||||
|
|
||||||
model QuotaUsage {
|
model QuotaUsage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
|
|||||||
80
src/modules/users/admin-users-mgmt.controller.ts
Normal file
80
src/modules/users/admin-users-mgmt.controller.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { Controller, Get, Post, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
|
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||||
|
import { AdminRoles } from '../../common/decorators/admin-roles.decorator';
|
||||||
|
import type { AdminRole } from '../../common/types/admin-role.enum';
|
||||||
|
|
||||||
|
@ApiTags('admin-users')
|
||||||
|
@Controller('admin-api/users')
|
||||||
|
@UseGuards(AdminAuthGuard, AdminRolesGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AdminUsersMgmtController {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// ── Membership ──
|
||||||
|
|
||||||
|
@Get('memberships')
|
||||||
|
@AdminRoles('ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '用户会员列表' })
|
||||||
|
async memberships(@Query('userId') userId?: string) {
|
||||||
|
return this.prisma.userMembership.findMany({
|
||||||
|
where: userId ? { userId } : undefined,
|
||||||
|
include: { plan: true },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('memberships')
|
||||||
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '手动分配会员' })
|
||||||
|
async addMembership(@Body() d: { userId: string; planId: string; expiresAt?: string }) {
|
||||||
|
return this.prisma.userMembership.create({
|
||||||
|
data: { userId: d.userId, planId: d.planId, expiresAt: d.expiresAt ? new Date(d.expiresAt) : null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deletion Requests ──
|
||||||
|
|
||||||
|
@Get('deletion-requests')
|
||||||
|
@AdminRoles('ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '注销申请列表' })
|
||||||
|
async deletionRequests(@Query('status') status?: string) {
|
||||||
|
return this.prisma.accountDeletionRequest.findMany({
|
||||||
|
where: status ? { status } : undefined,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('deletion-requests/:id/approve')
|
||||||
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '批准注销(立即执行)' })
|
||||||
|
async approveDeletion(@Param('id') id: string) {
|
||||||
|
return this.prisma.accountDeletionRequest.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: 'completed', reviewedAt: new Date(), completedAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('deletion-requests/:id/reject')
|
||||||
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '驳回注销' })
|
||||||
|
async rejectDeletion(@Param('id') id: string) {
|
||||||
|
return this.prisma.accountDeletionRequest.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: 'cancelled', reviewedAt: new Date(), cancelledAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Devices ──
|
||||||
|
|
||||||
|
@Get(':userId/devices')
|
||||||
|
@AdminRoles('ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '查看用户设备' })
|
||||||
|
async userDevices(@Param('userId') userId: string) {
|
||||||
|
return this.prisma.userDevice.findMany({ where: { userId }, orderBy: { lastSeenAt: 'desc' } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Patch, Body } from '@nestjs/common';
|
import { Controller, Get, Patch, Post, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
@ -10,9 +10,10 @@ import type { UserPayload } from '../../common/types';
|
|||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
// ── Profile ──
|
||||||
|
|
||||||
@Get('me')
|
@Get('me')
|
||||||
@ApiOperation({ summary: '获取当前用户信息' })
|
@ApiOperation({ summary: '获取当前用户信息' })
|
||||||
@ApiResponse({ status: 200, description: '用户信息' })
|
|
||||||
async getProfile(@CurrentUser() user: UserPayload) {
|
async getProfile(@CurrentUser() user: UserPayload) {
|
||||||
return this.usersService.getProfile(String(user.id));
|
return this.usersService.getProfile(String(user.id));
|
||||||
}
|
}
|
||||||
@ -23,15 +24,8 @@ export class UsersController {
|
|||||||
return this.usersService.updateProfile(String(user.id), body);
|
return this.usersService.updateProfile(String(user.id), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('me/preferences')
|
|
||||||
@ApiOperation({ summary: '更新用户偏好' })
|
|
||||||
async updatePreferences(@CurrentUser() user: UserPayload, @Body() body: any) {
|
|
||||||
return this.usersService.updatePreferences(String(user.id), body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('me/profile')
|
@Get('me/profile')
|
||||||
@ApiOperation({ summary: '获取用户学习档案' })
|
@ApiOperation({ summary: '获取用户学习档案' })
|
||||||
@ApiResponse({ status: 200, description: '用户学习档案' })
|
|
||||||
async getProfileDetail(@CurrentUser() user: UserPayload) {
|
async getProfileDetail(@CurrentUser() user: UserPayload) {
|
||||||
return this.usersService.getProfileDetail(String(user.id));
|
return this.usersService.getProfileDetail(String(user.id));
|
||||||
}
|
}
|
||||||
@ -41,4 +35,46 @@ export class UsersController {
|
|||||||
async updateProfileDetail(@CurrentUser() user: UserPayload, @Body() body: any) {
|
async updateProfileDetail(@CurrentUser() user: UserPayload, @Body() body: any) {
|
||||||
return this.usersService.updateProfileDetail(String(user.id), body);
|
return this.usersService.updateProfileDetail(String(user.id), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('me/preferences')
|
||||||
|
@ApiOperation({ summary: '更新用户偏好' })
|
||||||
|
async updatePreferences(@CurrentUser() user: UserPayload, @Body() body: any) {
|
||||||
|
return this.usersService.updatePreferences(String(user.id), body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Membership ──
|
||||||
|
|
||||||
|
@Get('me/membership')
|
||||||
|
@ApiOperation({ summary: '查询当前会员状态' })
|
||||||
|
async myMembership(@CurrentUser() user: UserPayload) {
|
||||||
|
return this.usersService.getMembership(String(user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Account Deletion ──
|
||||||
|
|
||||||
|
@Post('me/deletion-request')
|
||||||
|
@ApiOperation({ summary: '申请账号注销' })
|
||||||
|
async requestDeletion(@CurrentUser() user: UserPayload) {
|
||||||
|
return this.usersService.requestDeletion(String(user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('me/deletion-request')
|
||||||
|
@ApiOperation({ summary: '撤销注销申请' })
|
||||||
|
async cancelDeletion(@CurrentUser() user: UserPayload) {
|
||||||
|
return this.usersService.cancelDeletion(String(user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Device Management ──
|
||||||
|
|
||||||
|
@Get('me/devices')
|
||||||
|
@ApiOperation({ summary: '我的设备列表' })
|
||||||
|
async myDevices(@CurrentUser() user: UserPayload) {
|
||||||
|
return this.usersService.getDevices(String(user.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('me/devices/:deviceId')
|
||||||
|
@ApiOperation({ summary: '远程登出设备' })
|
||||||
|
async removeDevice(@CurrentUser() user: UserPayload, @Param('deviceId') deviceId: string) {
|
||||||
|
return this.usersService.removeDevice(String(user.id), deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UsersController } from './users.controller';
|
import { UsersController } from './users.controller';
|
||||||
|
import { AdminUsersMgmtController } from './admin-users-mgmt.controller';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
import { UsersRepository } from './users.repository';
|
import { UsersRepository } from './users.repository';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [UsersController],
|
controllers: [UsersController, AdminUsersMgmtController],
|
||||||
providers: [UsersService, UsersRepository],
|
providers: [UsersService, UsersRepository, PrismaService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
@ -1,27 +1,71 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { UsersRepository } from './users.repository';
|
import { UsersRepository } from './users.repository';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
|
||||||
|
const DELETION_COOLING_DAYS = 7;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private readonly usersRepository: UsersRepository) {}
|
constructor(
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getProfile(userId: string) {
|
async getProfile(userId: string) { return this.usersRepository.findProfileByUserId(userId); }
|
||||||
return this.usersRepository.findProfileByUserId(userId);
|
async updateProfile(userId: string, dto: any) { return this.usersRepository.updateProfile(userId, dto); }
|
||||||
|
async getProfileDetail(userId: string) { return this.usersRepository.findUserProfile(userId); }
|
||||||
|
async updateProfileDetail(userId: string, dto: any) { return this.usersRepository.upsertUserProfile(userId, dto); }
|
||||||
|
async updatePreferences(userId: string, dto: any) { return this.usersRepository.updatePreferences(userId, dto); }
|
||||||
|
|
||||||
|
// ── Membership ──
|
||||||
|
|
||||||
|
async getMembership(userId: string) {
|
||||||
|
return this.prisma.userMembership.findFirst({
|
||||||
|
where: { userId, active: true },
|
||||||
|
include: { plan: true },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfile(userId: string, dto: any) {
|
// ── Account Deletion ──
|
||||||
return this.usersRepository.updateProfile(userId, dto);
|
|
||||||
|
async requestDeletion(userId: string) {
|
||||||
|
const existing = await this.prisma.accountDeletionRequest.findFirst({
|
||||||
|
where: { userId, status: 'pending' },
|
||||||
|
});
|
||||||
|
if (existing) throw new BadRequestException('已有进行中的注销申请');
|
||||||
|
|
||||||
|
const coolingEndsAt = new Date(Date.now() + DELETION_COOLING_DAYS * 86400000);
|
||||||
|
return this.prisma.accountDeletionRequest.create({
|
||||||
|
data: { userId, coolingEndsAt },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProfileDetail(userId: string) {
|
async cancelDeletion(userId: string) {
|
||||||
return this.usersRepository.findUserProfile(userId);
|
const req = await this.prisma.accountDeletionRequest.findFirst({
|
||||||
|
where: { userId, status: 'pending' },
|
||||||
|
});
|
||||||
|
if (!req) throw new NotFoundException('未找到进行中的注销申请');
|
||||||
|
|
||||||
|
return this.prisma.accountDeletionRequest.update({
|
||||||
|
where: { id: req.id },
|
||||||
|
data: { status: 'cancelled', cancelledAt: new Date() },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfileDetail(userId: string, dto: any) {
|
// ── Device Management ──
|
||||||
return this.usersRepository.upsertUserProfile(userId, dto);
|
|
||||||
|
async getDevices(userId: string) {
|
||||||
|
return this.prisma.userDevice.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { lastSeenAt: 'desc' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePreferences(userId: string, dto: any) {
|
async removeDevice(userId: string, deviceId: string) {
|
||||||
return this.usersRepository.updatePreferences(userId, dto);
|
await this.prisma.userDevice.deleteMany({
|
||||||
|
where: { userId, deviceId },
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
test/m2.e2e-spec.ts
Normal file
95
test/m2.e2e-spec.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { AppModule } from '../src/app.module';
|
||||||
|
|
||||||
|
describe('M2 E2E Tests', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
app.setGlobalPrefix('api', { exclude: ['admin-api/(.*)', 'internal/(.*)'] });
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => { await app.close(); });
|
||||||
|
|
||||||
|
async function loginAdmin(): Promise<string> {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/admin-api/auth/login')
|
||||||
|
.send({ email: 'admin@zhixi.app', password: 'admin123' });
|
||||||
|
return res.body?.data?.accessToken || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// M2-01: User & Account 深化
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
describe('M2-01 User & Account Deepening', () => {
|
||||||
|
let token: string;
|
||||||
|
beforeAll(async () => { token = await loginAdmin(); });
|
||||||
|
|
||||||
|
// ── Admin membership management ──
|
||||||
|
|
||||||
|
it('GET /admin-api/users/memberships → 200 member list', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.get('/admin-api/users/memberships')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.expect(200);
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /admin-api/users/memberships → 200 assign membership', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/admin-api/users/memberships')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send({ userId: 'user1', planId: 'plan-free' })
|
||||||
|
.expect([200, 201]);
|
||||||
|
expect(res.body.data).toHaveProperty('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Admin deletion requests ──
|
||||||
|
|
||||||
|
it('GET /admin-api/users/deletion-requests → 200 list', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.get('/admin-api/users/deletion-requests')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.expect(200);
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /admin-api/users/deletion-requests/:id/approve → approve', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/admin-api/users/deletion-requests/test-id/approve')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.expect([200, 201]);
|
||||||
|
expect(res.body.data).toHaveProperty('status', 'completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /admin-api/users/deletion-requests/:id/reject → reject', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/admin-api/users/deletion-requests/test-id2/reject')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.expect([200, 201]);
|
||||||
|
expect(res.body.data).toHaveProperty('status', 'cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Admin device view ──
|
||||||
|
|
||||||
|
it('GET /admin-api/users/:userId/devices → 200 device list', async () => {
|
||||||
|
if (!token) return;
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.get('/admin-api/users/user1/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.expect(200);
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -90,7 +90,7 @@ const modelNames = [
|
|||||||
'contentSafetyCheck', 'contentReport', 'apiMetric', 'taskLog',
|
'contentSafetyCheck', 'contentReport', 'apiMetric', 'taskLog',
|
||||||
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
|
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
|
||||||
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
|
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
|
||||||
'violationRecord', 'contentReport',
|
'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest',
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const name of modelNames) {
|
for (const name of modelNames) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user