From 1eaaa50d8f09a17dd53f26aea9ca0cc5d23f9f44 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 23 Dec 2025 22:46:35 +0100 Subject: [PATCH] rate-limits refactor for single point of truth 2 --- .../SwaggerDefinitionsJSON.scala | 14 ++-- .../scala/code/api/v6_0_0/APIMethods600.scala | 10 ++- .../code/api/v6_0_0/JSONFactory6.0.0.scala | 46 +++++------ ...lLimitsTest.scala => RateLimitsTest.scala} | 76 +++++++++++++++++-- 4 files changed, 108 insertions(+), 38 deletions(-) rename obp-api/src/test/scala/code/api/v6_0_0/{CallLimitsTest.scala => RateLimitsTest.scala} (72%) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 2bd0db9a4..0f8a7f6f3 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4157,14 +4157,14 @@ object SwaggerDefinitionsJSON { ) lazy val activeCallLimitsJsonV600 = ActiveCallLimitsJsonV600( - call_limits = List(callLimitJsonV600), + considered_rate_limit_ids = List("80e1e0b2-d8bf-4f85-a579-e69ef36e3305"), active_at_date = DateWithDayExampleObject, - total_per_second_call_limit = 100, - total_per_minute_call_limit = 1000, - total_per_hour_call_limit = -1, - total_per_day_call_limit = -1, - total_per_week_call_limit = -1, - total_per_month_call_limit = -1 + active_per_second_rate_limit = 100, + active_per_minute_rate_limit = 1000, + active_per_hour_rate_limit = -1, + active_per_day_rate_limit = -1, + active_per_week_rate_limit = -1, + active_per_month_rate_limit = -1 ) lazy val accountWebhookPostJson = AccountWebhookPostJson( diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 52b7eb5e8..bfc4c74e1 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -457,10 +457,10 @@ trait APIMethods600 { implementedInApiVersion, nameOf(getActiveCallLimitsAtDate), "GET", - "/management/consumers/CONSUMER_ID/consumer/rate-limits/active-at-date/DATE", + "/management/consumers/CONSUMER_ID/consumer/active-rate-limits/DATE", "Get Active Rate Limits at Date", s""" - |Get the sum of rate limits at a certain date time. This returns a SUM of all the records that span that time. + |Get the active rate limits for a consumer at a specific date. Returns the aggregated rate limits from all active records at that time. | |Date format: YYYY-MM-DDTHH:MM:SSZ (e.g. 1099-12-31T23:00:00Z) | @@ -482,7 +482,7 @@ trait APIMethods600 { lazy val getActiveCallLimitsAtDate: OBPEndpoint = { - case "management" :: "consumers" :: consumerId :: "consumer" :: "rate-limits" :: "active-at-date" :: dateString :: Nil JsonGet _ => + case "management" :: "consumers" :: consumerId :: "consumer" :: "active-rate-limits" :: dateString :: Nil JsonGet _ => cc => implicit val ec = EndpointContext(Some(cc)) for { @@ -494,8 +494,10 @@ trait APIMethods600 { format.parse(dateString) } rateLimit <- RateLimitingUtil.getActiveRateLimits(consumerId, date) + rateLimitRecords <- RateLimitingDI.rateLimiting.vend.getActiveCallLimitsByConsumerIdAtDate(consumerId, date) + rateLimitIds = rateLimitRecords.map(_.rateLimitingId) } yield { - (JSONFactory600.createActiveCallLimitsJsonV600FromCallLimit(rateLimit, date), HttpCode.`200`(callContext)) + (JSONFactory600.createActiveCallLimitsJsonV600FromCallLimit(rateLimit, rateLimitIds, date), HttpCode.`200`(callContext)) } } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index ba882414c..d0f74f3c4 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -99,14 +99,14 @@ case class CallLimitJsonV600( ) case class ActiveCallLimitsJsonV600( - call_limits: List[CallLimitJsonV600], + considered_rate_limit_ids: List[String], active_at_date: java.util.Date, - total_per_second_call_limit: Long, - total_per_minute_call_limit: Long, - total_per_hour_call_limit: Long, - total_per_day_call_limit: Long, - total_per_week_call_limit: Long, - total_per_month_call_limit: Long + active_per_second_rate_limit: Long, + active_per_minute_rate_limit: Long, + active_per_hour_rate_limit: Long, + active_per_day_rate_limit: Long, + active_per_week_rate_limit: Long, + active_per_month_rate_limit: Long ) case class RateLimitV600( @@ -574,32 +574,34 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { rateLimitings: List[code.ratelimiting.RateLimiting], activeDate: java.util.Date ): ActiveCallLimitsJsonV600 = { - val callLimits = rateLimitings.map(createCallLimitJsonV600) + val rateLimitIds = rateLimitings.map(_.rateLimitingId) ActiveCallLimitsJsonV600( - call_limits = callLimits, + considered_rate_limit_ids = rateLimitIds, active_at_date = activeDate, - total_per_second_call_limit = rateLimitings.map(_.perSecondCallLimit).sum, - total_per_minute_call_limit = rateLimitings.map(_.perMinuteCallLimit).sum, - total_per_hour_call_limit = rateLimitings.map(_.perHourCallLimit).sum, - total_per_day_call_limit = rateLimitings.map(_.perDayCallLimit).sum, - total_per_week_call_limit = rateLimitings.map(_.perWeekCallLimit).sum, - total_per_month_call_limit = rateLimitings.map(_.perMonthCallLimit).sum + active_per_second_rate_limit = rateLimitings.map(_.perSecondCallLimit).sum, + active_per_minute_rate_limit = rateLimitings.map(_.perMinuteCallLimit).sum, + active_per_hour_rate_limit = rateLimitings.map(_.perHourCallLimit).sum, + active_per_day_rate_limit = rateLimitings.map(_.perDayCallLimit).sum, + active_per_week_rate_limit = rateLimitings.map(_.perWeekCallLimit).sum, + active_per_month_rate_limit = rateLimitings.map(_.perMonthCallLimit).sum ) } def createActiveCallLimitsJsonV600FromCallLimit( + rateLimit: code.api.util.RateLimitingJson.CallLimit, + rateLimitIds: List[String], activeDate: java.util.Date ): ActiveCallLimitsJsonV600 = { ActiveCallLimitsJsonV600( - call_limits = List.empty, + considered_rate_limit_ids = rateLimitIds, active_at_date = activeDate, - total_per_second_call_limit = rateLimit.per_second, - total_per_minute_call_limit = rateLimit.per_minute, - total_per_hour_call_limit = rateLimit.per_hour, - total_per_day_call_limit = rateLimit.per_day, - total_per_week_call_limit = rateLimit.per_week, - total_per_month_call_limit = rateLimit.per_month + active_per_second_rate_limit = rateLimit.per_second, + active_per_minute_rate_limit = rateLimit.per_minute, + active_per_hour_rate_limit = rateLimit.per_hour, + active_per_day_rate_limit = rateLimit.per_day, + active_per_week_rate_limit = rateLimit.per_week, + active_per_month_rate_limit = rateLimit.per_month ) } diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala similarity index 72% rename from obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala rename to obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala index 0550fefe6..0eeb146d3 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CallLimitsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala @@ -41,7 +41,7 @@ import java.time.format.DateTimeFormatter import java.time.{ZoneOffset, ZonedDateTime} import java.util.Date -class CallLimitsTest extends V600ServerSetup { +class RateLimitsTest extends V600ServerSetup { object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.createCallLimits)) @@ -171,15 +171,15 @@ class CallLimitsTest extends V600ServerSetup { val currentDateString = ZonedDateTime .now(ZoneOffset.UTC) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) - val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / "active-at-date" / currentDateString).GET <@ (user1) + val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "active-rate-limits" / currentDateString).GET <@ (user1) val getResponse = makeGetRequest(getRequest) Then("We should get a 200") getResponse.code should equal(200) And("we should get the active call limits response") val activeCallLimits = getResponse.body.extract[ActiveCallLimitsJsonV600] - activeCallLimits.call_limits.size == 0 - activeCallLimits.total_per_second_call_limit == 0L + activeCallLimits.considered_rate_limit_ids.size >= 0 + activeCallLimits.active_per_second_rate_limit == 0L } scenario("We will try to get active call limits without proper role", ApiEndpoint3, VersionOfApi) { @@ -189,7 +189,7 @@ class CallLimitsTest extends V600ServerSetup { val currentDateString = ZonedDateTime .now(ZoneOffset.UTC) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) - val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits" / "active-at-date" / currentDateString).GET <@ (user1) + val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "active-rate-limits" / currentDateString).GET <@ (user1) val getResponse = makeGetRequest(getRequest) Then("We should get a 403") @@ -197,5 +197,71 @@ class CallLimitsTest extends V600ServerSetup { And("error should be " + UserHasMissingRoles + CanGetRateLimits) getResponse.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetRateLimits) } + + scenario("We will get aggregated call limits for two overlapping rate limit records", ApiEndpoint3, VersionOfApi) { + Given("We create two call limit records with overlapping date ranges") + val Some((c, _)) = user1 + val consumerId = Consumers.consumers.vend.getConsumerByConsumerKey(c.key).map(_.consumerId.get).getOrElse("") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateRateLimits.toString) + + // Create first rate limit record + val fromDate1 = new Date() + val toDate1 = new Date(System.currentTimeMillis() + 172800000L) // +2 days + val rateLimit1 = CallLimitPostJsonV600( + from_date = fromDate1, + to_date = toDate1, + api_version = Some("v6.0.0"), + api_name = Some("testEndpoint1"), + bank_id = None, + per_second_call_limit = "10", + per_minute_call_limit = "100", + per_hour_call_limit = "1000", + per_day_call_limit = "5000", + per_week_call_limit = "-1", + per_month_call_limit = "-1" + ) + val request1 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) + val createResponse1 = makePostRequest(request1, write(rateLimit1)) + createResponse1.code should equal(201) + + // Create second rate limit record with same date range + val rateLimit2 = CallLimitPostJsonV600( + from_date = fromDate1, + to_date = toDate1, + api_version = Some("v6.0.0"), + api_name = Some("testEndpoint2"), + bank_id = None, + per_second_call_limit = "5", + per_minute_call_limit = "50", + per_hour_call_limit = "500", + per_day_call_limit = "2500", + per_week_call_limit = "-1", + per_month_call_limit = "-1" + ) + val request2 = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "rate-limits").POST <@ (user1) + val createResponse2 = makePostRequest(request2, write(rateLimit2)) + createResponse2.code should equal(201) + + When("We get active call limits at a date within the overlapping range") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetRateLimits.toString) + val targetDate = ZonedDateTime + .now(ZoneOffset.UTC) + .plusDays(1) // Check 1 day from now (within the range) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) + val getRequest = (v6_0_0_Request / "management" / "consumers" / consumerId / "consumer" / "active-rate-limits" / targetDate).GET <@ (user1) + val getResponse = makeGetRequest(getRequest) + + Then("We should get a 200") + getResponse.code should equal(200) + + And("the totals should be the sum of both records (using single source of truth aggregation)") + val activeCallLimits = getResponse.body.extract[ActiveCallLimitsJsonV600] + activeCallLimits.active_per_second_rate_limit should equal(15L) // 10 + 5 + activeCallLimits.active_per_minute_rate_limit should equal(150L) // 100 + 50 + activeCallLimits.active_per_hour_rate_limit should equal(1500L) // 1000 + 500 + activeCallLimits.active_per_day_rate_limit should equal(7500L) // 5000 + 2500 + activeCallLimits.active_per_week_rate_limit should equal(-1L) // -1 (both are -1, so unlimited) + activeCallLimits.active_per_month_rate_limit should equal(-1L) // -1 (both are -1, so unlimited) + } } } \ No newline at end of file