From 2ca386ca2037eef2e761be34e21f20a6e159500d Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Thu, 12 Mar 2026 15:27:41 +0800 Subject: [PATCH] update --- backend/docs/openapi.json | 1269 +++++++++++++++++ .../Permission/PermissionFlowTest.php | 177 +++ 2 files changed, 1446 insertions(+) create mode 100644 backend/docs/openapi.json diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json new file mode 100644 index 0000000..b90aa37 --- /dev/null +++ b/backend/docs/openapi.json @@ -0,0 +1,1269 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Datahub API", + "description": "Datahub API documentation", + "version": "1.0.0" + }, + "servers": [ + { + "url": "/api/v1", + "description": "API v1" + } + ], + "paths": { + "/register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "用户注册", + "description": "注册新用户,需提供用户名、密码和邮箱", + "operationId": "b099deca54ae9ddc3ee7e2261b6fc125", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "password", + "email" + ], + "properties": { + "username": { + "type": "string", + "example": "new_user", + "maxLength": 20, + "minLength": 3 + }, + "password": { + "type": "string", + "example": "Pass_1234", + "maxLength": 32, + "minLength": 6 + }, + "email": { + "type": "string", + "format": "email", + "example": "user@example.com", + "maxLength": 100 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "注册成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "注册成功" + }, + "data": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "new_user" + }, + "email": { + "type": "string", + "example": "user@example.com" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败或唯一性冲突", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "用户登录", + "description": "使用用户名和密码登录,返回 access_token 和 refresh_token", + "operationId": "383bcb1269d6dcce4609dc1f5d3ef129", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string", + "example": "admin" + }, + "password": { + "type": "string", + "example": "Pass_1234" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "登录成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "登录成功" + }, + "data": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "expires_in": { + "type": "integer", + "example": 7200 + }, + "user": { + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "用户名或密码错误", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "账号已被禁用", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/refresh": { + "get": { + "tags": [ + "Auth" + ], + "summary": "刷新 Access Token", + "description": "使用 refresh_token 获取新的 access_token,同时轮换 refresh_token", + "operationId": "fc18486b361cc4791acbafd8a2f25fff", + "parameters": [ + { + "name": "refresh_token", + "in": "query", + "description": "Refresh Token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Token 刷新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "Token 刷新成功" + }, + "data": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "expires_in": { + "type": "integer", + "example": 7200 + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "缺少 refresh_token 参数", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "refresh_token 无效或已过期", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "账号已被禁用", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "获取当前用户信息", + "description": "获取当前用户信息", + "operationId": "f03c8d46af839b7dbd0b659647cab574", + "responses": { + "200": { + "description": "获取成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "获取成功" + }, + "data": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "admin" + }, + "email": { + "type": "string", + "example": "admin@example.com" + }, + "status": { + "type": "integer", + "example": 1 + }, + "ext": { + "type": "object", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/me/profile": { + "put": { + "tags": [ + "Auth" + ], + "summary": "更新个人信息", + "description": "当前用户更新自己的 email 和 ext 字段", + "operationId": "01c6d8f425109cd3dfff51d1d69cd55c", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "new@example.com", + "maxLength": 100 + }, + "ext": { + "type": "object", + "example": { + "nickname": "user" + }, + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "个人信息更新成功" + }, + "data": { + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "ext": { + "type": "object", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败或唯一性冲突", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/me/password": { + "put": { + "tags": [ + "Auth" + ], + "summary": "修改密码", + "description": "修改当前用户密码,需验证旧密码。修改成功后清除 refresh_token,需重新登录", + "operationId": "c3ca37414997ba697e6c11173d9bc483", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "old_password", + "new_password" + ], + "properties": { + "old_password": { + "type": "string", + "example": "OldPass_1234" + }, + "new_password": { + "type": "string", + "example": "NewPass_5678", + "maxLength": 32, + "minLength": 6 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "密码修改成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "密码修改成功,请重新登录" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败或旧密码不正确", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/logout": { + "get": { + "tags": [ + "Auth" + ], + "summary": "退出登录", + "description": "退出登录,清除 refresh_token 并注销当前 JWT token", + "operationId": "dc5f3d60e870dd8b211f88cd97635158", + "responses": { + "200": { + "description": "退出成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "退出成功" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/users": { + "get": { + "tags": [ + "Users" + ], + "summary": "用户列表", + "description": "获取用户列表,支持分页、按 username/email 模糊搜索、按 status 精确筛选", + "operationId": "4f028975120b69092c0eae73bb36bcac", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 15, + "maximum": 100 + } + }, + { + "name": "username", + "in": "query", + "description": "用户名模糊搜索", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "email", + "in": "query", + "description": "邮箱模糊搜索", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "状态筛选(0=禁用,1=启用)", + "required": false, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + } + ], + "responses": { + "200": { + "description": "获取成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "获取成功" + }, + "data": { + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "total": { + "type": "integer", + "example": 100 + }, + "page": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 15 + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Users" + ], + "summary": "创建用户", + "description": "创建用户", + "operationId": "0b5969ce7b77ccaa0b2e9b551e302980", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "password", + "email" + ], + "properties": { + "username": { + "type": "string", + "example": "new_user", + "maxLength": 20, + "minLength": 3 + }, + "password": { + "type": "string", + "example": "Pass_1234", + "maxLength": 32, + "minLength": 6 + }, + "email": { + "type": "string", + "format": "email", + "example": "new@example.com", + "maxLength": 100 + }, + "status": { + "type": "integer", + "default": 1, + "enum": [ + 0, + 1 + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "创建成功" + }, + "data": { + "$ref": "#/components/schemas/User" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败或唯一性冲突", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/users/{id}": { + "get": { + "tags": [ + "Users" + ], + "summary": "用户详情", + "description": "用户详情", + "operationId": "b0770c8ad8eb11c5493fe9643c657673", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "用户 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "获取成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "获取成功" + }, + "data": { + "$ref": "#/components/schemas/User" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "用户不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": [ + "Users" + ], + "summary": "更新用户信息", + "description": "更新用户的 username、email 或 ext 字段,不支持修改密码", + "operationId": "c5ee224e653aac218896c93ecc3f9b67", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "用户 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "username": { + "type": "string", + "example": "updated_user", + "maxLength": 20, + "minLength": 3 + }, + "email": { + "type": "string", + "format": "email", + "example": "updated@example.com", + "maxLength": 100 + }, + "ext": { + "type": "object", + "example": { + "nickname": "Tester" + }, + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "更新成功" + }, + "data": { + "$ref": "#/components/schemas/User" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败或唯一性冲突", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "用户不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/users/{id}/status": { + "patch": { + "tags": [ + "Users" + ], + "summary": "更新用户状态", + "description": "启用或禁用用户", + "operationId": "c1ad72b12757d083cf0aebbf2f3787fd", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "用户 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "status" + ], + "properties": { + "status": { + "description": "0=禁用,1=启用", + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "状态更新成功", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "状态更新成功" + }, + "data": { + "$ref": "#/components/schemas/User" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "参数校验失败", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "未认证", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "用户不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "User": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "user_1234" + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "status": { + "type": "integer", + "example": 1 + }, + "ext": { + "type": "object", + "example": { + "nickname": "user" + }, + "nullable": true + }, + "refresh_token_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "type": "object" + }, + "ApiResponse": { + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "message": { + "type": "string", + "example": "success" + }, + "data": { + "type": "object", + "nullable": true + } + }, + "type": "object" + }, + "PaginatedData": { + "properties": { + "items": { + "type": "array", + "items": {} + }, + "total": { + "type": "integer", + "example": 0 + }, + "page": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 15 + } + }, + "type": "object" + }, + "ErrorResponse": { + "properties": { + "code": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "Bad request" + }, + "data": { + "type": "object", + "nullable": true + } + }, + "type": "object" + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "bearerFormat": "JWT", + "scheme": "bearer" + }, + "apiKeyAuth": { + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + } + } + }, + "tags": [ + { + "name": "Auth", + "description": "认证与个人信息" + }, + { + "name": "Users", + "description": "用户管理" + } + ] +} \ No newline at end of file diff --git a/backend/test/Cases/Integration/Permission/PermissionFlowTest.php b/backend/test/Cases/Integration/Permission/PermissionFlowTest.php index 101957d..8f0497d 100644 --- a/backend/test/Cases/Integration/Permission/PermissionFlowTest.php +++ b/backend/test/Cases/Integration/Permission/PermissionFlowTest.php @@ -223,4 +223,181 @@ class PermissionFlowTest extends TestCase $this->cleanupRouteGroup($user->role_id, $route, $auth_data); } } + + // ========== 边缘场景 ========== + + public function test_accessor_with_invalid_store_id_denied(): void + { + $user = $this->createTestUser('accessor'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 授权路由访问 + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => true, + ]); + + // 添加 store scope(store_id=1),然后传不在 bitmap 中的 store_id + $store = Store::query()->first(); + if (!$store) { + $this->markTestSkipped('stores 表中无数据'); + } + + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'store', + 'scope_id' => $store->id, + ]); + + try { + // 传不属于自己的 store_id → 403 + $response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user)); + $response->assertStatus(403); + } finally { + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + UserDataScope::query()->where('user_id', $user->id)->delete(); + } + } + + public function test_accessor_with_invalid_company_id_denied(): void + { + $user = $this->createTestUser('accessor'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => true, + ]); + + $store = Store::query()->first(); + if (!$store) { + $this->markTestSkipped('stores 表中无数据'); + } + + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'store', + 'scope_id' => $store->id, + ]); + + try { + // 传不属于自己的 company_id → 403 + $response = $this->get('/api/v1/users', ['company_id' => 999999], $this->authHeaders($user)); + $response->assertStatus(403); + } finally { + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + UserDataScope::query()->where('user_id', $user->id)->delete(); + } + } + + public function test_accessor_with_no_scope_data_gets_empty_results(): void + { + $user = $this->createTestUser('accessor'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 授权路由访问但不设置任何 scope 数据 + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => true, + ]); + + try { + // accessor 无 scope 数据 → bitmap 为空 → scope_ids=[] → 200 但无数据 + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + } finally { + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + } + } + + public function test_developer_with_invalid_store_id_denied(): void + { + $user = $this->createTestUser('developer'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + $auth_data = $this->authorizeRouteGroup($user->role_id, $route); + + try { + // developer 传不属于自己的 store_id → 403 + $response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user)); + $response->assertStatus(403); + } finally { + $this->cleanupRouteGroup($user->role_id, $route, $auth_data); + } + } + + public function test_developer_with_no_platforms_gets_empty_scope(): void + { + // 创建 developer 用户但不关联任何 platform + $user = $this->createTestUser('developer'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + $auth_data = $this->authorizeRouteGroup($user->role_id, $route); + + try { + // developer 无维护平台 → platform_ids=[] → scope_ids=[] → 200 但无数据 + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + } finally { + $this->cleanupRouteGroup($user->role_id, $route, $auth_data); + } + } + + public function test_ungrouped_route_denied_for_non_admin(): void + { + $user = $this->createTestUser('developer'); + + $route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first(); + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 确保路由未分组且无 override + $old_group_id = $route->group_id; + $route->group_id = null; + $route->save(); + + try { + // 路由已注册但未分组、无 override → 403 + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(403); + } finally { + $route->group_id = $old_group_id; + $route->save(); + } + } }