11using Moq ;
2+ using Moq . Protected ;
23using Azure . Sdk . Tools . Cli . Helpers ;
34using Azure . Sdk . Tools . Cli . Services ;
45using Azure . Sdk . Tools . Cli . Tests . Mocks . Services ;
@@ -21,6 +22,7 @@ internal class ReleasePlanToolTests
2122 private IEnvironmentHelper environmentHelper ;
2223 private ReleasePlanTool releasePlanTool ;
2324 private IInputSanitizer inputSanitizer ;
25+ private HttpClient httpClient ;
2426
2527 [ SetUp ]
2628 public void Setup ( )
@@ -30,6 +32,7 @@ public void Setup()
3032 devOpsService = new MockDevOpsService ( ) ;
3133 gitHubService = new MockGitHubService ( ) ;
3234 inputSanitizer = new InputSanitizer ( ) ;
35+ httpClient = new Mock < HttpClient > ( ) . Object ;
3336
3437 var userHelperMock = new Mock < IUserHelper > ( ) ;
3538 userHelperMock . Setup ( x
=> x . GetUserEmail ( ) ) . ReturnsAsync ( "[email protected] " ) ; @@ -57,7 +60,8 @@ public void Setup()
5760 userHelper ,
5861 gitHubService ,
5962 environmentHelper ,
60- inputSanitizer ) ;
63+ inputSanitizer ,
64+ httpClient ) ;
6165 }
6266
6367 [ Test ]
@@ -150,7 +154,8 @@ public async Task Test_Create_releasePlan_with_AZSDKTOOLS_AGENT_TESTING_true_cre
150154 userHelper ,
151155 gitHubService ,
152156 environmentHelperMock . Object ,
153- inputSanitizer ) ;
157+ inputSanitizer ,
158+ httpClient ) ;
154159
155160 var testCodeFilePath = "TypeSpecTestData/specification/testcontoso/Contoso.Management" ;
156161
@@ -191,7 +196,8 @@ public async Task Test_Create_releasePlan_with_AZSDKTOOLS_AGENT_TESTING_false_re
191196 userHelper ,
192197 gitHubService ,
193198 environmentHelperMock . Object ,
194- inputSanitizer ) ;
199+ inputSanitizer ,
200+ httpClient ) ;
195201
196202 var testCodeFilePath = "TypeSpecTestData/specification/testcontoso/Contoso.Management" ;
197203
@@ -419,5 +425,200 @@ public async Task Test_update_spec_pull_request_with_non_specs_repo()
419425 Assert . That ( response . ResponseError , Does . Contain ( "Invalid spec pull request URL" ) ) ;
420426 Assert . That ( response . ResponseError , Does . Contain ( "azure-rest-api-specs" ) ) ;
421427 }
428+
429+ [ Test ]
430+ public async Task Test_list_overdue_release_plans_notify_without_emailer_uri ( )
431+ {
432+ var response = await releasePlanTool . ListOverdueReleasePlans ( notifyOwners : true , emailerUri : "" ) ;
433+ Assert . IsNotNull ( response ) ;
434+ Assert . IsNotNull ( response . ResponseError ) ;
435+ Assert . That ( response . ResponseError , Does . Contain ( "Emailer URI is required" ) ) ;
436+ }
437+
438+ [ Test ]
439+ public async Task Test_notification_includes_correct_missing_sdks ( )
440+ {
441+ var mockDevOps = new Mock < IDevOpsService > ( ) ;
442+ var plan = new ReleasePlanWorkItem
443+ {
444+ WorkItemId = 200 ,
445+ Owner = "Test Owner" ,
446+ ReleasePlanSubmittedByEmail = "[email protected] " , 447+ IsManagementPlane = true ,
448+ IsDataPlane = false ,
449+ SDKReleaseMonth = "January 2026" ,
450+ ReleasePlanLink = "https://example.com/releaseplan/200" ,
451+ SDKInfo =
452+ [
453+ new SDKInfo { Language = "Java" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" } ,
454+ new SDKInfo { Language = "Python" , ReleaseStatus = "Released" , ReleaseExclusionStatus = "Not applicable" } ,
455+ new SDKInfo { Language = ".NET" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" }
456+ ]
457+ } ;
458+ mockDevOps . Setup ( x => x . ListOverdueReleasePlansAsync ( ) ) . ReturnsAsync ( [ plan ] ) ;
459+
460+ var mockHttpMessageHandler = new Mock < HttpMessageHandler > ( ) ;
461+ var capturedBody = "" ;
462+ mockHttpMessageHandler . Protected ( )
463+ . Setup < Task < HttpResponseMessage > > (
464+ "SendAsync" ,
465+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
466+ ItExpr . IsAny < CancellationToken > ( ) )
467+ . ReturnsAsync ( ( HttpRequestMessage request , CancellationToken token ) =>
468+ {
469+ var content = request . Content ? . ReadAsStringAsync ( ) . Result ?? "" ;
470+ var payload = JsonSerializer . Deserialize < JsonElement > ( content ) ;
471+ capturedBody = payload . GetProperty ( "Body" ) . GetString ( ) ?? "" ;
472+ return new HttpResponseMessage ( System . Net . HttpStatusCode . OK ) ;
473+ } ) ;
474+
475+ var testHttpClient = new HttpClient ( mockHttpMessageHandler . Object ) ;
476+ var tool = new ReleasePlanTool ( mockDevOps . Object , gitHelper , typeSpecHelper , logger , userHelper , gitHubService , environmentHelper , inputSanitizer , testHttpClient ) ;
477+
478+ await tool . ListOverdueReleasePlans ( notifyOwners : true , emailerUri : "https://test.com/email" ) ;
479+
480+ Assert . That ( capturedBody , Does . Contain ( "Java" ) ) ;
481+ Assert . That ( capturedBody , Does . Contain ( ".NET" ) ) ;
482+ Assert . That ( capturedBody , Does . Not . Contain ( "Python" ) ) ; // Released, should not be in missing list
483+ }
484+
485+ [ Test ]
486+ public async Task Test_notification_excludes_approved_and_requested_languages ( )
487+ {
488+ var mockDevOps = new Mock < IDevOpsService > ( ) ;
489+ var plan = new ReleasePlanWorkItem
490+ {
491+ WorkItemId = 201 ,
492+ Owner = "Test Owner" ,
493+ ReleasePlanSubmittedByEmail = "[email protected] " , 494+ IsManagementPlane = true ,
495+ IsDataPlane = false ,
496+ SDKReleaseMonth = "January 2026" ,
497+ ReleasePlanLink = "https://example.com/releaseplan/201" ,
498+ SDKInfo =
499+ [
500+ new SDKInfo { Language = "Java" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" } ,
501+ new SDKInfo { Language = "Python" , ReleaseStatus = "" , ReleaseExclusionStatus = "Approved" } ,
502+ new SDKInfo { Language = ".NET" , ReleaseStatus = "" , ReleaseExclusionStatus = "Requested" }
503+ ]
504+ } ;
505+ mockDevOps . Setup ( x => x . ListOverdueReleasePlansAsync ( ) ) . ReturnsAsync ( [ plan ] ) ;
506+
507+ var mockHttpMessageHandler = new Mock < HttpMessageHandler > ( ) ;
508+ var capturedBody = "" ;
509+ mockHttpMessageHandler . Protected ( )
510+ . Setup < Task < HttpResponseMessage > > (
511+ "SendAsync" ,
512+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
513+ ItExpr . IsAny < CancellationToken > ( ) )
514+ . ReturnsAsync ( ( HttpRequestMessage request , CancellationToken token ) =>
515+ {
516+ var content = request . Content ? . ReadAsStringAsync ( ) . Result ?? "" ;
517+ var payload = JsonSerializer . Deserialize < JsonElement > ( content ) ;
518+ capturedBody = payload . GetProperty ( "Body" ) . GetString ( ) ?? "" ;
519+ return new HttpResponseMessage ( System . Net . HttpStatusCode . OK ) ;
520+ } ) ;
521+
522+ var testHttpClient = new HttpClient ( mockHttpMessageHandler . Object ) ;
523+ var tool = new ReleasePlanTool ( mockDevOps . Object , gitHelper , typeSpecHelper , logger , userHelper , gitHubService , environmentHelper , inputSanitizer , testHttpClient ) ;
524+
525+ await tool . ListOverdueReleasePlans ( notifyOwners : true , emailerUri : "https://test.com/email" ) ;
526+
527+ Assert . That ( capturedBody , Does . Contain ( "Java" ) ) ;
528+ Assert . That ( capturedBody , Does . Not . Contain ( "Python" ) ) ; // Approved exclusion
529+ Assert . That ( capturedBody , Does . Not . Contain ( ".NET" ) ) ; // Requested exclusion
530+ }
531+
532+ [ Test ]
533+ public async Task Test_notification_excludes_go_for_dataplane ( )
534+ {
535+ var mockDevOps = new Mock < IDevOpsService > ( ) ;
536+ var plan = new ReleasePlanWorkItem
537+ {
538+ WorkItemId = 202 ,
539+ Owner = "Test Owner" ,
540+ ReleasePlanSubmittedByEmail = "[email protected] " , 541+ IsDataPlane = true ,
542+ IsManagementPlane = false ,
543+ SDKReleaseMonth = "January 2026" ,
544+ ReleasePlanLink = "https://example.com/releaseplan/202" ,
545+ SDKInfo =
546+ [
547+ new SDKInfo { Language = "Java" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" } ,
548+ new SDKInfo { Language = "Go" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" }
549+ ]
550+ } ;
551+ mockDevOps . Setup ( x => x . ListOverdueReleasePlansAsync ( ) ) . ReturnsAsync ( [ plan ] ) ;
552+
553+ var mockHttpMessageHandler = new Mock < HttpMessageHandler > ( ) ;
554+ var capturedBody = "" ;
555+ mockHttpMessageHandler . Protected ( )
556+ . Setup < Task < HttpResponseMessage > > (
557+ "SendAsync" ,
558+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
559+ ItExpr . IsAny < CancellationToken > ( ) )
560+ . ReturnsAsync ( ( HttpRequestMessage request , CancellationToken token ) =>
561+ {
562+ var content = request . Content ? . ReadAsStringAsync ( ) . Result ?? "" ;
563+ var payload = JsonSerializer . Deserialize < JsonElement > ( content ) ;
564+ capturedBody = payload . GetProperty ( "Body" ) . GetString ( ) ?? "" ;
565+ return new HttpResponseMessage ( System . Net . HttpStatusCode . OK ) ;
566+ } ) ;
567+
568+ var testHttpClient = new HttpClient ( mockHttpMessageHandler . Object ) ;
569+ var tool = new ReleasePlanTool ( mockDevOps . Object , gitHelper , typeSpecHelper , logger , userHelper , gitHubService , environmentHelper , inputSanitizer , testHttpClient ) ;
570+
571+ await tool . ListOverdueReleasePlans ( notifyOwners : true , emailerUri : "https://test.com/email" ) ;
572+
573+ Assert . That ( capturedBody , Does . Contain ( "Java" ) ) ;
574+ Assert . That ( capturedBody , Does . Not . Contain ( "Go" ) ) ; // Filtered for Data Plane
575+ Assert . That ( capturedBody , Does . Contain ( "Data Plane" ) ) ;
576+ }
577+
578+ [ Test ]
579+ public async Task Test_notification_includes_go_for_management_plane ( )
580+ {
581+ var mockDevOps = new Mock < IDevOpsService > ( ) ;
582+ var plan = new ReleasePlanWorkItem
583+ {
584+ WorkItemId = 203 ,
585+ Owner = "Test Owner" ,
586+ ReleasePlanSubmittedByEmail = "[email protected] " , 587+ IsManagementPlane = true ,
588+ IsDataPlane = false ,
589+ SDKReleaseMonth = "January 2026" ,
590+ ReleasePlanLink = "https://example.com/releaseplan/203" ,
591+ SDKInfo =
592+ [
593+ new SDKInfo { Language = "Java" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" } ,
594+ new SDKInfo { Language = "Go" , ReleaseStatus = "" , ReleaseExclusionStatus = "Not applicable" }
595+ ]
596+ } ;
597+ mockDevOps . Setup ( x => x . ListOverdueReleasePlansAsync ( ) ) . ReturnsAsync ( [ plan ] ) ;
598+
599+ var mockHttpMessageHandler = new Mock < HttpMessageHandler > ( ) ;
600+ var capturedBody = "" ;
601+ mockHttpMessageHandler . Protected ( )
602+ . Setup < Task < HttpResponseMessage > > (
603+ "SendAsync" ,
604+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
605+ ItExpr . IsAny < CancellationToken > ( ) )
606+ . ReturnsAsync ( ( HttpRequestMessage request , CancellationToken token ) =>
607+ {
608+ var content = request . Content ? . ReadAsStringAsync ( ) . Result ?? "" ;
609+ var payload = JsonSerializer . Deserialize < JsonElement > ( content ) ;
610+ capturedBody = payload . GetProperty ( "Body" ) . GetString ( ) ?? "" ;
611+ return new HttpResponseMessage ( System . Net . HttpStatusCode . OK ) ;
612+ } ) ;
613+
614+ var testHttpClient = new HttpClient ( mockHttpMessageHandler . Object ) ;
615+ var tool = new ReleasePlanTool ( mockDevOps . Object , gitHelper , typeSpecHelper , logger , userHelper , gitHubService , environmentHelper , inputSanitizer , testHttpClient ) ;
616+
617+ await tool . ListOverdueReleasePlans ( notifyOwners : true , emailerUri : "https://test.com/email" ) ;
618+
619+ Assert . That ( capturedBody , Does . Contain ( "Java" ) ) ;
620+ Assert . That ( capturedBody , Does . Contain ( "Go" ) ) ; // Included for Management Plane
621+ Assert . That ( capturedBody , Does . Contain ( "Management Plane" ) ) ;
622+ }
422623 }
423624}
0 commit comments