Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
b409ebb
SPINEDEM-5254 added initial ratelimit tests
agelledi Jun 3, 2025
36a066f
Enforce app rate limiting in internal-dev.
davidhamill1-nhs Jun 4, 2025
9a42bf4
SPINEDEM-5254 added rate limit tests
agelledi Jun 5, 2025
267d50e
SPINEDEM-5254 reduced rate limit for testing
agelledi Jun 6, 2025
af98ac2
SPINEDEM-5254 disabled spike arrest to confirm combined traffic behav…
agelledi Jun 6, 2025
3b9bf16
SPINEDEM-5254 enabled spikearrest config
agelledi Jun 6, 2025
af50fe1
SPINEDEM-5254 increased proxy rate limit for testing
agelledi Jun 9, 2025
4df88e7
SPINEDEM-5254 update ratelimit test to run sequentially
agelledi Jun 9, 2025
d3bf5b7
SPINEDEM-5254 added commandline args for test
agelledi Jun 9, 2025
215c191
Enforce rate limit in INT.
davidhamill1-nhs Jun 10, 2025
76dee69
SPINEDEM-524 reverted testing rate limit on internal dev
agelledi Jun 10, 2025
c0eb7cc
SPINEDEM-5254 added review comment related changes
agelledi Jun 11, 2025
f39dd7c
Merge branch 'master' into SPINEDEM-5254-add-rate-limit-tests-int
davidhamill1-nhs Jul 3, 2025
45aace9
Reduce code duplication.
davidhamill1-nhs Jul 7, 2025
1b01323
Reduce duplication of features.
davidhamill1-nhs Jul 7, 2025
726f03f
Reduce code duplication.
davidhamill1-nhs Jul 7, 2025
167e97e
Ensure the rateLimit testing is not picked up by the pipelines.
davidhamill1-nhs Jul 7, 2025
3e62ddd
Remove unused feature.
davidhamill1-nhs Jul 7, 2025
b1a3118
Merge branch 'master' into SPINEDEM-5254-add-rate-limit-tests-int
davidhamill1-nhs Jul 8, 2025
a13532f
responseStatus is an attribute of `result`.
davidhamill1-nhs Jul 8, 2025
6d1e57d
Revert "responseStatus is an attribute of `result`."
davidhamill1-nhs Jul 8, 2025
26248c3
Run second app immediately after first.
davidhamill1-nhs Jul 8, 2025
5acf34f
The feature file was moved out of its own directory.
davidhamill1-nhs Jul 8, 2025
ca376c6
Merge remote-tracking branch 'refs/remotes/origin/SPINEDEM-5254-add-r…
davidhamill1-nhs Jul 8, 2025
730fef8
Merge commit 'b409ebb3cf0d2241df5ffd3b319f307fa840d5d0' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
cdc1e4a
Enforce app rate limiting in internal-dev.
davidhamill1-nhs Jul 9, 2025
9416ea9
Merge commit '9a42bf4992d2a4cddd8f60001dd35b6f27aa57af' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
33fbd3f
Merge commit '267d50ea211f3cbd9e70e8dd14e19abf46cc7bfa' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
68021cf
Merge commit 'af98ac239cd218881ff7bb9656a825875dbd623f' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
e232910
Merge commit '3b9bf161813481d98ada0ba0f82428c5e0064ef6' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
c2fb566
Merge commit 'af50fe104e9d8dc49729528f10d5866bae0d56ed' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
29805c7
Merge commit '4df88e72837b47b0c8b50898d446f3261069f997' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
1d1c607
Merge commit 'd3bf5b7c50e65c10968a8c4670c74f94e86386e6' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
45a6523
Enforce rate limit in INT.
davidhamill1-nhs Jul 9, 2025
7ced264
Merge commit '76dee69bdb4435a962684227988b16b7aa127810' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
d0bc4b2
Merge commit 'c0eb7cc9a0ea51f1909cc7806a4ad913191fcc93' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
735ecee
Merge commit 'f39dd7c5f95ea07da70b48cb410818019f91cbd6' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
28f1416
Merge commit '45aace959af7bb0a052b0d917d50358822ea5bde' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
a71ebcc
Merge commit '1b01323146e0aa558dd971d41130ab9b44f61d5f' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
6f9561e
Merge commit '726f03ffb49e215347d777dc361cab0a505f6451' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
e3733ad
Merge commit '167e97e7b567cf4e2629f417725cbed0eb3cb419' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
5dc9a35
Merge commit '3e62ddda70202370ff9bde4ada20faf7ad84aad6' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
b9c3bc7
Merge commit 'b1a3118c492d5d3c4dcde6acb33b1a2aab2a7268' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
52ce43a
Merge commit 'a13532fc1c8d60186080e1597fcfc4a45ecc3381' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
16e8873
Merge commit '6d1e57dd4e2bd284ad757aad2888c12bac2f478b' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
f52674a
Merge commit '26248c3eb7315338066a63239393059963a5eea3' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
3ddda7b
Merge commit '5acf34fcc8ecec9350358bc35f3d2626093f06c3' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
b8155d1
Merge commit 'ca376c680502c339df8e34053b364be93e23b5db' into SPINEDEM…
davidhamill1-nhs Jul 9, 2025
d84bf63
Merge branch 'master' into SPINEDEM-5254-add-rate-limit-tests-int-v3
davidhamill1-nhs Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions karate-tests/karate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ function fn() {
privilegedAccessSigningKey: java.lang.System.getenv('PRIVILEGED_ACCESS_SIGNING_KEY_PATH'),
privilegedAccessApiKey: java.lang.System.getenv('PRIVILEGED_ACCESS_API_KEY'),
keyID: java.lang.System.getenv('KEY_ID'),
rateLimitingAppClientID: java.lang.System.getenv('RATE_LIMITING_APP_CLIENT_ID'),
rateLimitingAppClientSecret: java.lang.System.getenv('RATE_LIMITING_APP_CLIENT_SECRET'),
proxyRateLimitingAppClientID: java.lang.System.getenv('PROXY_RATE_LIMITING_APP_CLIENT_ID'),
proxyRateLimitingAppClientSecret: java.lang.System.getenv('PROXY_RATE_LIMITING_APP_CLIENT_SECRET'),
internalServerURL: `${java.lang.System.getenv('INTERNAL_SERVER_BASE_URI')}/personal-demographics/FHIR/R4`
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class TestLocalMockParallel {
void testLocalMockParallel() {
Results results = Runner.path("classpath:patients")
.karateEnv("local-sandbox")
.tags("@sandbox, @sandbox-only", "~@smoke-only")
.tags("@sandbox, @sandbox-only", "~@smoke-only", "~@rateLimit")
.outputJunitXml(true)
.parallel(5);
assertTrue(results.getFailCount() == 0, results.getErrorMessages());
Expand Down
2 changes: 1 addition & 1 deletion karate-tests/src/test/java/patients/TestMockParallel.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class TestMockParallel {
void testMockParallel() {
Results results = Runner.path("classpath:patients")
.karateEnv("sandbox")
.tags("@sandbox, @sandbox-only", "~@smoke-only")
.tags("@sandbox, @sandbox-only", "~@smoke-only", "~@rateLimit")
.outputJunitXml(true)
.parallel(5);
assertTrue(results.getFailCount() == 0, results.getErrorMessages());
Expand Down
2 changes: 1 addition & 1 deletion karate-tests/src/test/java/patients/TestParallel.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class TestParallel {
void testDevParallel() {
Results results = Runner.path("classpath:patients")
.outputJunitXml(true)
.tags("~@sandbox-only", "~@smoke-only")
.tags("~@sandbox-only", "~@smoke-only", "~@rateLimit")
.karateEnv("veit07")
.parallel(2);
assertTrue(results.getFailCount() == 0, results.getErrorMessages());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class TestSchemaParallel {
void testSchemaParallel() {
Results results = Runner.path("classpath:patients")
.outputJunitXml(true)
.tags("~@sandbox-only", "~@no-oas", "~@oas-bug", "~@smoke-only")
.tags("~@sandbox-only", "~@no-oas", "~@oas-bug", "~@smoke-only", "~@rateLimit")
.karateEnv("prism")
.parallel(5);
assertTrue(results.getFailCount() == 0, results.getErrorMessages());
Expand Down
31 changes: 31 additions & 0 deletions karate-tests/src/test/java/patients/rateLimits/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Rate Limit testing

To ensure the proxy's rate limit has been configured correctly, there is a test suite, using Gatling, that runs a scenario multiple times, across multiple threads.

## Rate Limiting Apps

As part of the parameters passed in to the test, the name of the requesting app is required. These must match the app's name as defined in the karate-config.js file, eg rateLimitingApp.

## Single-app Testing

To test rate limit configuration of a given app or proxy run
```bash
mvn test-compile gatling:test -Dgatling.simulationClass=patients.GetPatientRateLimitSimulation -DrequestingApp=rateLimitingApp -DnumberOfRequests=41 -Dduration=60
```
where
* requestingApp - the name of the app, which ties to the environment names for its client id and secret
* numberOfRequests - the number of request you wish to send
* duration - the duration over which you wish to send the requests

The expected rate limit varies according to the environment, access mode, requests and requesting app. See karate-config.js and the Simulation class for the variables the tests pick up.


## Two-app Testing

Some rate limits are counted per app, others are counted across all apps. In this way, one app's request can affect another.

To test rate limit configuration across two apps concurrently, run
Run the individual test:
```bash
mvn test-compile gatling:test -Dgatling.simulationClass=patients.GetPatientByTwoAppsSimulation -DrateLimitAppRequests=300 -DproxyRateLimitAppRequests=20 -Dduration=60
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Feature: Get a patient - Healthcare worker access mode
# By default, the test application uses the proxy rate limits. These rate limits vary across the internal-dev, int, and ref environments.
# As a precondition, the appropriate proxy must be assigned to the test application based on the specific testing requirements.

Background:

* def accessToken = karate.callSingle('classpath:auth/auth-redirect.feature', { clientID: karate.get(requestingApp + 'ClientID'),clientSecret: karate.get(requestingApp + 'ClientSecret')}).accessToken
* def requestHeaders = call read('classpath:auth/auth-headers.js')
* configure headers = requestHeaders
* url baseURL

@rateLimit
Scenario: Get patient details
* def nhsNumber = '9727022820'
* path 'Patient', nhsNumber
* method get
* def is429 = responseStatus == 429
* karate.set('is429', is429)
* if (is429) karate.set('timestamp429', java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).toString())
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package patients

import com.intuit.karate.gatling.PreDef._
import io.gatling.core.Predef._
import scala.concurrent.duration._


class GetPatientRateLimitSimulation extends Simulation {
val requestingApp = System.getProperty("requestingApp")

val numberOfRequests = Integer.getInteger("numberOfRequests")
val duration = Integer.getInteger("duration")

val protocol = karateProtocol()
protocol.runner.karateEnv("veit07")

var requestCounter = 0
var all429s: Seq[Int] = Seq()

val scn = scenario("RateLimitTest")
.exec(session => {
requestCounter += 1
session.set("requestIndex", requestCounter)
})
.exec(karateSet("requestingApp", session => requestingApp))
.exec(karateFeature(s"classpath:patients/rateLimits/getPatientDetails.feature"))
.exec { session =>
if (session.contains("is429") && session("is429").as[Boolean]) {
all429s = all429s :+ session("requestIndex").as[Int]
}
session
}

setUp(
scn.inject(rampUsers(numberOfRequests) during (duration.seconds)).protocols(protocol)
)

after {
println("=== Simulation Complete ===")
println(s"Total requests from $requestingApp: $numberOfRequests")
if (all429s.nonEmpty) {
println(s"Total 429 responses: ${all429s.size}")
}else {
println("No 429 responses received")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package patients

import com.intuit.karate.gatling.PreDef._
import io.gatling.core.Predef._
import scala.concurrent.duration._


class GetPatientByTwoAppsSimulation extends Simulation {
// === Configuration ===
val rateLimitAppRequests = Integer.getInteger("rateLimitAppRequests")
val proxyRateLimit = Integer.getInteger("proxyRateLimitAppRequests")
val duration = Integer.getInteger("duration")
val protocol = karateProtocol()
protocol.runner.karateEnv("veit07")

// === State Tracking ===
var app1Counter = 0
var app2Counter = 0
var app1_429s, app2_429s = Seq[Int]()
var app1_429Timestamps, app2_429Timestamps = Seq[String]()

// === Helper to Handle 429 Tracking ===
def track429(session: Session, appLabel: String): Session = {
if (session.contains("is429") && session("is429").as[Boolean]) {
val index = session("requestIndex").as[Int]
val timestamp = session("timestamp429").as[String]
val entry = s"Request $index at $timestamp"

appLabel match {
case "App1" =>
app1_429s = app1_429s :+ index
app1_429Timestamps = app1_429Timestamps :+ entry
case "App2" =>
app2_429s = app2_429s :+ index
app2_429Timestamps = app2_429Timestamps :+ entry
}
}
session
}

// === Scenario Definitions ===
def scenarioForApp(appLabel: String, requestingApp: String) = {
scenario(s"${appLabel}Test")
.exec(session => {
val index = appLabel match {
case "App1" => app1Counter += 1; app1Counter
case "App2" => app2Counter += 1; app2Counter
}
session
.set("requestIndex", index)
.set("appName", appLabel)
})
.exec(karateSet("requestingApp", session => requestingApp))
// Optional: Log each request for debugging
.exec(session => {
val app = session("appName").as[String]
val idx = session("requestIndex").as[Int]
val now = java.time.Instant.now.toString
println(s"[DEBUG] $app - Request $idx at $now")
session
})
.exec(karateFeature("classpath:patients/rateLimits/getPatientDetails.feature"))
.exec(session => track429(session, appLabel))
}

val rateLimitingAppScenario = scenarioForApp("App1", "rateLimitingApp")
val proxyRateLimitingAppScenario = scenarioForApp("App2", "proxyRateLimitingApp")

// === Simulation Setup ===
setUp(
rateLimitingAppScenario.inject(rampUsers(rateLimitAppRequests) during (duration.seconds)).protocols(protocol).andThen(
proxyRateLimitingAppScenario.inject(rampUsers(proxyRateLimit) during (duration.seconds)).protocols(protocol)
)
)

// === Report Output ===
after {
println("=== Simulation Complete ===")
println(s"Total requests from App1Test: $rateLimitAppRequests")
println(s"Total requests from App2Test: $proxyRateLimit")

def print429s(appLabel: String, count: Int, timestamps: Seq[String]) = {
println(s"Total 429 responses from $appLabel: $count")
if (timestamps.nonEmpty) timestamps.foreach(println)
else println("No 429 responses received")
}

print429s("App1Test", app1_429s.size, app1_429Timestamps)
print429s("App2Test", app2_429s.size, app2_429Timestamps)
}
}
16 changes: 12 additions & 4 deletions manifest_template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ APIGEE_ENVIRONMENTS:
ratelimit: 600pm # 10 requests per second
app:
quota:
enabled: false
enabled: true
limit: 300
interval: 1
timeunit: minute
spikeArrest:
enabled: false
enabled: true
ratelimit: 900pm
# TODO: Remove as we don't want this included in an actual release
- name: internal-dev-sandbox
display_name_suffix: Internal Development Sandbox
Expand Down Expand Up @@ -92,9 +96,13 @@ APIGEE_ENVIRONMENTS:
ratelimit: 2400pm # 40 requests per second
app:
quota:
enabled: false
enabled: true
limit: 300
interval: 1
timeunit: minute
spikeArrest:
enabled: false
enabled: true
ratelimit: 900pm
- name: prod
approval_type: manual
display_name_suffix: Production
Expand Down