import { mockDeep } from 'jest-mock-extended'; import type { IExecuteFunctions, INode } from 'n8n-workflow'; import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import { msGraphSecurityApiRequest, tolerateDoubleQuotes, throwOnEmptyUpdate, } from '../GenericFunctions'; describe('Microsoft GraphSecurity GenericFunctions', () => { let mockExecuteFunctions: jest.Mocked; let mockNode: INode; let mockRequest: jest.Mock; beforeEach(() => { mockExecuteFunctions = mockDeep(); mockRequest = jest.fn(); mockExecuteFunctions.helpers.request = mockRequest; mockNode = { id: 'test-node', name: 'Test GraphSecurity Node', type: 'n8n-nodes-base.microsoftGraphSecurity', typeVersion: 1, position: [0, 0], parameters: {}, }; mockExecuteFunctions.getNode.mockReturnValue(mockNode); jest.clearAllMocks(); }); afterEach(() => { jest.resetAllMocks(); }); describe('msGraphSecurityApiRequest', () => { const mockCredentials = { oauthTokenData: { access_token: 'test-access-token', }, }; beforeEach(() => { mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials); }); describe('successful requests', () => { it('should make a successful GET request with default parameters', async () => { const mockResponse = { data: 'test data' }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); expect(result).toEqual(mockResponse); }); it('should make a POST request with body data', async () => { const mockResponse = { id: '123', status: 'created' }; const requestBody = { name: 'Test Alert', status: 'active' }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'POST', '/alerts', requestBody, ); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'POST', body: requestBody, uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); expect(result).toEqual(mockResponse); }); it('should make a request with query string parameters', async () => { const mockResponse = { alerts: [] }; const queryParams = { $filter: "status eq 'active'", $top: 10 }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'GET', '/alerts', {}, queryParams, ); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', qs: queryParams, uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); expect(result).toEqual(mockResponse); }); it('should make a request with custom headers', async () => { const mockResponse = { success: true }; const customHeaders = { 'Content-Type': 'application/json', 'X-Custom-Header': 'test' }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'PUT', '/secureScores', { data: 'test' }, {}, customHeaders, ); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', 'Content-Type': 'application/json', 'X-Custom-Header': 'test', }, method: 'PUT', body: { data: 'test' }, uri: 'https://graph.microsoft.com/v1.0/security/secureScores', json: true, }); expect(result).toEqual(mockResponse); }); it('should handle all parameters together', async () => { const mockResponse = { updated: true }; const body = { status: 'resolved' }; const qs = { $select: 'id,status' }; const headers = { 'If-Match': 'etag-value' }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'PATCH', '/alerts/123', body, qs, headers, ); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', 'If-Match': 'etag-value', }, method: 'PATCH', body, qs, uri: 'https://graph.microsoft.com/v1.0/security/alerts/123', json: true, }); expect(result).toEqual(mockResponse); }); it('should remove empty body when no body data is provided', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts', {}); const requestOptions = mockRequest.mock.calls[0][0]; expect(requestOptions.body).toBeUndefined(); }); it('should remove empty query string when no qs data is provided', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts', {}, {}); const requestOptions = mockRequest.mock.calls[0][0]; expect(requestOptions.qs).toBeUndefined(); }); }); describe('credential handling', () => { it('should handle missing credentials', async () => { mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('Credentials not found')); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow('Credentials not found'); }); it('should handle malformed credentials', async () => { mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: {}, } as any); mockRequest.mockResolvedValue({ data: 'test' }); const result = await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer undefined', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); expect(result).toEqual({ data: 'test' }); }); }); describe('error handling', () => { it('should handle basic API errors', async () => { const apiError = { error: { error: { message: 'Resource not found', code: 'NotFound', }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts/invalid-id'), ).rejects.toThrow(NodeApiError); }); it('should parse JSON error messages', async () => { const jsonErrorMessage = '{"error":{"code":"InvalidRequest","message":"Invalid request"}}'; const apiError = { error: { error: { message: jsonErrorMessage, }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); }); it('should handle BadRequest errors', async () => { const apiError = { error: { error: { message: 'Http request failed with statusCode=BadRequest: Invalid filter', }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); expect(apiError.error.error.message).toBe('Request failed with bad request'); }); it('should handle Http request failed errors with JSON content', async () => { const jsonError = '{"error":{"code":"Forbidden","message":"Insufficient privileges"}}'; const apiError = { error: { error: { message: `Http request failed with statusCode=403: ${jsonError}`, }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); }); it('should handle Invalid filter clause errors', async () => { const apiError = { error: { error: { message: 'Invalid filter clause', }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); expect(apiError.error.error.message).toContain( 'Please check that your query parameter syntax is correct', ); }); it('should handle Invalid ODATA query filter errors', async () => { const apiError = { error: { error: { message: 'Invalid ODATA query filter', }, }, }; mockRequest.mockRejectedValue(apiError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); expect(apiError.error.error.message).toContain( 'Please check that your query parameter syntax is correct', ); }); it('should handle errors without nested structure', async () => { const simpleError = { error: { error: { message: 'Simple network error', }, }, }; mockRequest.mockRejectedValue(simpleError); await expect( msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'), ).rejects.toThrow(NodeApiError); }); }); describe('endpoint construction', () => { it('should construct correct URL for different endpoints', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/secureScores'); expect(mockRequest).toHaveBeenCalledWith( expect.objectContaining({ uri: 'https://graph.microsoft.com/v1.0/security/secureScores', }), ); }); it('should handle endpoints with parameters', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts/123/comments'); expect(mockRequest).toHaveBeenCalledWith( expect.objectContaining({ uri: 'https://graph.microsoft.com/v1.0/security/alerts/123/comments', }), ); }); it('should handle endpoints without leading slash', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', 'alerts'); expect(mockRequest).toHaveBeenCalledWith( expect.objectContaining({ uri: 'https://graph.microsoft.com/v1.0/securityalerts', }), ); }); }); describe('graphApiBaseUrl from credentials', () => { it('should use base URL from credentials', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://graph.microsoft.us', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.us/v1.0/security/alerts', json: true, }); }); it('should fall back to default when credentials.graphApiBaseUrl is empty', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: '', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); }); it('should fall back to default when credentials.graphApiBaseUrl is undefined', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); }); it('should strip trailing slashes from base URL using regex', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://graph.microsoft.com/', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); }); it('should strip multiple trailing slashes from base URL', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://graph.microsoft.com///', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.com/v1.0/security/alerts', json: true, }); }); it('should use US Government cloud endpoint', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://graph.microsoft.us', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://graph.microsoft.us/v1.0/security/alerts', json: true, }); }); it('should use US Government DOD cloud endpoint', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://dod-graph.microsoft.us', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://dod-graph.microsoft.us/v1.0/security/alerts', json: true, }); }); it('should use China cloud endpoint', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, graphApiBaseUrl: 'https://microsoftgraph.chinacloudapi.cn', }); await msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts'); expect(mockRequest).toHaveBeenCalledWith({ headers: { Authorization: 'Bearer test-access-token', }, method: 'GET', uri: 'https://microsoftgraph.chinacloudapi.cn/v1.0/security/alerts', json: true, }); }); }); }); describe('tolerateDoubleQuotes', () => { it('should replace double quotes with single quotes', () => { const input = 'status eq "active" and severity eq "high"'; const expected = "status eq 'active' and severity eq 'high'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle multiple double quotes', () => { const input = '"test" and "another" and "third"'; const expected = "'test' and 'another' and 'third'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle empty string', () => { const input = ''; const expected = ''; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle string with no double quotes', () => { const input = 'status eq active and severity eq high'; const expected = 'status eq active and severity eq high'; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle string with only single quotes', () => { const input = "status eq 'active' and severity eq 'high'"; const expected = "status eq 'active' and severity eq 'high'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle mixed quotes', () => { const input = 'status eq "active" and name eq \'test\' and type eq "alert"'; const expected = "status eq 'active' and name eq 'test' and type eq 'alert'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle escaped quotes', () => { const input = 'description eq "He said \\"hello\\""'; const expected = "description eq 'He said \\'hello\\''"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle special characters within quotes', () => { const input = 'title eq "Alert: SQL Injection @#$%^&*()"'; const expected = "title eq 'Alert: SQL Injection @#$%^&*()'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); it('should handle very long strings', () => { const longString = '"' + 'a'.repeat(1000) + '"'; const expectedString = "'" + 'a'.repeat(1000) + "'"; const result = tolerateDoubleQuotes(longString); expect(result).toEqual(expectedString); }); it('should handle unicode characters', () => { const input = 'title eq "Alert: 测试 🚨 данные"'; const expected = "title eq 'Alert: 测试 🚨 данные'"; const result = tolerateDoubleQuotes(input); expect(result).toEqual(expected); }); }); describe('throwOnEmptyUpdate', () => { it('should throw NodeOperationError with correct message', () => { expect(() => { throwOnEmptyUpdate.call(mockExecuteFunctions); }).toThrow(NodeOperationError); }); it('should throw with expected error message', () => { expect(() => { throwOnEmptyUpdate.call(mockExecuteFunctions); }).toThrow('Please enter at least one field to update'); }); it('should use the correct node context', () => { try { throwOnEmptyUpdate.call(mockExecuteFunctions); } catch (error) { expect(mockExecuteFunctions.getNode).toHaveBeenCalled(); } }); it('should always throw regardless of input', () => { expect(() => { throwOnEmptyUpdate.call(mockExecuteFunctions); }).toThrow(); }); }); describe('Edge Cases and Integration', () => { beforeEach(() => { mockExecuteFunctions.getCredentials.mockResolvedValue({ oauthTokenData: { access_token: 'test-access-token', }, }); }); it('should handle concurrent requests', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); const promises: Array> = []; for (let i = 0; i < 5; i++) { promises.push(msGraphSecurityApiRequest.call(mockExecuteFunctions, 'GET', '/alerts/' + i)); } const results = await Promise.all(promises); expect(results).toHaveLength(5); expect(mockRequest).toHaveBeenCalledTimes(5); }); it('should handle extremely large request bodies', async () => { const mockResponse = { success: true }; const largeBody = { data: 'x'.repeat(10000) }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'POST', '/alerts', largeBody, ); expect(result).toEqual(mockResponse); expect(mockRequest).toHaveBeenCalledWith( expect.objectContaining({ body: largeBody, }), ); }); it('should handle empty parameters gracefully', async () => { const mockResponse = { data: 'test' }; mockRequest.mockResolvedValue(mockResponse); const result = await msGraphSecurityApiRequest.call( mockExecuteFunctions, 'GET', '/alerts', {}, {}, {}, ); expect(result).toEqual(mockResponse); }); }); });