| jest.mock('@microsoft/microsoft-graph-client'); |
| jest.mock('~/strategies/openidStrategy'); |
| jest.mock('~/cache/getLogStores'); |
| jest.mock('@librechat/data-schemas', () => ({ |
| ...jest.requireActual('@librechat/data-schemas'), |
| logger: { |
| error: jest.fn(), |
| debug: jest.fn(), |
| }, |
| })); |
| jest.mock('~/config', () => ({ |
| logger: { |
| error: jest.fn(), |
| debug: jest.fn(), |
| }, |
| createAxiosInstance: jest.fn(() => ({ |
| create: jest.fn(), |
| defaults: {}, |
| })), |
| })); |
| jest.mock('~/utils', () => ({ |
| logAxiosError: jest.fn(), |
| })); |
|
|
| jest.mock('~/server/services/Config', () => ({})); |
| jest.mock('~/server/services/Files/strategies', () => ({ |
| getStrategyFunctions: jest.fn(), |
| })); |
|
|
| const mongoose = require('mongoose'); |
| const client = require('openid-client'); |
| const { MongoMemoryServer } = require('mongodb-memory-server'); |
| const { Client } = require('@microsoft/microsoft-graph-client'); |
| const { getOpenIdConfig } = require('~/strategies/openidStrategy'); |
| const getLogStores = require('~/cache/getLogStores'); |
| const GraphApiService = require('./GraphApiService'); |
|
|
| describe('GraphApiService', () => { |
| let mongoServer; |
| let mockGraphClient; |
| let mockTokensCache; |
| let mockOpenIdConfig; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| await mongoose.connect(mongoUri); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| afterEach(() => { |
| |
| delete process.env.OPENID_GRAPH_SCOPES; |
| }); |
|
|
| beforeEach(async () => { |
| jest.clearAllMocks(); |
| await mongoose.connection.dropDatabase(); |
|
|
| |
| process.env.OPENID_GRAPH_SCOPES = 'User.Read,People.Read,Group.Read.All'; |
|
|
| |
| mockGraphClient = { |
| api: jest.fn().mockReturnThis(), |
| search: jest.fn().mockReturnThis(), |
| filter: jest.fn().mockReturnThis(), |
| select: jest.fn().mockReturnThis(), |
| header: jest.fn().mockReturnThis(), |
| top: jest.fn().mockReturnThis(), |
| get: jest.fn(), |
| post: jest.fn(), |
| }; |
|
|
| Client.init.mockReturnValue(mockGraphClient); |
|
|
| |
| mockTokensCache = { |
| get: jest.fn(), |
| set: jest.fn(), |
| }; |
| getLogStores.mockReturnValue(mockTokensCache); |
|
|
| |
| mockOpenIdConfig = { |
| client_id: 'test-client-id', |
| issuer: 'https://test-issuer.com', |
| }; |
| getOpenIdConfig.mockReturnValue(mockOpenIdConfig); |
|
|
| |
| if (client.genericGrantRequest) { |
| client.genericGrantRequest.mockResolvedValue({ |
| access_token: 'mocked-graph-token', |
| expires_in: 3600, |
| }); |
| } |
| }); |
|
|
| describe('Dependency Contract Tests', () => { |
| it('should fail if getOpenIdConfig interface changes', () => { |
| |
| const config = getOpenIdConfig(); |
|
|
| expect(config).toBeDefined(); |
| expect(typeof config).toBe('object'); |
| |
| expect(config).toHaveProperty('client_id'); |
| expect(config).toHaveProperty('issuer'); |
|
|
| |
| expect(typeof getOpenIdConfig).toBe('function'); |
| }); |
|
|
| it('should fail if openid-client.genericGrantRequest interface changes', () => { |
| |
| if (client.genericGrantRequest) { |
| expect(typeof client.genericGrantRequest).toBe('function'); |
|
|
| |
| const mockCall = client.genericGrantRequest( |
| mockOpenIdConfig, |
| 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
| { |
| scope: 'test-scope', |
| assertion: 'test-token', |
| requested_token_use: 'on_behalf_of', |
| }, |
| ); |
|
|
| expect(mockCall).toBeDefined(); |
| } |
| }); |
|
|
| it('should fail if Microsoft Graph Client interface changes', () => { |
| |
| expect(typeof Client.init).toBe('function'); |
|
|
| const client = Client.init({ authProvider: jest.fn() }); |
| expect(client).toHaveProperty('api'); |
| expect(typeof client.api).toBe('function'); |
| }); |
| }); |
|
|
| describe('createGraphClient', () => { |
| it('should create graph client with exchanged token', async () => { |
| const accessToken = 'test-access-token'; |
| const sub = 'test-user-id'; |
|
|
| const result = await GraphApiService.createGraphClient(accessToken, sub); |
|
|
| expect(getOpenIdConfig).toHaveBeenCalled(); |
| expect(Client.init).toHaveBeenCalledWith({ |
| authProvider: expect.any(Function), |
| }); |
| expect(result).toBe(mockGraphClient); |
| }); |
|
|
| it('should handle token exchange errors gracefully', async () => { |
| if (client.genericGrantRequest) { |
| client.genericGrantRequest.mockRejectedValue(new Error('Token exchange failed')); |
| } |
|
|
| await expect(GraphApiService.createGraphClient('invalid-token', 'test-user')).rejects.toThrow( |
| 'Token exchange failed', |
| ); |
| }); |
| }); |
|
|
| describe('exchangeTokenForGraphAccess', () => { |
| it('should return cached token if available', async () => { |
| const cachedToken = { access_token: 'cached-token' }; |
| mockTokensCache.get.mockResolvedValue(cachedToken); |
|
|
| const result = await GraphApiService.exchangeTokenForGraphAccess( |
| mockOpenIdConfig, |
| 'test-token', |
| 'test-user', |
| ); |
|
|
| expect(result).toBe('cached-token'); |
| expect(mockTokensCache.get).toHaveBeenCalledWith('test-user:graph'); |
| if (client.genericGrantRequest) { |
| expect(client.genericGrantRequest).not.toHaveBeenCalled(); |
| } |
| }); |
|
|
| it('should exchange token and cache result', async () => { |
| mockTokensCache.get.mockResolvedValue(null); |
|
|
| const result = await GraphApiService.exchangeTokenForGraphAccess( |
| mockOpenIdConfig, |
| 'test-token', |
| 'test-user', |
| ); |
|
|
| if (client.genericGrantRequest) { |
| expect(client.genericGrantRequest).toHaveBeenCalledWith( |
| mockOpenIdConfig, |
| 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
| { |
| scope: |
| 'https://graph.microsoft.com/User.Read https://graph.microsoft.com/People.Read https://graph.microsoft.com/Group.Read.All', |
| assertion: 'test-token', |
| requested_token_use: 'on_behalf_of', |
| }, |
| ); |
| } |
|
|
| expect(mockTokensCache.set).toHaveBeenCalledWith( |
| 'test-user:graph', |
| { access_token: 'mocked-graph-token' }, |
| 3600000, |
| ); |
|
|
| expect(result).toBe('mocked-graph-token'); |
| }); |
|
|
| it('should use custom scopes from environment', async () => { |
| const originalEnv = process.env.OPENID_GRAPH_SCOPES; |
| process.env.OPENID_GRAPH_SCOPES = 'Custom.Read,Custom.Write'; |
|
|
| mockTokensCache.get.mockResolvedValue(null); |
|
|
| await GraphApiService.exchangeTokenForGraphAccess( |
| mockOpenIdConfig, |
| 'test-token', |
| 'test-user', |
| ); |
|
|
| if (client.genericGrantRequest) { |
| expect(client.genericGrantRequest).toHaveBeenCalledWith( |
| mockOpenIdConfig, |
| 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
| { |
| scope: |
| 'https://graph.microsoft.com/Custom.Read https://graph.microsoft.com/Custom.Write', |
| assertion: 'test-token', |
| requested_token_use: 'on_behalf_of', |
| }, |
| ); |
| } |
|
|
| process.env.OPENID_GRAPH_SCOPES = originalEnv; |
| }); |
| }); |
|
|
| describe('searchEntraIdPrincipals', () => { |
| |
| const mockContactsResponse = { |
| value: [ |
| { |
| id: 'contact-user-1', |
| displayName: 'John Doe', |
| userPrincipalName: 'john@company.com', |
| mail: 'john@company.com', |
| personType: { class: 'Person', subclass: 'OrganizationUser' }, |
| scoredEmailAddresses: [{ address: 'john@company.com', relevanceScore: 0.9 }], |
| }, |
| { |
| id: 'contact-group-1', |
| displayName: 'Marketing Team', |
| mail: 'marketing@company.com', |
| personType: { class: 'Group', subclass: 'UnifiedGroup' }, |
| scoredEmailAddresses: [{ address: 'marketing@company.com', relevanceScore: 0.8 }], |
| }, |
| ], |
| }; |
|
|
| const mockUsersResponse = { |
| value: [ |
| { |
| id: 'dir-user-1', |
| displayName: 'Jane Smith', |
| userPrincipalName: 'jane@company.com', |
| mail: 'jane@company.com', |
| }, |
| ], |
| }; |
|
|
| const mockGroupsResponse = { |
| value: [ |
| { |
| id: 'dir-group-1', |
| displayName: 'Development Team', |
| mail: 'dev@company.com', |
| }, |
| ], |
| }; |
|
|
| beforeEach(() => { |
| |
| jest.clearAllMocks(); |
|
|
| |
| Client.init.mockReturnValue(mockGraphClient); |
|
|
| |
| if (client.genericGrantRequest) { |
| client.genericGrantRequest.mockResolvedValue({ |
| access_token: 'mocked-graph-token', |
| expires_in: 3600, |
| }); |
| } |
|
|
| |
| mockTokensCache.get.mockResolvedValue(null); |
| mockTokensCache.set.mockResolvedValue(); |
| getLogStores.mockReturnValue(mockTokensCache); |
| getOpenIdConfig.mockReturnValue(mockOpenIdConfig); |
| }); |
|
|
| it('should return empty results for short queries', async () => { |
| const result = await GraphApiService.searchEntraIdPrincipals('token', 'user', 'a', 'all', 10); |
|
|
| expect(result).toEqual([]); |
| expect(mockGraphClient.api).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should search contacts first and additional users for users type', async () => { |
| |
| const contactsFilteredResponse = { |
| value: [ |
| { |
| id: 'contact-user-1', |
| displayName: 'John Doe', |
| userPrincipalName: 'john@company.com', |
| mail: 'john@company.com', |
| personType: { class: 'Person', subclass: 'OrganizationUser' }, |
| scoredEmailAddresses: [{ address: 'john@company.com', relevanceScore: 0.9 }], |
| }, |
| ], |
| }; |
|
|
| mockGraphClient.get |
| .mockResolvedValueOnce(contactsFilteredResponse) |
| .mockResolvedValueOnce(mockUsersResponse); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'john', |
| 'users', |
| 10, |
| ); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith('"john"'); |
| expect(mockGraphClient.filter).toHaveBeenCalledWith( |
| "personType/subclass eq 'OrganizationUser'", |
| ); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/users'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith( |
| '"displayName:john" OR "userPrincipalName:john" OR "mail:john" OR "givenName:john" OR "surname:john"', |
| ); |
|
|
| |
| expect(Array.isArray(result)).toBe(true); |
| expect(result).toHaveLength(2); |
| expect(result[0]).toMatchObject({ |
| id: null, |
| type: 'user', |
| name: 'John Doe', |
| email: 'john@company.com', |
| source: 'entra', |
| idOnTheSource: 'contact-user-1', |
| }); |
| }); |
|
|
| it('should search groups endpoint only for groups type', async () => { |
| |
| mockGraphClient.get.mockResolvedValueOnce(mockGroupsResponse); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'team', |
| 'groups', |
| 10, |
| ); |
|
|
| |
| expect(mockGraphClient.api).not.toHaveBeenCalledWith('/me/people'); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/groups'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith( |
| '"displayName:team" OR "mail:team" OR "mailNickname:team"', |
| ); |
|
|
| expect(Array.isArray(result)).toBe(true); |
| expect(result).toHaveLength(1); |
| }); |
|
|
| it('should search all endpoints for all type', async () => { |
| |
| mockGraphClient.get |
| .mockResolvedValueOnce(mockContactsResponse) |
| .mockResolvedValueOnce(mockUsersResponse) |
| .mockResolvedValueOnce(mockGroupsResponse); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'test', |
| 'all', |
| 10, |
| ); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith('"test"'); |
| expect(mockGraphClient.filter).toHaveBeenCalledWith( |
| "personType/subclass eq 'OrganizationUser'", |
| ); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/users'); |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/groups'); |
|
|
| expect(Array.isArray(result)).toBe(true); |
| expect(result).toHaveLength(4); |
| }); |
|
|
| it('should early exit if contacts reach limit', async () => { |
| |
| const limitedContactsResponse = { |
| value: Array(10).fill({ |
| id: 'contact-1', |
| displayName: 'Contact User', |
| mail: 'contact@company.com', |
| personType: { class: 'Person', subclass: 'OrganizationUser' }, |
| }), |
| }; |
|
|
| mockGraphClient.get.mockResolvedValueOnce(limitedContactsResponse); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'test', |
| 'all', |
| 10, |
| ); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith('"test"'); |
| |
| expect(mockGraphClient.api).not.toHaveBeenCalledWith('/users'); |
|
|
| expect(result).toHaveLength(10); |
| }); |
|
|
| it('should deduplicate results based on idOnTheSource', async () => { |
| |
| const duplicateContactsResponse = { |
| value: [ |
| { |
| id: 'duplicate-id', |
| displayName: 'John Doe', |
| mail: 'john@company.com', |
| personType: { class: 'Person', subclass: 'OrganizationUser' }, |
| }, |
| ], |
| }; |
|
|
| const duplicateUsersResponse = { |
| value: [ |
| { |
| id: 'duplicate-id', |
| displayName: 'John Doe', |
| mail: 'john@company.com', |
| }, |
| ], |
| }; |
|
|
| mockGraphClient.get |
| .mockResolvedValueOnce(duplicateContactsResponse) |
| .mockResolvedValueOnce(duplicateUsersResponse); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'john', |
| 'users', |
| 10, |
| ); |
|
|
| |
| expect(result).toHaveLength(1); |
| expect(result[0].idOnTheSource).toBe('duplicate-id'); |
| }); |
|
|
| it('should handle Graph API errors gracefully', async () => { |
| mockGraphClient.get.mockRejectedValue(new Error('Graph API error')); |
|
|
| const result = await GraphApiService.searchEntraIdPrincipals( |
| 'token', |
| 'user', |
| 'test', |
| 'all', |
| 10, |
| ); |
|
|
| expect(result).toEqual([]); |
| }); |
| }); |
|
|
| describe('getUserEntraGroups', () => { |
| it('should fetch user groups using getMemberGroups endpoint', async () => { |
| const mockGroupsResponse = { |
| value: ['group-1', 'group-2'], |
| }; |
|
|
| mockGraphClient.post.mockResolvedValue(mockGroupsResponse); |
|
|
| const result = await GraphApiService.getUserEntraGroups('token', 'user'); |
|
|
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me/getMemberGroups'); |
| expect(mockGraphClient.post).toHaveBeenCalledWith({ securityEnabledOnly: false }); |
|
|
| expect(result).toEqual(['group-1', 'group-2']); |
| }); |
|
|
| it('should deduplicate returned group ids', async () => { |
| mockGraphClient.post.mockResolvedValue({ |
| value: ['group-1', 'group-2', 'group-1'], |
| }); |
|
|
| const result = await GraphApiService.getUserEntraGroups('token', 'user'); |
|
|
| expect(result).toEqual(['group-1', 'group-2']); |
| }); |
|
|
| it('should return empty array on error', async () => { |
| mockGraphClient.post.mockRejectedValue(new Error('API error')); |
|
|
| const result = await GraphApiService.getUserEntraGroups('token', 'user'); |
|
|
| expect(result).toEqual([]); |
| }); |
|
|
| it('should handle empty response', async () => { |
| const mockGroupsResponse = { |
| value: [], |
| }; |
|
|
| mockGraphClient.post.mockResolvedValue(mockGroupsResponse); |
|
|
| const result = await GraphApiService.getUserEntraGroups('token', 'user'); |
|
|
| expect(result).toEqual([]); |
| }); |
|
|
| it('should handle missing value property', async () => { |
| mockGraphClient.post.mockResolvedValue({}); |
|
|
| const result = await GraphApiService.getUserEntraGroups('token', 'user'); |
|
|
| expect(result).toEqual([]); |
| }); |
| }); |
|
|
| describe('getUserOwnedEntraGroups', () => { |
| it('should fetch owned groups with pagination support', async () => { |
| const firstPage = { |
| value: [ |
| { |
| id: 'owned-group-1', |
| }, |
| ], |
| '@odata.nextLink': |
| 'https://graph.microsoft.com/v1.0/me/ownedObjects/microsoft.graph.group?$skiptoken=xyz', |
| }; |
|
|
| const secondPage = { |
| value: [ |
| { |
| id: 'owned-group-2', |
| }, |
| ], |
| }; |
|
|
| mockGraphClient.get.mockResolvedValueOnce(firstPage).mockResolvedValueOnce(secondPage); |
|
|
| const result = await GraphApiService.getUserOwnedEntraGroups('token', 'user'); |
|
|
| expect(mockGraphClient.api).toHaveBeenNthCalledWith( |
| 1, |
| '/me/ownedObjects/microsoft.graph.group', |
| ); |
| expect(mockGraphClient.api).toHaveBeenNthCalledWith( |
| 2, |
| '/me/ownedObjects/microsoft.graph.group?$skiptoken=xyz', |
| ); |
| expect(mockGraphClient.top).toHaveBeenCalledWith(999); |
| expect(mockGraphClient.get).toHaveBeenCalledTimes(2); |
|
|
| expect(result).toEqual(['owned-group-1', 'owned-group-2']); |
| }); |
|
|
| it('should return empty array on error', async () => { |
| mockGraphClient.get.mockRejectedValue(new Error('API error')); |
|
|
| const result = await GraphApiService.getUserOwnedEntraGroups('token', 'user'); |
|
|
| expect(result).toEqual([]); |
| }); |
| }); |
|
|
| describe('getGroupMembers', () => { |
| it('should fetch transitive members and include only users', async () => { |
| const firstPage = { |
| value: [ |
| { id: 'user-1', '@odata.type': '#microsoft.graph.user' }, |
| { id: 'child-group', '@odata.type': '#microsoft.graph.group' }, |
| ], |
| '@odata.nextLink': |
| 'https://graph.microsoft.com/v1.0/groups/group-id/transitiveMembers?$skiptoken=abc', |
| }; |
| const secondPage = { |
| value: [{ id: 'user-2', '@odata.type': '#microsoft.graph.user' }], |
| }; |
|
|
| mockGraphClient.get.mockResolvedValueOnce(firstPage).mockResolvedValueOnce(secondPage); |
|
|
| const result = await GraphApiService.getGroupMembers('token', 'user', 'group-id'); |
|
|
| expect(mockGraphClient.api).toHaveBeenNthCalledWith(1, '/groups/group-id/transitiveMembers'); |
| expect(mockGraphClient.api).toHaveBeenNthCalledWith( |
| 2, |
| '/groups/group-id/transitiveMembers?$skiptoken=abc', |
| ); |
| expect(mockGraphClient.top).toHaveBeenCalledWith(999); |
| expect(result).toEqual(['user-1', 'user-2']); |
| }); |
|
|
| it('should return empty array on error', async () => { |
| mockGraphClient.get.mockRejectedValue(new Error('API error')); |
|
|
| const result = await GraphApiService.getGroupMembers('token', 'user', 'group-id'); |
|
|
| expect(result).toEqual([]); |
| }); |
| }); |
|
|
| describe('testGraphApiAccess', () => { |
| beforeEach(() => { |
| jest.clearAllMocks(); |
| }); |
|
|
| it('should test all permissions and return success results', async () => { |
| |
| mockGraphClient.get |
| .mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }); |
|
|
| const result = await GraphApiService.testGraphApiAccess('token', 'user'); |
|
|
| expect(result).toEqual({ |
| userAccess: true, |
| peopleAccess: true, |
| groupsAccess: true, |
| usersEndpointAccess: true, |
| groupsEndpointAccess: true, |
| errors: [], |
| }); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me'); |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people'); |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/users'); |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/groups'); |
| expect(mockGraphClient.filter).toHaveBeenCalledWith( |
| "personType/subclass eq 'OrganizationUser'", |
| ); |
| expect(mockGraphClient.filter).toHaveBeenCalledWith("personType/subclass eq 'UnifiedGroup'"); |
| expect(mockGraphClient.search).toHaveBeenCalledWith('"displayName:test"'); |
| }); |
|
|
| it('should handle partial failures and record errors', async () => { |
| |
| mockGraphClient.get |
| .mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) |
| .mockRejectedValueOnce(new Error('People access denied')) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockRejectedValueOnce(new Error('Users endpoint access denied')) |
| .mockResolvedValueOnce({ value: [] }); |
|
|
| const result = await GraphApiService.testGraphApiAccess('token', 'user'); |
|
|
| expect(result).toEqual({ |
| userAccess: true, |
| peopleAccess: false, |
| groupsAccess: true, |
| usersEndpointAccess: false, |
| groupsEndpointAccess: true, |
| errors: [ |
| 'People.Read (OrganizationUser): People access denied', |
| 'Users endpoint: Users endpoint access denied', |
| ], |
| }); |
| }); |
|
|
| it('should handle complete Graph client creation failure', async () => { |
| |
| if (client.genericGrantRequest) { |
| client.genericGrantRequest.mockRejectedValue(new Error('Token exchange failed')); |
| } |
|
|
| const result = await GraphApiService.testGraphApiAccess('invalid-token', 'user'); |
|
|
| expect(result).toEqual({ |
| userAccess: false, |
| peopleAccess: false, |
| groupsAccess: false, |
| usersEndpointAccess: false, |
| groupsEndpointAccess: false, |
| errors: ['Token exchange failed'], |
| }); |
| }); |
|
|
| it('should record all permission errors', async () => { |
| |
| mockGraphClient.get |
| .mockRejectedValueOnce(new Error('User.Read denied')) |
| .mockRejectedValueOnce(new Error('People.Read OrganizationUser denied')) |
| .mockRejectedValueOnce(new Error('People.Read UnifiedGroup denied')) |
| .mockRejectedValueOnce(new Error('Users directory access denied')) |
| .mockRejectedValueOnce(new Error('Groups directory access denied')); |
|
|
| const result = await GraphApiService.testGraphApiAccess('token', 'user'); |
|
|
| expect(result).toEqual({ |
| userAccess: false, |
| peopleAccess: false, |
| groupsAccess: false, |
| usersEndpointAccess: false, |
| groupsEndpointAccess: false, |
| errors: [ |
| 'User.Read: User.Read denied', |
| 'People.Read (OrganizationUser): People.Read OrganizationUser denied', |
| 'People.Read (UnifiedGroup): People.Read UnifiedGroup denied', |
| 'Users endpoint: Users directory access denied', |
| 'Groups endpoint: Groups directory access denied', |
| ], |
| }); |
| }); |
|
|
| it('should test new endpoints with correct search patterns', async () => { |
| |
| mockGraphClient.get |
| .mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }); |
|
|
| await GraphApiService.testGraphApiAccess('token', 'user'); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/users'); |
| expect(mockGraphClient.search).toHaveBeenCalledWith('"displayName:test"'); |
| expect(mockGraphClient.select).toHaveBeenCalledWith('id,displayName,userPrincipalName'); |
|
|
| |
| expect(mockGraphClient.api).toHaveBeenCalledWith('/groups'); |
| expect(mockGraphClient.select).toHaveBeenCalledWith('id,displayName,mail'); |
| }); |
|
|
| it('should handle endpoint-specific permission failures', async () => { |
| |
| mockGraphClient.get |
| .mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockResolvedValueOnce({ value: [] }) |
| .mockRejectedValueOnce(new Error('Insufficient privileges')) |
| .mockRejectedValueOnce(new Error('Access denied to groups')); |
|
|
| const result = await GraphApiService.testGraphApiAccess('token', 'user'); |
|
|
| expect(result).toEqual({ |
| userAccess: true, |
| peopleAccess: true, |
| groupsAccess: true, |
| usersEndpointAccess: false, |
| groupsEndpointAccess: false, |
| errors: [ |
| 'Users endpoint: Insufficient privileges', |
| 'Groups endpoint: Access denied to groups', |
| ], |
| }); |
| }); |
| }); |
| }); |
|
|