diff --git a/auth/auth.go b/auth/auth.go index a343d2e..2239e73 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -125,9 +125,8 @@ func init() { VBaseAuth.AddRole(RoleCodeAdmin, "管理员", "*:*") VBaseAuth.AddRole(RoleCodeUser, "普通用户", - "user:read", - "org:read", "org:create", + "org:read", "oauth-client:read", "oauth-client:create", "oauth-client:update", diff --git a/tests/oauth_client_test.go b/tests/oauth_client_test.go new file mode 100644 index 0000000..279ffa9 --- /dev/null +++ b/tests/oauth_client_test.go @@ -0,0 +1,120 @@ +package tests + +import ( + "testing" +) + +// OAuthClientResp OAuth 客户端响应 +type OAuthClientResp struct { + ID string `json:"id"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret,omitempty"` + Name string `json:"name"` + RedirectURIs string `json:"redirect_uris"` + AllowedScopes string `json:"allowed_scopes"` +} + +// Test OAuth Client CRUD +func TestOAuthClientCRUD(t *testing.T) { + ensureUsers(t) + + var clientID string // This is ClientID (string), not ID (UUID) + + // Test 1: List OAuth Clients + t.Run("List OAuth Clients", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/oauth/clients", nil, AdminToken) + assertStatus(t, resp, 200) + + var data struct { + Items []OAuthClientResp `json:"items"` + } + decodeResponse(t, resp, &data) + t.Logf("Total OAuth clients: %d", len(data.Items)) + }) + + // Test 2: Create OAuth Client + t.Run("Create OAuth Client", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/oauth/clients", map[string]interface{}{ + "name": "Test OAuth Client", + "redirect_uris": []string{"https://example.com/callback"}, + "allowed_scopes": "openid profile email", + }, AdminToken) + assertStatus(t, resp, 200) + + var data OAuthClientResp + decodeResponse(t, resp, &data) + clientID = data.ClientID // Use ClientID, not ID + t.Logf("Created OAuth client: %s (ID: %s)", clientID, data.ID) + }) + + if clientID == "" { + t.Fatal("Failed to create OAuth client, skipping remaining tests") + } + + // Test 3: Get OAuth Client Details + t.Run("Get OAuth Client Details", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/oauth/clients/"+clientID, nil, AdminToken) + assertStatus(t, resp, 200) + + var data OAuthClientResp + decodeResponse(t, resp, &data) + + if data.Name != "Test OAuth Client" { + t.Errorf("Expected name 'Test OAuth Client', got '%s'", data.Name) + } + }) + + // Test 4: Update OAuth Client + t.Run("Update OAuth Client", func(t *testing.T) { + resp := doRequest(t, "PATCH", "/api/oauth/clients/"+clientID, map[string]string{ + "name": "Updated OAuth Client", + }, AdminToken) + assertStatus(t, resp, 200) + + // Verify update + resp = doRequest(t, "GET", "/api/oauth/clients/"+clientID, nil, AdminToken) + assertStatus(t, resp, 200) + + var data OAuthClientResp + decodeResponse(t, resp, &data) + + if data.Name != "Updated OAuth Client" { + t.Errorf("Expected name 'Updated OAuth Client', got '%s'", data.Name) + } + }) + + // Test 5: Delete OAuth Client + t.Run("Delete OAuth Client", func(t *testing.T) { + resp := doRequest(t, "DELETE", "/api/oauth/clients/"+clientID, nil, AdminToken) + assertStatus(t, resp, 200) + + // Verify deletion + resp = doRequest(t, "GET", "/api/oauth/clients/"+clientID, nil, AdminToken) + if resp.Code == 200 { + t.Errorf("Expected client to be deleted, but got 200") + } else { + t.Logf("Client deleted successfully, got code: %d", resp.Code) + } + }) +} + +// Test regular user OAuth client access +func TestOAuthClientAccessControl(t *testing.T) { + ensureUsers(t) + + // Regular user should be able to list OAuth clients (oauth-client:read) + t.Run("Regular User List Clients", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/oauth/clients", nil, User1Token) + assertStatus(t, resp, 200) + }) + + // Regular user should be able to create OAuth clients (oauth-client:create) + t.Run("Regular User Create Client", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/oauth/clients", map[string]interface{}{ + "name": "User OAuth Client", + "redirect_uris": []string{"https://example.com/callback"}, + "allowed_scopes": "openid profile email", + }, User1Token) + assertStatus(t, resp, 200) + }) +} diff --git a/tests/org_crud_test.go b/tests/org_crud_test.go new file mode 100644 index 0000000..b8a9610 --- /dev/null +++ b/tests/org_crud_test.go @@ -0,0 +1,223 @@ +package tests + +import ( + "testing" +) + +func TestOrgCRUD(t *testing.T) { + ensureUsers(t) + + var orgID string + + // Test 1: Create Organization + t.Run("Create Organization", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/orgs", map[string]string{ + "code": "test_org_crud", + "name": "Test Org CRUD", + "description": "Organization for CRUD tests", + }, User1Token) + + assertStatus(t, resp, 200) + + var data struct { + ID string `json:"id"` + } + decodeResponse(t, resp, &data) + orgID = data.ID + t.Logf("Created org: %s", orgID) + }) + + if orgID == "" { + t.Fatal("Failed to create org, skipping remaining tests") + } + + // Test 2: List Organizations + t.Run("List Organizations", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs", nil, User1Token) + assertStatus(t, resp, 200) + + var data struct { + Items []OrgResp `json:"items"` + Total int64 `json:"total"` + } + decodeResponse(t, resp, &data) + t.Logf("Total orgs: %d", data.Total) + + if data.Total <= 0 { + t.Errorf("Expected at least 1 org, got %d", data.Total) + } + }) + + // Test 3: Get Org Details + t.Run("Get Org Details", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs/"+orgID, nil, User1Token) + assertStatus(t, resp, 200) + + var data OrgResp + decodeResponse(t, resp, &data) + + if data.Name != "Test Org CRUD" { + t.Errorf("Expected name 'Test Org CRUD', got '%s'", data.Name) + } + if data.Code != "test_org_crud" { + t.Errorf("Expected code 'test_org_crud', got '%s'", data.Code) + } + }) + + // Test 4: Update Organization + t.Run("Update Organization", func(t *testing.T) { + resp := doRequest(t, "PATCH", "/api/orgs/"+orgID, map[string]string{ + "name": "Updated Org Name", + "description": "Updated description", + }, User1Token) + assertStatus(t, resp, 200) + + // Verify update + resp = doRequest(t, "GET", "/api/orgs/"+orgID, nil, User1Token) + assertStatus(t, resp, 200) + + var data OrgResp + decodeResponse(t, resp, &data) + + if data.Name != "Updated Org Name" { + t.Errorf("Expected name 'Updated Org Name', got '%s'", data.Name) + } + }) + + // Test 5: Get Org Members + t.Run("Get Org Members", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs/"+orgID+"/members", nil, User1Token) + assertStatus(t, resp, 200) + + var data struct { + Items []struct { + UserID string `json:"user_id"` + Role string `json:"role"` + } `json:"items"` + } + decodeResponse(t, resp, &data) + t.Logf("Org members count: %d", len(data.Items)) + }) + + // Test 6: Delete Organization (as owner) + t.Run("Delete Organization", func(t *testing.T) { + resp := doRequest(t, "DELETE", "/api/orgs/"+orgID, nil, User1Token) + assertStatus(t, resp, 200) + + // Verify deletion + resp = doRequest(t, "GET", "/api/orgs/"+orgID, nil, User1Token) + if resp.Code == 200 { + t.Errorf("Expected org to be deleted, but got 200") + } else { + t.Logf("Org deleted successfully, got code: %d", resp.Code) + } + }) + + // Test 7: List after deletion + t.Run("List After Delete", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs", nil, User1Token) + assertStatus(t, resp, 200) + + var data struct { + Total int64 `json:"total"` + } + decodeResponse(t, resp, &data) + t.Logf("Total orgs after delete: %d", data.Total) + }) +} + +// Test Org Tree +func TestOrgTree(t *testing.T) { + ensureUsers(t) + + // Create parent org + var parentOrgID string + t.Run("Create Parent Org", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/orgs", map[string]string{ + "code": "parent_org", + "name": "Parent Organization", + "description": "Parent org for tree test", + }, User1Token) + assertStatus(t, resp, 200) + + var data struct { + ID string `json:"id"` + } + decodeResponse(t, resp, &data) + parentOrgID = data.ID + }) + + if parentOrgID == "" { + t.Fatal("Failed to create parent org") + } + + // Get org tree + t.Run("Get Org Tree", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs/tree", nil, User1Token) + assertStatus(t, resp, 200) + + // Tree endpoint returns array directly + var data []OrgResp + decodeResponse(t, resp, &data) + t.Logf("Org tree items: %d", len(data)) + }) +} + +// Test non-member access +func TestOrgAccessControl(t *testing.T) { + ensureUsers(t) + + // User1 creates org + var orgID string + t.Run("User1 Creates Org", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/orgs", map[string]string{ + "code": "private_org", + "name": "Private Organization", + "description": "Private org for access test", + }, User1Token) + assertStatus(t, resp, 200) + + var data struct { + ID string `json:"id"` + } + decodeResponse(t, resp, &data) + orgID = data.ID + }) + + if orgID == "" { + t.Fatal("Failed to create org") + } + + // User2 (non-member) tries to access + t.Run("Non-member Get Org", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs/"+orgID, nil, User2Token) + // Should fail with 403 + if resp.Code == 200 { + t.Errorf("Expected non-member to be denied, got 200") + } else { + t.Logf("Non-member correctly denied access, code: %d", resp.Code) + } + }) + + t.Run("Non-member Update Org", func(t *testing.T) { + resp := doRequest(t, "PATCH", "/api/orgs/"+orgID, map[string]string{ + "name": "Hacked", + }, User2Token) + // Should fail with 403 + if resp.Code == 200 { + t.Errorf("Expected non-member to be denied, got 200") + } else { + t.Logf("Non-member correctly denied update, code: %d", resp.Code) + } + }) + + t.Run("Non-member Get Members", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/orgs/"+orgID+"/members", nil, User2Token) + // Should fail with 403 + if resp.Code == 200 { + t.Errorf("Expected non-member to be denied, got 200") + } else { + t.Logf("Non-member correctly denied members access, code: %d", resp.Code) + } + }) +} diff --git a/tests/role_test.go b/tests/role_test.go new file mode 100644 index 0000000..afdda57 --- /dev/null +++ b/tests/role_test.go @@ -0,0 +1,204 @@ +package tests + +import ( + "testing" +) + +func TestRoleCRUD(t *testing.T) { + ensureUsers(t) + + var roleID string + + // Test 1: List Roles (as admin) + t.Run("List Roles", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/roles", nil, AdminToken) + assertStatus(t, resp, 200) + + var data struct { + Items []struct { + ID string `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + } `json:"items"` + } + decodeResponse(t, resp, &data) + t.Logf("Total roles: %d", len(data.Items)) + + if len(data.Items) == 0 { + t.Errorf("Expected some roles, got 0") + } + }) + + // Test 2: Create Role + t.Run("Create Role", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/roles", map[string]string{ + "code": "test_role", + "name": "Test Role", + "description": "Role created for testing", + }, AdminToken) + assertStatus(t, resp, 200) + + var data struct { + ID string `json:"id"` + } + decodeResponse(t, resp, &data) + roleID = data.ID + t.Logf("Created role: %s", roleID) + }) + + if roleID == "" { + t.Fatal("Failed to create role, skipping remaining tests") + } + + // Test 3: Get Role Details + t.Run("Get Role Details", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/roles/"+roleID, nil, AdminToken) + assertStatus(t, resp, 200) + + var data struct { + Code string `json:"code"` + Name string `json:"name"` + } + decodeResponse(t, resp, &data) + + if data.Code != "test_role" { + t.Errorf("Expected code 'test_role', got '%s'", data.Code) + } + if data.Name != "Test Role" { + t.Errorf("Expected name 'Test Role', got '%s'", data.Name) + } + }) + + // Test 4: Update Role + t.Run("Update Role", func(t *testing.T) { + resp := doRequest(t, "PATCH", "/api/roles/"+roleID, map[string]string{ + "name": "Updated Test Role", + "description": "Updated description", + }, AdminToken) + assertStatus(t, resp, 200) + + // Verify update + resp = doRequest(t, "GET", "/api/roles/"+roleID, nil, AdminToken) + assertStatus(t, resp, 200) + + var data struct { + Name string `json:"name"` + } + decodeResponse(t, resp, &data) + + if data.Name != "Updated Test Role" { + t.Errorf("Expected name 'Updated Test Role', got '%s'", data.Name) + } + }) + + // Test 5: Get Role Permissions + t.Run("Get Role Permissions", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/roles/"+roleID+"/permissions", nil, AdminToken) + assertStatus(t, resp, 200) + + // Returns array directly + var data []struct { + ID string `json:"id"` + } + decodeResponse(t, resp, &data) + t.Logf("Role permissions count: %d", len(data)) + }) + + // Test 6: Update Role Permissions + t.Run("Update Role Permissions", func(t *testing.T) { + // First, get available permissions + resp := doRequest(t, "GET", "/api/roles", nil, AdminToken) + assertStatus(t, resp, 200) + + // Try to update with an empty permission list first + resp = doRequest(t, "PUT", "/api/roles/"+roleID+"/permissions", map[string]interface{}{ + "permission_ids": []string{}, + }, AdminToken) + assertStatus(t, resp, 200) + t.Logf("Updated role permissions to empty") + }) + + // Test 7: Delete Role + t.Run("Delete Role", func(t *testing.T) { + resp := doRequest(t, "DELETE", "/api/roles/"+roleID, nil, AdminToken) + assertStatus(t, resp, 200) + + // Verify deletion + resp = doRequest(t, "GET", "/api/roles/"+roleID, nil, AdminToken) + if resp.Code == 200 { + t.Errorf("Expected role to be deleted, but got 200") + } else { + t.Logf("Role deleted successfully, got code: %d", resp.Code) + } + }) +} + +// Test role access control +func TestRoleAccessControl(t *testing.T) { + ensureUsers(t) + + // Regular user tries to access role endpoints - should fail (admin only) + t.Run("Regular User List Roles", func(t *testing.T) { + resp := doRequest(t, "GET", "/api/roles", nil, User1Token) + // Should fail - role:read requires admin permission + if resp.Code == 200 { + t.Errorf("Expected regular user to be denied, got 200") + } else { + t.Logf("Regular user correctly denied list roles, code: %d", resp.Code) + } + }) + + t.Run("Regular User Create Role", func(t *testing.T) { + resp := doRequest(t, "POST", "/api/roles", map[string]string{ + "code": "illegal_role", + "name": "Should Fail", + "description": "Should not be created", + }, User1Token) + // Should fail - needs role:create permission + if resp.Code == 200 { + t.Errorf("Expected regular user to be denied, got 200") + } else { + t.Logf("Regular user correctly denied create role, code: %d", resp.Code) + } + }) +} + +// Test system role protection +func TestSystemRoleProtection(t *testing.T) { + ensureUsers(t) + + // Try to modify system role (admin) + t.Run("Update System Role", func(t *testing.T) { + resp := doRequest(t, "PATCH", "/api/roles/admin", map[string]string{ + "name": "Hacked Admin", + }, AdminToken) + // Should fail - system roles are protected + if resp.Code == 200 { + t.Errorf("Expected system role update to be denied, got 200") + } else { + t.Logf("System role correctly protected, code: %d", resp.Code) + } + }) + + t.Run("Update System Role Permissions", func(t *testing.T) { + resp := doRequest(t, "PUT", "/api/roles/admin/permissions", map[string]interface{}{ + "permission_ids": []string{}, + }, AdminToken) + // Should fail - system roles are protected + if resp.Code == 200 { + t.Errorf("Expected system role permissions update to be denied, got 200") + } else { + t.Logf("System role permissions correctly protected, code: %d", resp.Code) + } + }) + + t.Run("Delete System Role", func(t *testing.T) { + resp := doRequest(t, "DELETE", "/api/roles/admin", nil, AdminToken) + // Should fail - system roles cannot be deleted + if resp.Code == 200 { + t.Errorf("Expected system role deletion to be denied, got 200") + } else { + t.Logf("System role deletion correctly protected, code: %d", resp.Code) + } + }) +}