@@ -379,6 +379,34 @@ public void testSetBucketQuotaZeroTolerates404() throws Exception {
379379 driver .setBucketQuota (bucketTO , TEST_STORE_ID , 0 );
380380 }
381381
382+ @ Test
383+ public void testSetBucketQuotaClearExistingPropagates404 () throws Exception {
384+ BucketTO bucketTO = mock (BucketTO .class );
385+ when (bucketTO .getName ()).thenReturn (TEST_BUCKET_NAME );
386+ when (bucketTO .getAccountId ()).thenReturn (TEST_ACCOUNT_ID );
387+ doReturn (TEST_S3_URL ).when (driver ).getS3Url (TEST_STORE_ID );
388+ doReturn ("access-key" ).when (driver ).getAccessKey (TEST_STORE_ID );
389+ doReturn ("secret-key" ).when (driver ).getSecretKey (TEST_STORE_ID );
390+
391+ // The bucket already has a positive quota, so a quota 0 request is a
392+ // clear of an existing quota. A 404 must NOT be tolerated: reporting
393+ // success would lower CloudStack accounting while SeaweedFS keeps the
394+ // old quota and read-only state.
395+ List <BucketVO > buckets = new ArrayList <>();
396+ buckets .add (new BucketVO (TEST_ACCOUNT_ID , TEST_DOMAIN_ID , TEST_STORE_ID , TEST_BUCKET_NAME , 100 , false , false , false , null ));
397+ when (bucketDao .listByObjectStoreIdAndAccountId (TEST_STORE_ID , TEST_ACCOUNT_ID )).thenReturn (buckets );
398+
399+ HttpClient mockHttpClient = mock (HttpClient .class );
400+ HttpResponse <String > mockResponse = mock (HttpResponse .class );
401+ when (mockResponse .statusCode ()).thenReturn (404 );
402+ when (mockResponse .body ()).thenReturn ("not found" );
403+ when (mockHttpClient .send (ArgumentMatchers .<HttpRequest >any (),
404+ ArgumentMatchers .<HttpResponse .BodyHandler <String >>any ())).thenReturn (mockResponse );
405+ doReturn (mockHttpClient ).when (driver ).getS3ExtensionHttpClient ();
406+
407+ assertThrows (CloudRuntimeException .class , () -> driver .setBucketQuota (bucketTO , TEST_STORE_ID , 0 ));
408+ }
409+
382410 @ Test
383411 public void testSetBucketQuotaRejects3xx () throws Exception {
384412 BucketTO bucketTO = mock (BucketTO .class );
@@ -833,4 +861,94 @@ public void testGetAllBucketsUsageMetricsFailureFallsBackToList() throws Excepti
833861 assertEquals (1 , usage .size ());
834862 assertEquals (42L , usage .get ("b1" ).longValue ());
835863 }
864+
865+ @ Test
866+ public void testGetAllBucketsUsageMetricsFloatValueParsed () throws Exception {
867+ doReturn ("http://metrics.local:9327" ).when (driver ).getMetricsUrl (TEST_STORE_ID );
868+
869+ List <BucketVO > buckets = new ArrayList <>();
870+ buckets .add (new BucketVO (TEST_ACCOUNT_ID , TEST_DOMAIN_ID , TEST_STORE_ID , "b1" , null , false , false , false , null ));
871+ when (bucketDao .listByObjectStoreId (TEST_STORE_ID )).thenReturn (buckets );
872+
873+ // Prometheus gauges are floating point and may use scientific notation
874+ String metricsBody = "SeaweedFS_s3_bucket_size_bytes{bucket=\" b1\" } 1.2345678e+07\n " ;
875+ HttpClient mockHttpClient = mock (HttpClient .class );
876+ HttpResponse <String > mockResponse = mock (HttpResponse .class );
877+ when (mockResponse .statusCode ()).thenReturn (200 );
878+ when (mockResponse .body ()).thenReturn (metricsBody );
879+ when (mockHttpClient .send (ArgumentMatchers .<HttpRequest >any (),
880+ ArgumentMatchers .<HttpResponse .BodyHandler <String >>any ()))
881+ .thenReturn (mockResponse );
882+ doReturn (mockHttpClient ).when (driver ).getS3ExtensionHttpClient ();
883+
884+ Map <String , Long > usage = driver .getAllBucketsUsage (TEST_STORE_ID );
885+ assertEquals (12345678L , usage .get ("b1" ).longValue ());
886+ verify (s3Client , never ()).listObjectsV2 (any (ListObjectsV2Request .class ));
887+ }
888+
889+ @ Test
890+ public void testGetAllBucketsUsageUnparseableMetricFallsBackToList () throws Exception {
891+ doReturn ("http://metrics.local:9327" ).when (driver ).getMetricsUrl (TEST_STORE_ID );
892+ doReturn (s3Client ).when (driver ).getS3ClientByStoreId (TEST_STORE_ID );
893+
894+ List <BucketVO > buckets = new ArrayList <>();
895+ buckets .add (new BucketVO (TEST_ACCOUNT_ID , TEST_DOMAIN_ID , TEST_STORE_ID , "b1" , null , false , false , false , null ));
896+ when (bucketDao .listByObjectStoreId (TEST_STORE_ID )).thenReturn (buckets );
897+
898+ // An unparseable sample value must be treated as a scrape failure so
899+ // the S3 fallback runs, rather than reporting the bucket as zero.
900+ String metricsBody = "SeaweedFS_s3_bucket_size_bytes{bucket=\" b1\" } not-a-number\n " ;
901+ HttpClient mockHttpClient = mock (HttpClient .class );
902+ HttpResponse <String > mockResponse = mock (HttpResponse .class );
903+ when (mockResponse .statusCode ()).thenReturn (200 );
904+ when (mockResponse .body ()).thenReturn (metricsBody );
905+ when (mockHttpClient .send (ArgumentMatchers .<HttpRequest >any (),
906+ ArgumentMatchers .<HttpResponse .BodyHandler <String >>any ()))
907+ .thenReturn (mockResponse );
908+ doReturn (mockHttpClient ).when (driver ).getS3ExtensionHttpClient ();
909+
910+ ListObjectsV2Result b1Result = mock (ListObjectsV2Result .class );
911+ S3ObjectSummary s1 = new S3ObjectSummary (); s1 .setSize (77L );
912+ List <S3ObjectSummary > summaries = new ArrayList <>(); summaries .add (s1 );
913+ when (b1Result .getObjectSummaries ()).thenReturn (summaries );
914+ when (b1Result .isTruncated ()).thenReturn (false );
915+ when (s3Client .listObjectsV2 (any (ListObjectsV2Request .class ))).thenReturn (b1Result );
916+
917+ Map <String , Long > usage = driver .getAllBucketsUsage (TEST_STORE_ID );
918+ assertEquals (77L , usage .get ("b1" ).longValue ());
919+ }
920+
921+ @ Test
922+ public void testGetAllBucketsUsageWrongMetricsEndpointFallsBackToList () throws Exception {
923+ doReturn ("http://prometheus.local:9090" ).when (driver ).getMetricsUrl (TEST_STORE_ID );
924+ doReturn (s3Client ).when (driver ).getS3ClientByStoreId (TEST_STORE_ID );
925+
926+ List <BucketVO > buckets = new ArrayList <>();
927+ buckets .add (new BucketVO (TEST_ACCOUNT_ID , TEST_DOMAIN_ID , TEST_STORE_ID , "b1" , null , false , false , false , null ));
928+ when (bucketDao .listByObjectStoreId (TEST_STORE_ID )).thenReturn (buckets );
929+
930+ // metricsUrl pointing at a Prometheus server returns HTTP 200 with its
931+ // own internal metrics, not the SeaweedFS bucket series. This must be
932+ // detected so the S3 fallback runs instead of reporting zero.
933+ String metricsBody = "prometheus_build_info{version=\" 2.0\" } 1\n " +
934+ "go_goroutines 42\n " ;
935+ HttpClient mockHttpClient = mock (HttpClient .class );
936+ HttpResponse <String > mockResponse = mock (HttpResponse .class );
937+ when (mockResponse .statusCode ()).thenReturn (200 );
938+ when (mockResponse .body ()).thenReturn (metricsBody );
939+ when (mockHttpClient .send (ArgumentMatchers .<HttpRequest >any (),
940+ ArgumentMatchers .<HttpResponse .BodyHandler <String >>any ()))
941+ .thenReturn (mockResponse );
942+ doReturn (mockHttpClient ).when (driver ).getS3ExtensionHttpClient ();
943+
944+ ListObjectsV2Result b1Result = mock (ListObjectsV2Result .class );
945+ S3ObjectSummary s1 = new S3ObjectSummary (); s1 .setSize (88L );
946+ List <S3ObjectSummary > summaries = new ArrayList <>(); summaries .add (s1 );
947+ when (b1Result .getObjectSummaries ()).thenReturn (summaries );
948+ when (b1Result .isTruncated ()).thenReturn (false );
949+ when (s3Client .listObjectsV2 (any (ListObjectsV2Request .class ))).thenReturn (b1Result );
950+
951+ Map <String , Long > usage = driver .getAllBucketsUsage (TEST_STORE_ID );
952+ assertEquals (88L , usage .get ("b1" ).longValue ());
953+ }
836954}
0 commit comments