mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
721 lines
24 KiB
JavaScript
721 lines
24 KiB
JavaScript
![]() |
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(() => {
|
||
|
// Clean up environment variables
|
||
|
delete process.env.OPENID_GRAPH_SCOPES;
|
||
|
});
|
||
|
|
||
|
beforeEach(async () => {
|
||
|
jest.clearAllMocks();
|
||
|
await mongoose.connection.dropDatabase();
|
||
|
|
||
|
// Set up environment variable for People.Read scope
|
||
|
process.env.OPENID_GRAPH_SCOPES = 'User.Read,People.Read,Group.Read.All';
|
||
|
|
||
|
// Mock Graph client
|
||
|
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(),
|
||
|
};
|
||
|
|
||
|
Client.init.mockReturnValue(mockGraphClient);
|
||
|
|
||
|
// Mock tokens cache
|
||
|
mockTokensCache = {
|
||
|
get: jest.fn(),
|
||
|
set: jest.fn(),
|
||
|
};
|
||
|
getLogStores.mockReturnValue(mockTokensCache);
|
||
|
|
||
|
// Mock OpenID config
|
||
|
mockOpenIdConfig = {
|
||
|
client_id: 'test-client-id',
|
||
|
issuer: 'https://test-issuer.com',
|
||
|
};
|
||
|
getOpenIdConfig.mockReturnValue(mockOpenIdConfig);
|
||
|
|
||
|
// Mock openid-client (using the existing jest mock configuration)
|
||
|
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', () => {
|
||
|
// Reason: Ensure getOpenIdConfig returns expected structure
|
||
|
const config = getOpenIdConfig();
|
||
|
|
||
|
expect(config).toBeDefined();
|
||
|
expect(typeof config).toBe('object');
|
||
|
// Add specific property checks that GraphApiService depends on
|
||
|
expect(config).toHaveProperty('client_id');
|
||
|
expect(config).toHaveProperty('issuer');
|
||
|
|
||
|
// Ensure the function is callable
|
||
|
expect(typeof getOpenIdConfig).toBe('function');
|
||
|
});
|
||
|
|
||
|
it('should fail if openid-client.genericGrantRequest interface changes', () => {
|
||
|
// Reason: Ensure client.genericGrantRequest maintains expected signature
|
||
|
if (client.genericGrantRequest) {
|
||
|
expect(typeof client.genericGrantRequest).toBe('function');
|
||
|
|
||
|
// Test that it accepts the expected parameters
|
||
|
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', () => {
|
||
|
// Reason: Ensure Graph Client maintains expected fluent API
|
||
|
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', () => {
|
||
|
// Mock data used by multiple tests
|
||
|
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(() => {
|
||
|
// Reset mock call history for each test
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
// Re-apply the Client.init mock after clearAllMocks
|
||
|
Client.init.mockReturnValue(mockGraphClient);
|
||
|
|
||
|
// Re-apply openid-client mock
|
||
|
if (client.genericGrantRequest) {
|
||
|
client.genericGrantRequest.mockResolvedValue({
|
||
|
access_token: 'mocked-graph-token',
|
||
|
expires_in: 3600,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Re-apply cache mock
|
||
|
mockTokensCache.get.mockResolvedValue(null); // Force token exchange
|
||
|
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 () => {
|
||
|
// Mock responses for this specific test
|
||
|
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) // contacts call
|
||
|
.mockResolvedValueOnce(mockUsersResponse); // users call
|
||
|
|
||
|
const result = await GraphApiService.searchEntraIdPrincipals(
|
||
|
'token',
|
||
|
'user',
|
||
|
'john',
|
||
|
'users',
|
||
|
10,
|
||
|
);
|
||
|
|
||
|
// Should call contacts first with user filter
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people');
|
||
|
expect(mockGraphClient.search).toHaveBeenCalledWith('"john"');
|
||
|
expect(mockGraphClient.filter).toHaveBeenCalledWith(
|
||
|
"personType/subclass eq 'OrganizationUser'",
|
||
|
);
|
||
|
|
||
|
// Should call users endpoint for additional results
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/users');
|
||
|
expect(mockGraphClient.search).toHaveBeenCalledWith(
|
||
|
'"displayName:john" OR "userPrincipalName:john" OR "mail:john" OR "givenName:john" OR "surname:john"',
|
||
|
);
|
||
|
|
||
|
// Should return TPrincipalSearchResult array
|
||
|
expect(Array.isArray(result)).toBe(true);
|
||
|
expect(result).toHaveLength(2); // 1 from contacts + 1 from users
|
||
|
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 () => {
|
||
|
// Mock responses for this specific test - only groups endpoint called
|
||
|
mockGraphClient.get.mockResolvedValueOnce(mockGroupsResponse); // only groups call
|
||
|
|
||
|
const result = await GraphApiService.searchEntraIdPrincipals(
|
||
|
'token',
|
||
|
'user',
|
||
|
'team',
|
||
|
'groups',
|
||
|
10,
|
||
|
);
|
||
|
|
||
|
// Should NOT call contacts for groups type
|
||
|
expect(mockGraphClient.api).not.toHaveBeenCalledWith('/me/people');
|
||
|
|
||
|
// Should call groups endpoint only
|
||
|
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); // 1 from groups only
|
||
|
});
|
||
|
|
||
|
it('should search all endpoints for all type', async () => {
|
||
|
// Mock responses for this specific test
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce(mockContactsResponse) // contacts call (both user and group)
|
||
|
.mockResolvedValueOnce(mockUsersResponse) // users call
|
||
|
.mockResolvedValueOnce(mockGroupsResponse); // groups call
|
||
|
|
||
|
const result = await GraphApiService.searchEntraIdPrincipals(
|
||
|
'token',
|
||
|
'user',
|
||
|
'test',
|
||
|
'all',
|
||
|
10,
|
||
|
);
|
||
|
|
||
|
// Should call contacts first with user filter
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people');
|
||
|
expect(mockGraphClient.search).toHaveBeenCalledWith('"test"');
|
||
|
expect(mockGraphClient.filter).toHaveBeenCalledWith(
|
||
|
"personType/subclass eq 'OrganizationUser'",
|
||
|
);
|
||
|
|
||
|
// Should call both users and groups endpoints
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/users');
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/groups');
|
||
|
|
||
|
expect(Array.isArray(result)).toBe(true);
|
||
|
expect(result).toHaveLength(4); // 2 from contacts + 1 from users + 1 from groups
|
||
|
});
|
||
|
|
||
|
it('should early exit if contacts reach limit', async () => {
|
||
|
// Mock contacts to return exactly the limit
|
||
|
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,
|
||
|
);
|
||
|
|
||
|
// Should call contacts first
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/people');
|
||
|
expect(mockGraphClient.search).toHaveBeenCalledWith('"test"');
|
||
|
// Should not call users endpoint since limit was reached
|
||
|
expect(mockGraphClient.api).not.toHaveBeenCalledWith('/users');
|
||
|
|
||
|
expect(result).toHaveLength(10);
|
||
|
});
|
||
|
|
||
|
it('should deduplicate results based on idOnTheSource', async () => {
|
||
|
// Mock responses with duplicate IDs
|
||
|
const duplicateContactsResponse = {
|
||
|
value: [
|
||
|
{
|
||
|
id: 'duplicate-id',
|
||
|
displayName: 'John Doe',
|
||
|
mail: 'john@company.com',
|
||
|
personType: { class: 'Person', subclass: 'OrganizationUser' },
|
||
|
},
|
||
|
],
|
||
|
};
|
||
|
|
||
|
const duplicateUsersResponse = {
|
||
|
value: [
|
||
|
{
|
||
|
id: 'duplicate-id', // Same ID as contact
|
||
|
displayName: 'John Doe',
|
||
|
mail: 'john@company.com',
|
||
|
},
|
||
|
],
|
||
|
};
|
||
|
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce(duplicateContactsResponse)
|
||
|
.mockResolvedValueOnce(duplicateUsersResponse);
|
||
|
|
||
|
const result = await GraphApiService.searchEntraIdPrincipals(
|
||
|
'token',
|
||
|
'user',
|
||
|
'john',
|
||
|
'users',
|
||
|
10,
|
||
|
);
|
||
|
|
||
|
// Should only return one result despite duplicate IDs
|
||
|
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 from memberOf endpoint', async () => {
|
||
|
const mockGroupsResponse = {
|
||
|
value: [
|
||
|
{
|
||
|
id: 'group-1',
|
||
|
},
|
||
|
{
|
||
|
id: 'group-2',
|
||
|
},
|
||
|
],
|
||
|
};
|
||
|
|
||
|
mockGraphClient.get.mockResolvedValue(mockGroupsResponse);
|
||
|
|
||
|
const result = await GraphApiService.getUserEntraGroups('token', 'user');
|
||
|
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/memberOf');
|
||
|
expect(mockGraphClient.select).toHaveBeenCalledWith('id');
|
||
|
|
||
|
expect(result).toHaveLength(2);
|
||
|
expect(result).toEqual(['group-1', 'group-2']);
|
||
|
});
|
||
|
|
||
|
it('should return empty array on error', async () => {
|
||
|
mockGraphClient.get.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.get.mockResolvedValue(mockGroupsResponse);
|
||
|
|
||
|
const result = await GraphApiService.getUserEntraGroups('token', 'user');
|
||
|
|
||
|
expect(result).toEqual([]);
|
||
|
});
|
||
|
|
||
|
it('should handle missing value property', async () => {
|
||
|
mockGraphClient.get.mockResolvedValue({});
|
||
|
|
||
|
const result = await GraphApiService.getUserEntraGroups('token', 'user');
|
||
|
|
||
|
expect(result).toEqual([]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('testGraphApiAccess', () => {
|
||
|
beforeEach(() => {
|
||
|
jest.clearAllMocks();
|
||
|
});
|
||
|
|
||
|
it('should test all permissions and return success results', async () => {
|
||
|
// Mock successful responses for all tests
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) // /me test
|
||
|
.mockResolvedValueOnce({ value: [] }) // people OrganizationUser test
|
||
|
.mockResolvedValueOnce({ value: [] }) // people UnifiedGroup test
|
||
|
.mockResolvedValueOnce({ value: [] }) // /users endpoint test
|
||
|
.mockResolvedValueOnce({ value: [] }); // /groups endpoint test
|
||
|
|
||
|
const result = await GraphApiService.testGraphApiAccess('token', 'user');
|
||
|
|
||
|
expect(result).toEqual({
|
||
|
userAccess: true,
|
||
|
peopleAccess: true,
|
||
|
groupsAccess: true,
|
||
|
usersEndpointAccess: true,
|
||
|
groupsEndpointAccess: true,
|
||
|
errors: [],
|
||
|
});
|
||
|
|
||
|
// Verify all endpoints were tested
|
||
|
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 () => {
|
||
|
// Mock mixed success/failure responses
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) // /me success
|
||
|
.mockRejectedValueOnce(new Error('People access denied')) // people OrganizationUser fail
|
||
|
.mockResolvedValueOnce({ value: [] }) // people UnifiedGroup success
|
||
|
.mockRejectedValueOnce(new Error('Users endpoint access denied')) // /users fail
|
||
|
.mockResolvedValueOnce({ value: [] }); // /groups success
|
||
|
|
||
|
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 () => {
|
||
|
// Mock token exchange failure to test error handling
|
||
|
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 () => {
|
||
|
// Mock all requests to fail
|
||
|
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 () => {
|
||
|
// Mock successful responses for endpoint testing
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) // /me
|
||
|
.mockResolvedValueOnce({ value: [] }) // people OrganizationUser
|
||
|
.mockResolvedValueOnce({ value: [] }) // people UnifiedGroup
|
||
|
.mockResolvedValueOnce({ value: [] }) // /users
|
||
|
.mockResolvedValueOnce({ value: [] }); // /groups
|
||
|
|
||
|
await GraphApiService.testGraphApiAccess('token', 'user');
|
||
|
|
||
|
// Verify /users endpoint test
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/users');
|
||
|
expect(mockGraphClient.search).toHaveBeenCalledWith('"displayName:test"');
|
||
|
expect(mockGraphClient.select).toHaveBeenCalledWith('id,displayName,userPrincipalName');
|
||
|
|
||
|
// Verify /groups endpoint test
|
||
|
expect(mockGraphClient.api).toHaveBeenCalledWith('/groups');
|
||
|
expect(mockGraphClient.select).toHaveBeenCalledWith('id,displayName,mail');
|
||
|
});
|
||
|
|
||
|
it('should handle endpoint-specific permission failures', async () => {
|
||
|
// Mock specific endpoint failures
|
||
|
mockGraphClient.get
|
||
|
.mockResolvedValueOnce({ id: 'user-123', displayName: 'Test User' }) // /me success
|
||
|
.mockResolvedValueOnce({ value: [] }) // people OrganizationUser success
|
||
|
.mockResolvedValueOnce({ value: [] }) // people UnifiedGroup success
|
||
|
.mockRejectedValueOnce(new Error('Insufficient privileges')) // /users fail (User.Read.All needed)
|
||
|
.mockRejectedValueOnce(new Error('Access denied to groups')); // /groups fail (Group.Read.All needed)
|
||
|
|
||
|
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',
|
||
|
],
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|