tests/test_graph_client.py — Data Room Toolkit

From version 0.1.5 of Data Room Toolkit, shown publicly by its creator. 15.7 KiB · SHA-256 eee8363edc54e1b64cd609ff60d54a0fc0f2ae863ba3e80d092f3802690a1a8b.

# By Zero Width Lab (zerowidthlab.com)
from __future__ import annotations
import contextlib
import io
import json
import os
import sys
import tempfile
import unittest
import urllib.error
from email.message import Message
from pathlib import Path
from unittest.mock import patch
__author__ = "Zero Width Lab"
TEST_URL ='https://graph.microsoft.com/v1.0/test'
PACKAGE_ROOT =Path(__file__).resolve().parent.parent
sys.path.insert(0,str(PACKAGE_ROOT / 'scripts'))
import graph_client as gc
from graph_client import GraphClientError as GraphError

class PathAndPageTests(unittest.TestCase):

 def test_paths(self):
  bad =('..','.','a//b','//a','a//','//','C:a','a\\b','%2e%2e/a','a/%2f../b','a/%5cb','a\n','a/%00')
  for path in bad:
   with self.subTest(path=path),self.assertRaises(gc.ConfigError):
    gc.load_config(None,env={},overrides={'root_path':path},require_target=False)
  self.assertEqual(gc.rooted_remote_path('Room','a b/file'),'Room/a b/file')
  for root in ('Room','/Room','Room/','/Room/'):
   self.assertEqual(gc.load_config(None,env={},overrides={'root_path':root},require_target=False)['root_path'],'Room')
  with self.assertRaisesRegex(gc.ConfigError,'folder.*\\\\u200d.*file') as raised:
   gc.rooted_remote_path('Room','folder/\u200dfile')
  self.assertNotIn('\u200d',str(raised.exception))

 def test_pages(self):
  url ='https://graph.microsoft.com/v1.0/drives/d/root:/Room:/children'
  for payload in (None,[],{},{'value':{}},{'value':[None]},{'value':[7]},{'value':[],'@odata.nextLink':7},{'value':[],'@odata.nextLink':url.replace('Room','Outside')}):
   with self.subTest(payload=payload),self.assertRaises(GraphError):
    gc.collection_page(payload,url)
  self.assertEqual(gc.collection_page({'value':[],'@odata.nextLink':url + '?page=2'},url),([],url + '?page=2'))

 def test_bounded_pages_private_outputs(self):
  seen =set()
  gc.check_collection_url('page1',seen)
  with self.assertRaisesRegex(GraphError,'repeated'):
   gc.check_collection_url('page1',seen)
  with patch.object(gc,'MAX_COLLECTION_PAGES',1),self.assertRaisesRegex(GraphError,'exceeds 1 pages'):
   gc.check_collection_url('page2',seen)
  with tempfile.TemporaryDirectory() as folder:
   root =Path(folder).resolve()
   output =root / 'report'
   gc.write_new_text(output,'café')
   self.assertEqual(output.read_text(),'café')
   self.assertEqual(output.stat().st_mode & 511,384)
   (root / 'link').symlink_to(root,target_is_directory=True)
   (root / 'file-link').symlink_to(output)
   for path in (output,root / 'file-link',root / 'link' / 'new',root / 'missing' / 'new'):
    with self.subTest(path=path),self.assertRaisesRegex(GraphError,'cannot create new output'):
     gc.write_new_text(path,'replacement')
   self.assertEqual(output.read_text(),'café')
   with self.assertRaisesRegex(GraphError,'use the physical path.*private/tmp'):
    gc.write_new_text(root / 'link' / 'new','x')
   self.assertFalse((root / 'new').exists())
   self.assertFalse((root / 'missing').exists())

class FakeResponse:

 def __init__(self,status,payload=None,headers=None):
  self.status =status
  self.headers =headers or {}
  self._body =payload if isinstance(payload,bytes) else b'' if payload is None else json.dumps(payload).encode('utf-8')

 def read(self):
  return self._body

 def getcode(self):
  return self.status

 def __enter__(self):
  return self

 def __exit__(self,exc_type,exc,traceback):
  return None

def http_error(status,payload=None,headers=None):
 message_headers =Message()
 for key,value in (headers or {}).items():
  message_headers[key] =value
 body =json.dumps(payload or {'error':{'code':'fixture'}}).encode('utf-8')
 return urllib.error.HTTPError(TEST_URL,status,'fixture error',message_headers,io.BytesIO(body))

class GraphClientTests(unittest.TestCase):

 def setUp(self):
  gc._fake_path =None
  gc._fake_responses =None
  gc.reset_contacted_hosts()

 @contextlib.contextmanager
 def fake_transport(self,*responses):
  with tempfile.TemporaryDirectory() as temp_dir:
   path =Path(temp_dir) / 'responses.json'
   path.write_text(json.dumps({'responses':[({'method':'GET','url_prefix':TEST_URL,'status':200} | r) for r in responses]}),encoding='utf-8')
   with patch.dict(os.environ,{'GRAPH_FAKE_RESPONSES':str(path),gc._FAKE_TRANSPORT_ACK_VAR:gc._FAKE_TRANSPORT_ACK_VALUE}):
    yield

 credentials ={'GRAPH_TENANT_ID':'tenant-test','GRAPH_CLIENT_ID':'client-test','GRAPH_CLIENT_SECRET':'secret-test'}

 @patch('graph_client._open_url')
 def test_token_success(self,transport):
  transport.return_value =FakeResponse(200,{'access_token':'fixture-token'})
  token =gc.acquire_token(env=self.credentials)
  self.assertEqual(token,'fixture-token')
  request_object =transport.call_args.args[0]
  self.assertEqual(request_object.get_method(),'POST')
  self.assertIn('grant_type=client_credentials',request_object.data.decode())
  self.assertIn('scope=https%3A%2F%2Fgraph.microsoft.com%2F.default',request_object.data.decode())

  transport.side_effect =http_error(401)
  with self.assertRaisesRegex(GraphError,'token acquisition failed with HTTP 401') as raised:
   gc.acquire_token(env=self.credentials)
  self.assertEqual(raised.exception.status,401)
  self.assertNotIn('secret-test',str(raised.exception))

  with self.assertRaisesRegex(gc.CredentialError,'GRAPH_TENANT_ID'):
   gc.load_credentials({})

 @patch('graph_client.time.sleep')
 @patch('graph_client._open_url')
 def test_retries(self,transport,sleep):
  ok,ra0 =(FakeResponse(200,{'value':[]}),{'Retry-After':'0'})
  unknown =gc.WriteOutcomeUnknown
  last ={'error':{'code':'last'}}
  cases =[(False,4,[http_error(429,headers=ra0),ok],200,2,[0.0],{'value':[]}),*((False,1,[http_error(code),ok],200,2,[1.0],None) for code in (429,507,501)),(False,1,[http_error(503,headers=ra0),ok],200,2,[0.0],None),(False,4,[http_error(403,{'error':{'code':'denied'}})],403,1,[],{'error':{'code':'denied'}}),(False,2,[http_error(503),http_error(503),http_error(503,last)],503,3,[1.0,2.0],last),(True,4,[http_error(429,headers=ra0),FakeResponse(201,{})],201,2,[0.0],None),(True,4,[http_error(503,headers=ra0),ok],200,2,[0.0],None),*((True,4,[http_error(code),ok],unknown,1,[],None) for code in (500,503)),(True,4,[http_error(429,headers=ra0),http_error(409)],unknown,2,[0.0],None),(True,4,[http_error(412)],412,1,[],None),(True,4,[urllib.error.URLError('reset')],unknown,1,[],None),(True,4,[TimeoutError('timed out')],unknown,1,[],None)]
  url =TEST_URL
  for index,(write,retries,responses,expected,calls,sleeps,body) in enumerate(cases):
   with self.subTest(case=index):
    transport.reset_mock()
    sleep.reset_mock()
    transport.side_effect =responses
    method ='PUT' if write else 'GET'
    if expected is unknown:
     with self.assertRaisesRegex(unknown,'outcome unknown'):
      gc.request(method,url,max_retries=retries,write=write)
    else:
     status,payload,headers =gc.request(method,url,max_retries=retries,write=write)
     self.assertEqual(status,expected)
     if index ==0:
      self.assertEqual(headers,{})
     if body is not None:
      self.assertEqual(payload,body)
    self.assertEqual(transport.call_count,calls)
    self.assertEqual([c.args[0] for c in sleep.call_args_list],sleeps)

 def test_environment_proxy(self):
  import subprocess
  probe ="import graph_client as gc, urllib.request as u; print([h.proxies['https'] for h in gc._DESTINATION_OPENER.handlers if type(h) is u.ProxyHandler])"
  env ={k:v for k,v in os.environ.items() if 'proxy' not in k.lower()}
  env['HTTPS_PROXY'] ='http://127.0.0.1:9'
  result =subprocess.run([sys.executable,'-c',probe],cwd=PACKAGE_ROOT / 'scripts',env=env,capture_output=True,text=True,check=True,timeout=10)
  self.assertEqual(result.stdout.strip(),"['http://127.0.0.1:9']")

 def test_encoding(self):
  self.assertEqual(gc.encode_path_segments('folder name/hash#100%/plain'),'folder%20name/hash%23100%25/plain')
  self.assertEqual(gc.encode_path_segments(['folder/name','hash# percent% space']),'folder%2Fname/hash%23%20percent%25%20space')

 def test_config(self):
  with tempfile.TemporaryDirectory() as folder:
   path =Path(folder) / 'config.json'
   path.write_text(json.dumps({'drive_id':'config-drive','site_id':'config-site','root_path':'config-root','max_retries':2}))
   config =gc.load_config(path,overrides={'drive_id':'flag-drive','root_path':None,'max_retries':7},env={})
   for key,value in {'drive_id':'flag-drive','site_id':'config-site','root_path':'config-root','max_retries':7,'request_timeout_seconds':60}.items():
    self.assertEqual(config[key],value)
   self.assertEqual(gc.load_config(None,env={'SHAREPOINT_DATAROOM_CONFIG':str(path)})['drive_id'],'config-drive')
   path.write_text('{')
   for target,message in ((path,'invalid JSON'),(path.with_name('missing'),'config file not found'),(None,'neither drive_id nor site_id')):
    with self.subTest(target=target),self.assertRaisesRegex(gc.ConfigError,message):
     gc.load_config(target,env={})

 @patch('graph_client._open_url')
 def test_site_drive(self,transport):
  transport.return_value =FakeResponse(200,{'id':'resolved-drive'})
  resolved =gc.resolve_drive_id('fixture-token',site_id='site,value')
  self.assertEqual(resolved,'resolved-drive')
  request_object =transport.call_args.args[0]
  self.assertIn('/sites/site%2Cvalue/drive',request_object.full_url)
  self.assertEqual(request_object.get_header('Authorization'),'Bearer fixture-token')
  self.assertEqual(request_object.unredirected_hdrs['Authorization'],'Bearer fixture-token')

 def test_upload_boundary(self):
  threshold =4 * 1024 * 1024
  self.assertFalse(gc.uses_chunked_upload(threshold,threshold))
  self.assertTrue(gc.uses_chunked_upload(threshold + 1,threshold))
  self.assertEqual(gc.upload_method(threshold,threshold),'direct')
  self.assertEqual(gc.upload_method(threshold + 1,threshold),'chunked')

 def test_fake_retries(self):
  with self.fake_transport({'status':429,'headers':{'Retry-After':'0'},'once':True},{'json':{'value':[]}}),patch('graph_client.time.sleep') as sleep:
   status,payload,_ =gc.request('GET',TEST_URL,max_retries=1)
  self.assertEqual((status,payload),(200,{'value':[]}))
  sleep.assert_called_once_with(0.0)

 def test_fake_unmatched(self):
  with self.fake_transport(),self.assertRaisesRegex(GraphError,'no fake response matched GET https://graph.microsoft.com/v1.0/missing'):
   gc.request('GET','https://graph.microsoft.com/v1.0/missing')

 def test_host_before_transport(self):
  blocked ='https://blocked.example.invalid/'
  with self.fake_transport({'url_prefix':blocked,'json':{}}),self.assertRaisesRegex(GraphError,'request destination host is not allowed: blocked.example.invalid'):
   gc.request('GET',blocked)
  self.assertEqual(gc.contacted_hosts(),[])
  self.assertIsNone(gc._fake_responses)

 def test_tracked_hosts(self):
  cases =[('GET',TEST_URL,False,200),('POST','https://login.microsoftonline.com/tenant-placeholder/token',False,200),('PUT','https://contoso.sharepoint.com/upload-session',True,202)]
  for method,url,issued,expected in cases:
   with self.subTest(url=url),self.fake_transport({'method':method,'url_prefix':url,'status':expected,'json':{}}):
    self.assertEqual(gc.request(method,url,graph_issued_url=issued)[0],expected)
  self.assertEqual(gc.contacted_hosts(),['contoso.sharepoint.com','graph.microsoft.com','login.microsoftonline.com'])

 def test_tenant_hosts(self):
  for url in ('https://contoso.sharepoint.com/sites/room/_api/upload','https://contoso-my.sharepoint.com/personal/upload','https://CONTOSO.SharePoint.com/download','https://contoso.sharepoint.com:443/download','https://graph.microsoft.com/v1.0/drives/d/items/i/content'):
   with self.subTest(url=url):
    host =gc._guard_destination(url,graph_issued_url=True)
    self.assertTrue(gc.is_graph_issued_host(host))

 @patch('graph_client._open_url')
 def test_host_refusals(self,transport):
  rejected ={'https://sharepoint.com/upload':'not allowed','https://a.b.sharepoint.com/upload':'not allowed','https://contoso.sharepoint.com.evil.example/upload':'not allowed','https://contoso-sharepoint.com/upload':'not allowed','https://contososharepoint.com/upload':'not allowed','https://-contoso.sharepoint.com/upload':'not allowed','https://content.example.invalid/upload':'not allowed','https://login.microsoftonline.com/upload':'not allowed','http://contoso.sharepoint.com/upload':'must use HTTPS','https://user!@contoso.sharepoint.com/upload':'user information','https://user:pass!@contoso.sharepoint.com/upload':'user information','https://evil.example!@contoso.sharepoint.com/upload':'user information','https://contoso.sharepoint.com:8443/upload':'default HTTPS port','https://graph.microsoft.com:444/v1.0/x':'default HTTPS port'}
  for url,message in rejected.items():
   with self.subTest(url=url):
    with self.assertRaisesRegex(GraphError,message):
     gc.request('PUT',url,graph_issued_url=True)
  transport.assert_not_called()
  self.assertEqual(gc.contacted_hosts(),[])

 def test_unflagged_hosts(self):
  with self.assertRaisesRegex(GraphError,'request destination host is not allowed: contoso.sharepoint.com'):
   gc._guard_destination('https://contoso.sharepoint.com/x',graph_issued_url=False)
  with self.assertRaisesRegex(GraphError,'user information'):
   gc._guard_destination('https://user!@login.microsoftonline.com/x',graph_issued_url=False)

 def test_redirects(self):
  handler =gc._DestinationRedirectHandler()
  for flagged in (False,True):
   first =gc.urllib.request.Request(TEST_URL)
   setattr(first,'_zwl_graph_issued_url',flagged)
   for target in ('https://blocked.example.invalid/content','https://content.example.invalid/download','https://contoso.sharepoint.com.evil.example/download','http://contoso.sharepoint.com/download','https://user!@contoso.sharepoint.com/download','https://contoso.sharepoint.com:8443/download'):
    with self.subTest(flagged=flagged,target=target),self.assertRaises(GraphError):
     handler.redirect_request(first,None,302,'redirect',{},target)
   if flagged:
    second =handler.redirect_request(first,None,302,'redirect',{},'https://contoso.sharepoint.com/download')
    self.assertIsNotNone(second)
    self.assertTrue(getattr(second,'_zwl_graph_issued_url'))
    with self.assertRaisesRegex(GraphError,'not allowed'):
     handler.redirect_request(second,None,302,'redirect',{},'https://content.example.invalid/download')
    self.assertEqual(gc.contacted_hosts(),['contoso.sharepoint.com'])
   else:
    self.assertEqual(gc.contacted_hosts(),[])
    with self.assertRaises(GraphError):
     handler.redirect_request(first,None,302,'redirect',{},'https://contoso.sharepoint.com/download')

 def test_fake_matching(self):
  body ={'item':{'mode':'fail'}}
  no_match =(GraphError,'no fake')
  with self.fake_transport({'method':'PUT','match_headers':{'If-Match':'etag-1'},'json':{'matched':'header'}},{'method':'POST','match_absent_headers':['If-Match'],'match_json':body,'json':{'matched':'body'}}):
   _,payload,_ =gc.request('PUT',TEST_URL,headers={'if-match':'etag-1'})
   self.assertEqual(payload,{'matched':'header'})
   with self.assertRaisesRegex(*no_match):
    gc.request('PUT',TEST_URL)
   _,payload,_ =gc.request('POST',TEST_URL,json_body=body)
   self.assertEqual(payload,{'matched':'body'})
   with self.assertRaisesRegex(*no_match):
    gc.request('POST',TEST_URL,headers={'If-Match':'etag-1'},json_body=body)

 def test_https_required(self):
  with self.assertRaisesRegex(GraphError,'request destination URL must use HTTPS'):
   gc.request('GET','http://graph.microsoft.com/v1.0/test')

 def test_redaction(self):
  self.assertEqual(gc._safe_url('https://graph.microsoft.com/v1.0/drives/private-drive/items/private-item'),'https://graph.microsoft.com/v1.0/drives/<redacted>/items/<redacted>')
  self.assertEqual(gc._safe_url('https://login.microsoftonline.com/tenant-value/oauth2/v2.0/token'),'https://login.microsoftonline.com/<redacted>/oauth2/v2.0/token')
  self.assertEqual(gc._safe_url('https://upload.example.invalid/private-session?token=value'),'https://upload.example.invalid/<redacted>')
if __name__ =='__main__':
 unittest.main()

License: MIT (read it).

MITMIT License

Copyright (c) 2026 Zero Width Lab

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Back to the listing