1818import os
1919import sys
2020import time
21+
22+ import pytest
2123from datetime import datetime , timezone
2224from typing import Any , Callable , Dict , List , Optional
2325
9698 "metadata" ,
9799 # Comment fields
98100 "tagged_message" ,
101+ # Collections (only populated on Business/Enterprise tier accounts)
102+ "collections" ,
99103}
100104
101105
@@ -1899,7 +1903,7 @@ def run_file_tests(self) -> tuple[int, int]:
18991903 files = {
19001904 "file" : (version_filename , io .BytesIO (v2_content )),
19011905 },
1902- headers = {"Authorization" : f"Bearer { self .dev_token } " },
1906+ headers = {"Authorization" : f"Bearer { self .prod_headers [ 'Authorization' ]. split ()[ - 1 ] } " },
19031907 )
19041908
19051909 replica_version_resp = requests .post (
@@ -1978,7 +1982,7 @@ def run_file_tests(self) -> tuple[int, int]:
19781982 f"https://upload.box.com/api/2.0/files/{ prod_file_id } /content" ,
19791983 files = {"file" : (ifmatch_filename , io .BytesIO (content_v2 ))},
19801984 headers = {
1981- "Authorization" : f"Bearer { self .dev_token } " ,
1985+ "Authorization" : f"Bearer { self .prod_headers [ 'Authorization' ]. split ()[ - 1 ] } " ,
19821986 "If-Match" : prod_etag ,
19831987 },
19841988 )
@@ -2114,6 +2118,53 @@ def run_file_tests(self) -> tuple[int, int]:
21142118 except Exception as e :
21152119 print (f" { e } " )
21162120
2121+ # === POST /files/{id}/content (file version upload) ===
2122+
2123+ # [COMMON] Upload a new version of an existing file
2124+ total += 1
2125+ print (" POST /files/{id}/content (new version)..." , end = " " )
2126+ try :
2127+ ts = datetime .now (timezone .utc ).strftime ("%H%M%S%f" )
2128+ version_content = f"Updated content v2 at { ts } " .encode ("utf-8" )
2129+
2130+ prod_resp = requests .post (
2131+ f"https://upload.box.com/api/2.0/files/{ self .prod_file_id } /content" ,
2132+ files = {
2133+ "file" : (f"version_test_{ ts } .txt" , io .BytesIO (version_content ), "application/octet-stream" ),
2134+ },
2135+ headers = {"Authorization" : self .prod_headers ["Authorization" ]},
2136+ )
2137+ replica_resp = requests .post (
2138+ f"{ self .replica_url } /files/{ self .replica_file_id } /content" ,
2139+ files = {
2140+ "file" : (f"version_test_{ ts } .txt" , io .BytesIO (version_content ), "application/octet-stream" ),
2141+ },
2142+ )
2143+
2144+ prod_ok = prod_resp .status_code in (200 , 201 )
2145+ replica_ok = replica_resp .status_code in (200 , 201 )
2146+
2147+ if prod_ok and replica_ok :
2148+ prod_shape = self .extract_shape (prod_resp .json ())
2149+ replica_shape = self .extract_shape (replica_resp .json ())
2150+ diffs = self .compare_shapes (prod_shape , replica_shape , "data" )
2151+ if diffs :
2152+ print (" SCHEMA MISMATCH" )
2153+ for d in diffs [:2 ]:
2154+ print (f" { d } " )
2155+ else :
2156+ print ("✅" )
2157+ passed += 1
2158+ elif prod_ok == replica_ok :
2159+ print ("✅ (both failed)" )
2160+ passed += 1
2161+ else :
2162+ print (
2163+ f" STATUS: prod={ prod_resp .status_code } , replica={ replica_resp .status_code } "
2164+ )
2165+ except Exception as e :
2166+ print (f" { e } " )
2167+
21172168 return passed , total
21182169
21192170 def run_comment_tests (self ) -> tuple [int , int ]:
@@ -2188,9 +2239,8 @@ def run_comment_tests(self) -> tuple[int, int]:
21882239 else :
21892240 print ("✅" )
21902241 passed += 1
2191- # IDs available for potential follow-up tests:
2192- # prod_comment_id = prod_data.get("id")
2193- # replica_comment_id = replica_data.get("id")
2242+ prod_comment_id = prod_data .get ("id" )
2243+ replica_comment_id = replica_data .get ("id" )
21942244 else :
21952245 print (
21962246 f" STATUS: prod={ prod_resp .status_code } , replica={ replica_resp .status_code } "
@@ -2370,6 +2420,38 @@ def run_comment_tests(self) -> tuple[int, int]:
23702420 ):
23712421 passed += 1
23722422
2423+ # === DELETE /comments/{id} ===
2424+
2425+ # [COMMON] Delete a comment
2426+ if prod_comment_id and replica_comment_id :
2427+ total += 1
2428+ print (" DELETE /comments/{id}..." , end = " " )
2429+ try :
2430+ prod_resp = self .api_prod ("DELETE" , f"comments/{ prod_comment_id } " )
2431+ replica_resp = self .api_replica (
2432+ "DELETE" , f"comments/{ replica_comment_id } "
2433+ )
2434+ prod_ok = prod_resp .status_code in (200 , 204 )
2435+ replica_ok = replica_resp .status_code in (200 , 204 )
2436+ if prod_ok == replica_ok :
2437+ print ("✅" )
2438+ passed += 1
2439+ else :
2440+ print (
2441+ f" STATUS: prod={ prod_resp .status_code } , replica={ replica_resp .status_code } "
2442+ )
2443+ except Exception as e :
2444+ print (f" { e } " )
2445+
2446+ # [EDGE] Delete non-existent comment (404)
2447+ total += 1
2448+ if self .test_operation (
2449+ "DELETE /comments/{id} (non-existent - 404)" ,
2450+ lambda : self .api_prod ("DELETE" , "comments/999999999999999" ),
2451+ lambda : self .api_replica ("DELETE" , "comments/999999999999999" ),
2452+ ):
2453+ passed += 1
2454+
23732455 return passed , total
23742456
23752457 def run_task_tests (self ) -> tuple [int , int ]:
@@ -2396,6 +2478,8 @@ def run_task_tests(self) -> tuple[int, int]:
23962478 print ("\n ✅ Task Operations:" )
23972479 passed = 0
23982480 total = 0
2481+ prod_task_id = None
2482+ replica_task_id = None
23992483
24002484 if self .prod_file_id and self .replica_file_id :
24012485 # === POST /tasks ===
@@ -2445,6 +2529,8 @@ def run_task_tests(self) -> tuple[int, int]:
24452529 else :
24462530 print ("✅" )
24472531 passed += 1
2532+ prod_task_id = prod_data .get ("id" )
2533+ replica_task_id = replica_data .get ("id" )
24482534 else :
24492535 print (
24502536 f" STATUS: prod={ prod_resp .status_code } , replica={ replica_resp .status_code } "
@@ -2656,6 +2742,38 @@ def run_task_tests(self) -> tuple[int, int]:
26562742 ):
26572743 passed += 1
26582744
2745+ # === DELETE /tasks/{id} ===
2746+
2747+ # [COMMON] Delete a task
2748+ if prod_task_id and replica_task_id :
2749+ total += 1
2750+ print (" DELETE /tasks/{id}..." , end = " " )
2751+ try :
2752+ prod_resp = self .api_prod ("DELETE" , f"tasks/{ prod_task_id } " )
2753+ replica_resp = self .api_replica (
2754+ "DELETE" , f"tasks/{ replica_task_id } "
2755+ )
2756+ prod_ok = prod_resp .status_code in (200 , 204 )
2757+ replica_ok = replica_resp .status_code in (200 , 204 )
2758+ if prod_ok == replica_ok :
2759+ print ("✅" )
2760+ passed += 1
2761+ else :
2762+ print (
2763+ f" STATUS: prod={ prod_resp .status_code } , replica={ replica_resp .status_code } "
2764+ )
2765+ except Exception as e :
2766+ print (f" { e } " )
2767+
2768+ # [EDGE] Delete non-existent task (404)
2769+ total += 1
2770+ if self .test_operation (
2771+ "DELETE /tasks/{id} (non-existent - 404)" ,
2772+ lambda : self .api_prod ("DELETE" , "tasks/999999999999999" ),
2773+ lambda : self .api_replica ("DELETE" , "tasks/999999999999999" ),
2774+ ):
2775+ passed += 1
2776+
26592777 return passed , total
26602778
26612779 def run_hub_tests (self ) -> tuple [int , int ]:
@@ -4016,13 +4134,12 @@ def run_tests(self):
40164134# =============================================================================
40174135
40184136
4137+ @pytest .mark .conformance
4138+ @pytest .mark .external
40194139def test_box_parity ():
40204140 """Run Box parity tests as pytest test."""
40214141 if not BOX_DEV_TOKEN :
4022- print ("ERROR: BOX_DEV_TOKEN environment variable not set" )
4023- print ("Set it via: export BOX_DEV_TOKEN=<your_token>" )
4024- print ("Or edit the BOX_DEV_TOKEN constant in this file" )
4025- return
4142+ pytest .skip ("BOX_DEV_TOKEN environment variable not set" )
40264143
40274144 tester = BoxParityTester (BOX_DEV_TOKEN )
40284145 passed , total = tester .run_tests ()
0 commit comments