@@ -60,6 +60,21 @@ async function docxResponse() {
6060 }
6161}
6262
63+ /** The `createUploadSession` response carrying the preauthenticated upload URL. */
64+ function uploadSessionResponse ( uploadUrl = 'https://sn3302.up.1drv.com/up/session-abc' ) {
65+ const body = { uploadUrl, expirationDateTime : '2026-01-01T00:00:00Z' }
66+ return {
67+ ok : true ,
68+ status : 200 ,
69+ statusText : '' ,
70+ headers : new Headers ( ) ,
71+ body : null ,
72+ text : async ( ) => JSON . stringify ( body ) ,
73+ json : async ( ) => body ,
74+ arrayBuffer : async ( ) => new ArrayBuffer ( 0 ) ,
75+ }
76+ }
77+
6378function preconditionFailedResponse ( ) {
6479 return {
6580 ok : false ,
@@ -88,11 +103,11 @@ beforeEach(() => {
88103} )
89104
90105describe ( 'POST /api/tools/microsoft_word/append' , ( ) => {
91- it ( 'uploads the edit when the document has not changed, guarded by its content tag' , async ( ) => {
106+ it ( 'writes through a conditional upload session carrying the content tag' , async ( ) => {
92107 mockSecureFetchWithPinnedIP
93108 . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
94109 . mockResolvedValueOnce ( await docxResponse ( ) )
95- . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
110+ . mockResolvedValueOnce ( uploadSessionResponse ( ) )
96111 . mockResolvedValueOnce ( itemResponse ( 'tag-2' ) )
97112
98113 const response = await POST ( createMockRequest ( 'POST' , baseBody ) )
@@ -105,16 +120,26 @@ describe('POST /api/tools/microsoft_word/append', () => {
105120 expect ( data . success ) . toBe ( true )
106121 expect ( data . output . updatedContent ) . toBe ( true )
107122
123+ // The precondition rides on the session creation, which Graph documents as
124+ // returning 412 on a mismatch.
125+ const sessionCall = mockSecureFetchWithPinnedIP . mock . calls . find ( ( call ) =>
126+ String ( call [ 0 ] ) . endsWith ( '/createUploadSession' )
127+ )
128+ expect ( sessionCall ?. [ 2 ] ) . toMatchObject ( { method : 'POST' } )
129+ expect ( sessionCall ?. [ 2 ] . headers ) . toMatchObject ( { 'if-match' : 'tag-1' } )
130+
131+ // Bytes go to the preauthenticated URL, and must not carry the bearer token.
108132 const uploadCall = mockSecureFetchWithPinnedIP . mock . calls . at ( - 1 )
133+ expect ( uploadCall ?. [ 0 ] ) . toBe ( 'https://sn3302.up.1drv.com/up/session-abc' )
109134 expect ( uploadCall ?. [ 2 ] ) . toMatchObject ( { method : 'PUT' } )
110- expect ( uploadCall ?. [ 2 ] . headers ) . toMatchObject ( { 'if-match' : 'tag-1' } )
135+ expect ( uploadCall ?. [ 2 ] . headers . Authorization ) . toBeUndefined ( )
111136 } )
112137
113- it ( 'refuses to overwrite a document that changed while the edit was in flight ' , async ( ) => {
138+ it ( 'refuses to overwrite when the service rejects the precondition ' , async ( ) => {
114139 mockSecureFetchWithPinnedIP
115140 . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
116141 . mockResolvedValueOnce ( await docxResponse ( ) )
117- . mockResolvedValueOnce ( itemResponse ( 'tag-2' ) )
142+ . mockResolvedValueOnce ( preconditionFailedResponse ( ) )
118143
119144 const response = await POST ( createMockRequest ( 'POST' , baseBody ) )
120145
@@ -123,12 +148,21 @@ describe('POST /api/tools/microsoft_word/append', () => {
123148 expect ( data . success ) . toBe ( false )
124149 expect ( data . error ) . toMatch ( / n o o t h e r c h a n g e w a s o v e r w r i t t e n / )
125150
126- // The conflict is detected before the PUT , so nothing was written .
151+ // The session was refused , so no bytes were ever sent .
127152 expect ( mockSecureFetchWithPinnedIP . mock . calls . some ( ( call ) => call [ 2 ] ?. method === 'PUT' ) ) . toBe (
128153 false
129154 )
130155 } )
131156
157+ it ( 'maps a malformed document ID to a client error, not a server error' , async ( ) => {
158+ const response = await POST ( createMockRequest ( 'POST' , { ...baseBody , documentId : 'bad/../id' } ) )
159+
160+ expect ( response . status ) . toBe ( 400 )
161+ const data = ( await response . json ( ) ) as { success : boolean ; error : string }
162+ expect ( data . success ) . toBe ( false )
163+ expect ( mockSecureFetchWithPinnedIP ) . not . toHaveBeenCalled ( )
164+ } )
165+
132166 it ( 'refuses to write Word bytes over a drive item that is not a .docx' , async ( ) => {
133167 const pdf = {
134168 id : 'doc-abc' ,
@@ -201,41 +235,11 @@ describe('POST /api/tools/microsoft_word/append', () => {
201235 )
202236 } )
203237
204- it ( 'refuses to write when the re-read reports no version' , async ( ) => {
205- const untagged = {
206- id : 'doc-abc' ,
207- name : 'notes.docx' ,
208- file : {
209- mimeType : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
210- } ,
211- }
238+ it ( 'surfaces a conflict raised when the bytes commit' , async ( ) => {
212239 mockSecureFetchWithPinnedIP
213240 . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
214241 . mockResolvedValueOnce ( await docxResponse ( ) )
215- . mockResolvedValueOnce ( {
216- ok : true ,
217- status : 200 ,
218- statusText : '' ,
219- headers : new Headers ( ) ,
220- body : null ,
221- text : async ( ) => JSON . stringify ( untagged ) ,
222- json : async ( ) => untagged ,
223- arrayBuffer : async ( ) => new ArrayBuffer ( 0 ) ,
224- } )
225-
226- const response = await POST ( createMockRequest ( 'POST' , baseBody ) )
227-
228- expect ( response . status ) . toBe ( 409 )
229- expect ( mockSecureFetchWithPinnedIP . mock . calls . some ( ( call ) => call [ 2 ] ?. method === 'PUT' ) ) . toBe (
230- false
231- )
232- } )
233-
234- it ( 'surfaces a 412 from the upload precondition as the same conflict' , async ( ) => {
235- mockSecureFetchWithPinnedIP
236- . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
237- . mockResolvedValueOnce ( await docxResponse ( ) )
238- . mockResolvedValueOnce ( itemResponse ( 'tag-1' ) )
242+ . mockResolvedValueOnce ( uploadSessionResponse ( ) )
239243 . mockResolvedValueOnce ( preconditionFailedResponse ( ) )
240244
241245 const response = await POST ( createMockRequest ( 'POST' , baseBody ) )
0 commit comments