<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="&lt;CLIENT&gt; &lt;SYSTEM&gt; - &lt;the one question this test answers&gt;" enabled="true">
      <stringProp name="TestPlan.comments">WHAT THIS ANSWERS: &lt;e.g. can the portal serve 500 logged-in users browsing documents?&gt;
WHAT IT IS NOT: &lt;e.g. not the submission flow - nothing here writes data&gt;

HOW TO RUN (LoadLitmus)
  Load card          users / ramp / loops - reads a.users, a.ramp, a.loops
  Parameter form     host, smoke, password, a.csv, thinkMin, thinkMax
  smoke=true         stops after the read-only steps (default)
  smoke=false        runs the steps that write data

PLUGINS REQUIRED: none. Add any here, and install on master AND every slave.

NOT included on purpose: listeners and a Backend Listener. LoadLitmus writes
the JTL and injects its own InfluxDB listener when monitoring is configured,
with runId set to the result folder. Keep secrets out of this file.</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
    </TestPlan>
    <hashTree>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="Environment" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="host" elementType="Argument">
            <stringProp name="Argument.name">host</stringProp>
            <stringProp name="Argument.value">${__P(host,app-stg.client.my)}</stringProp>
            <stringProp name="Argument.desc">Target host. Staging by default - override in the parameter form for prod.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="protocol" elementType="Argument">
            <stringProp name="Argument.name">protocol</stringProp>
            <stringProp name="Argument.value">${__P(protocol,https)}</stringProp>
            <stringProp name="Argument.desc">http or https.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="port" elementType="Argument">
            <stringProp name="Argument.name">port</stringProp>
            <stringProp name="Argument.value">${__P(port,)}</stringProp>
            <stringProp name="Argument.desc">Leave empty for the protocol default (80/443). Set it when the app runs on another port.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="password" elementType="Argument">
            <stringProp name="Argument.name">password</stringProp>
            <stringProp name="Argument.value">${__P(password)}</stringProp>
            <stringProp name="Argument.desc">Shared test password. NO DEFAULT ON PURPOSE - type it in the parameter form so it never lives in this file.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="smoke" elementType="Argument">
            <stringProp name="Argument.name">smoke</stringProp>
            <stringProp name="Argument.value">${__P(smoke,true)}</stringProp>
            <stringProp name="Argument.desc">true = read-only steps only. false = also run the steps that write data.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="thinkMin" elementType="Argument">
            <stringProp name="Argument.name">thinkMin</stringProp>
            <stringProp name="Argument.value">${__P(thinkMin,3000)}</stringProp>
            <stringProp name="Argument.desc">Shortest pause between pages, ms. A real user reads before clicking.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="thinkMax" elementType="Argument">
            <stringProp name="Argument.name">thinkMax</stringProp>
            <stringProp name="Argument.value">${__P(thinkMax,8000)}</stringProp>
            <stringProp name="Argument.desc">Longest pause between pages, ms. Must be &gt;= thinkMin.</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">The control panel. Read ONCE at test start and shared by every thread - never put anything per-user here (tokens, ids). Those belong in the CSV or an extractor.</stringProp>
      </Arguments>
      <hashTree/>
      <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
        <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
          <collectionProp name="Arguments.arguments"/>
        </elementProp>
        <stringProp name="HTTPSampler.domain">${host}</stringProp>
        <stringProp name="HTTPSampler.protocol">${protocol}</stringProp>
        <stringProp name="HTTPSampler.port">${port}</stringProp>
        <stringProp name="HTTPSampler.connect_timeout">10000</stringProp>
        <stringProp name="HTTPSampler.response_timeout">60000</stringProp>
        <boolProp name="HTTPSampler.image_parser">false</boolProp>
        <boolProp name="HTTPSampler.concurrentDwn">false</boolProp>
      </ConfigTestElement>
      <hashTree/>
      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="Global Headers" enabled="true">
        <collectionProp name="HeaderManager.headers">
          <elementProp name="" elementType="Header">
            <stringProp name="Header.name">User-Agent</stringProp>
            <stringProp name="Header.value">Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36</stringProp>
          </elementProp>
          <elementProp name="" elementType="Header">
            <stringProp name="Header.name">Accept-Language</stringProp>
            <stringProp name="Header.value">en-US,en;q=0.9</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">Some WAFs answer a request with no User-Agent with a 401. Keep this row.</stringProp>
      </HeaderManager>
      <hashTree/>
      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
        <collectionProp name="CookieManager.cookies"/>
        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
        <boolProp name="CookieManager.controlledByThreadGroup">false</boolProp>
      </CookieManager>
      <hashTree/>
      <CacheManager guiclass="CacheManagerGui" testclass="CacheManager" testname="HTTP Cache Manager" enabled="true">
        <boolProp name="clearEachIteration">true</boolProp>
        <boolProp name="useExpires">true</boolProp>
        <boolProp name="CacheManager.controlledByThread">false</boolProp>
      </CacheManager>
      <hashTree/>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="01 - &lt;PERSONA A&gt;" enabled="true">
        <stringProp name="ThreadGroup.num_threads">${__P(a.users,1)}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${__P(a.ramp,1)}</stringProp>
        <stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <stringProp name="LoopController.loops">${__P(a.loops,1)}</stringProp>
          <boolProp name="LoopController.continue_forever">false</boolProp>
        </elementProp>
        <stringProp name="TestPlan.comments">${__P(...)} sits DIRECTLY in the three fields above - that is what makes LoadLitmus's Load card editable. Do not move them behind a UDV variable. Copy this whole group for a second persona and rename a.users/a.ramp/a.loops to b.*.</stringProp>
      </ThreadGroup>
      <hashTree>
        <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="CSV - users" enabled="true">
          <stringProp name="filename">${__P(user.dir)}/${__P(a.csv,test_data/users.csv)}</stringProp>
          <stringProp name="fileEncoding">UTF-8</stringProp>
          <stringProp name="variableNames">USERNAME</stringProp>
          <boolProp name="ignoreFirstLine">true</boolProp>
          <stringProp name="delimiter">,</stringProp>
          <boolProp name="quotedData">false</boolProp>
          <boolProp name="recycle">true</boolProp>
          <boolProp name="stopThread">false</boolProp>
          <stringProp name="shareMode">shareMode.all</stringProp>
          <stringProp name="TestPlan.comments">user.dir = the folder JMeter starts in. LoadLitmus starts it in the project folder, so this reads data/projects/&lt;slug&gt;/test_data/users.csv; on a slave it reads the slave folder. Never use an absolute path.</stringProp>
        </CSVDataSet>
        <hashTree/>
        <TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TC01_Login" enabled="true">
          <boolProp name="TransactionController.includeTimers">false</boolProp>
          <boolProp name="TransactionController.parent">true</boolProp>
        </TransactionController>
        <hashTree>
          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="01_GET_Login" enabled="true">
            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
              <collectionProp name="Arguments.arguments"/>
            </elementProp>
            <stringProp name="HTTPSampler.path">/login</stringProp>
            <stringProp name="HTTPSampler.method">GET</stringProp>
            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          </HTTPSamplerProxy>
          <hashTree>
            <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Init - every extracted var = NOT_FOUND" enabled="true">
              <stringProp name="scriptLanguage">groovy</stringProp>
              <stringProp name="cacheKey">true</stringProp>
              <stringProp name="script">// Runs once per iteration, before the first request.
// Every variable an extractor fills must start here, so a guard below can
// tell "never extracted" apart from a value left over from the last loop.
["csrf", "landing_url"].each { vars.put(it, "NOT_FOUND") }</stringProp>
            </JSR223PreProcessor>
            <hashTree/>
            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract csrf" enabled="true">
              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
              <stringProp name="RegexExtractor.refname">csrf</stringProp>
              <stringProp name="RegexExtractor.regex">name="_token" value="([^"]+)"</stringProp>
              <stringProp name="RegexExtractor.template">$1$</stringProp>
              <stringProp name="RegexExtractor.default">NOT_FOUND</stringProp>
              <stringProp name="RegexExtractor.match_number">1</stringProp>
            </RegexExtractor>
            <hashTree/>
            <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert HTTP 200" enabled="true">
              <collectionProp name="Asserion.test_strings">
                <stringProp name="49586">200</stringProp>
              </collectionProp>
              <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
              <boolProp name="Assertion.assume_success">false</boolProp>
              <intProp name="Assertion.test_type">8</intProp>
            </ResponseAssertion>
            <hashTree/>
          </hashTree>
          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="02_POST_Login" enabled="true">
            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
              <collectionProp name="Arguments.arguments">
                <elementProp name="_token" elementType="HTTPArgument">
                  <boolProp name="HTTPArgument.always_encode">true</boolProp>
                  <stringProp name="Argument.name">_token</stringProp>
                  <stringProp name="Argument.value">${csrf}</stringProp>
                  <stringProp name="Argument.metadata">=</stringProp>
                  <boolProp name="HTTPArgument.use_equals">true</boolProp>
                </elementProp>
                <elementProp name="username" elementType="HTTPArgument">
                  <boolProp name="HTTPArgument.always_encode">true</boolProp>
                  <stringProp name="Argument.name">username</stringProp>
                  <stringProp name="Argument.value">${USERNAME}</stringProp>
                  <stringProp name="Argument.metadata">=</stringProp>
                  <boolProp name="HTTPArgument.use_equals">true</boolProp>
                </elementProp>
                <elementProp name="password" elementType="HTTPArgument">
                  <boolProp name="HTTPArgument.always_encode">true</boolProp>
                  <stringProp name="Argument.name">password</stringProp>
                  <stringProp name="Argument.value">${password}</stringProp>
                  <stringProp name="Argument.metadata">=</stringProp>
                  <boolProp name="HTTPArgument.use_equals">true</boolProp>
                </elementProp>
              </collectionProp>
            </elementProp>
            <stringProp name="HTTPSampler.path">/login</stringProp>
            <stringProp name="HTTPSampler.method">POST</stringProp>
            <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
            <stringProp name="TestPlan.comments">follow_redirects is OFF so the 302 itself can be asserted. Turn it on only if you do not care where login lands.</stringProp>
          </HTTPSamplerProxy>
          <hashTree>
            <UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="Think Time" enabled="true">
              <stringProp name="ConstantTimer.delay">${thinkMin}</stringProp>
              <stringProp name="RandomTimer.range">${__jexl3(${thinkMax} - ${thinkMin})}</stringProp>
            </UniformRandomTimer>
            <hashTree/>
            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract landing_url (Location header)" enabled="true">
              <stringProp name="RegexExtractor.useHeaders">true</stringProp>
              <stringProp name="RegexExtractor.refname">landing_url</stringProp>
              <stringProp name="RegexExtractor.regex">Location: (.+)</stringProp>
              <stringProp name="RegexExtractor.template">$1$</stringProp>
              <stringProp name="RegexExtractor.default">NOT_FOUND</stringProp>
              <stringProp name="RegexExtractor.match_number">1</stringProp>
            </RegexExtractor>
            <hashTree/>
            <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert not bounced back to login" enabled="true">
              <collectionProp name="Asserion.test_strings">
                <stringProp name="-1943470433">/login</stringProp>
              </collectionProp>
              <stringProp name="Assertion.test_field">Assertion.response_headers</stringProp>
              <boolProp name="Assertion.assume_success">false</boolProp>
              <intProp name="Assertion.test_type">20</intProp>
              <stringProp name="TestPlan.comments">test_type 20 = "Substring" + "Not". A failed login usually answers 200 or 302 back to /login, so checking the code alone is not enough.</stringProp>
            </ResponseAssertion>
            <hashTree/>
            <ResultSaver guiclass="ResultSaverGui" testclass="ResultSaver" testname="Save failed response" enabled="true">
              <stringProp name="FileSaver.filename">${__P(user.dir)}/failed_response/02_login/${USERNAME}</stringProp>
              <boolProp name="FileSaver.errorsonly">true</boolProp>
              <boolProp name="FileSaver.successonly">false</boolProp>
              <boolProp name="FileSaver.skipsuffix">false</boolProp>
              <boolProp name="FileSaver.skipautonumber">true</boolProp>
              <stringProp name="TestPlan.comments">Writes the page ONLY when this step fails. Fleet can collect and clear this folder on every slave after a run.</stringProp>
            </ResultSaver>
            <hashTree/>
          </hashTree>
        </hashTree>
        <IfController guiclass="IfControllerPanel" testclass="IfController" testname="Guard: logged in" enabled="true">
          <stringProp name="IfController.condition">${__jexl3("${landing_url}" != "NOT_FOUND")}</stringProp>
          <boolProp name="IfController.evaluateAll">false</boolProp>
          <boolProp name="IfController.useExpression">true</boolProp>
          <stringProp name="TestPlan.comments">Without this guard, a user whose login failed keeps firing every later request and fills the error rate with noise.</stringProp>
        </IfController>
        <hashTree>
          <TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TC02_&lt;READ_ONLY_JOURNEY&gt;" enabled="true">
            <boolProp name="TransactionController.includeTimers">false</boolProp>
            <boolProp name="TransactionController.parent">true</boolProp>
          </TransactionController>
          <hashTree>
            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="03_GET_&lt;PAGE&gt;" enabled="true">
              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
                <collectionProp name="Arguments.arguments"/>
              </elementProp>
              <stringProp name="HTTPSampler.path">/home</stringProp>
              <stringProp name="HTTPSampler.method">GET</stringProp>
              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
              <stringProp name="TestPlan.comments">Copy this sampler for each page of the journey. Keep the numbering (03, 04, 05...) so the report reads in order.</stringProp>
            </HTTPSamplerProxy>
            <hashTree>
              <UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="Think Time" enabled="true">
                <stringProp name="ConstantTimer.delay">${thinkMin}</stringProp>
                <stringProp name="RandomTimer.range">${__jexl3(${thinkMax} - ${thinkMin})}</stringProp>
              </UniformRandomTimer>
              <hashTree/>
              <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert HTTP 200" enabled="true">
                <collectionProp name="Asserion.test_strings">
                  <stringProp name="49586">200</stringProp>
                </collectionProp>
                <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
                <boolProp name="Assertion.assume_success">false</boolProp>
                <intProp name="Assertion.test_type">8</intProp>
              </ResponseAssertion>
              <hashTree/>
              <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert the page really rendered" enabled="true">
                <collectionProp name="Asserion.test_strings">
                  <stringProp name="-1147692044">&lt;SOMETHING ONLY THIS PAGE CONTAINS&gt;</stringProp>
                </collectionProp>
                <stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
                <boolProp name="Assertion.assume_success">false</boolProp>
                <intProp name="Assertion.test_type">16</intProp>
                <stringProp name="TestPlan.comments">A 200 carrying an error page is still a 200. Check for text only the real page has.</stringProp>
              </ResponseAssertion>
              <hashTree/>
              <ResultSaver guiclass="ResultSaverGui" testclass="ResultSaver" testname="Save failed response" enabled="true">
                <stringProp name="FileSaver.filename">${__P(user.dir)}/failed_response/03_page/${USERNAME}</stringProp>
                <boolProp name="FileSaver.errorsonly">true</boolProp>
                <boolProp name="FileSaver.successonly">false</boolProp>
                <boolProp name="FileSaver.skipsuffix">false</boolProp>
                <boolProp name="FileSaver.skipautonumber">true</boolProp>
              </ResultSaver>
              <hashTree/>
            </hashTree>
          </hashTree>
          <IfController guiclass="IfControllerPanel" testclass="IfController" testname="Flag: smoke==false (steps that WRITE data)" enabled="true">
            <stringProp name="IfController.condition">${__jexl3("${smoke}" == "false")}</stringProp>
            <boolProp name="IfController.evaluateAll">false</boolProp>
            <boolProp name="IfController.useExpression">true</boolProp>
            <stringProp name="TestPlan.comments">Everything that creates, submits or pays sits in here. A smoke run leaves the system untouched; the full run flips smoke to false.</stringProp>
          </IfController>
          <hashTree>
            <TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TC03_&lt;SUBMIT&gt;" enabled="true">
              <boolProp name="TransactionController.includeTimers">false</boolProp>
              <boolProp name="TransactionController.parent">true</boolProp>
            </TransactionController>
            <hashTree>
              <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="04_POST_&lt;SUBMIT&gt;" enabled="true">
                <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
                  <collectionProp name="Arguments.arguments">
                    <elementProp name="_token" elementType="HTTPArgument">
                      <boolProp name="HTTPArgument.always_encode">true</boolProp>
                      <stringProp name="Argument.name">_token</stringProp>
                      <stringProp name="Argument.value">${csrf}</stringProp>
                      <stringProp name="Argument.metadata">=</stringProp>
                      <boolProp name="HTTPArgument.use_equals">true</boolProp>
                    </elementProp>
                  </collectionProp>
                </elementProp>
                <stringProp name="HTTPSampler.path">/submit</stringProp>
                <stringProp name="HTTPSampler.method">POST</stringProp>
                <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
                <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
              </HTTPSamplerProxy>
              <hashTree>
                <UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="Think Time" enabled="true">
                  <stringProp name="ConstantTimer.delay">${thinkMin}</stringProp>
                  <stringProp name="RandomTimer.range">${__jexl3(${thinkMax} - ${thinkMin})}</stringProp>
                </UniformRandomTimer>
                <hashTree/>
                <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert submit accepted" enabled="true">
                  <collectionProp name="Asserion.test_strings">
                    <stringProp name="-1147692045">&lt;TEXT THAT PROVES IT WAS SAVED&gt;</stringProp>
                  </collectionProp>
                  <stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
                  <boolProp name="Assertion.assume_success">false</boolProp>
                  <intProp name="Assertion.test_type">16</intProp>
                </ResponseAssertion>
                <hashTree/>
                <ResultSaver guiclass="ResultSaverGui" testclass="ResultSaver" testname="Save failed response" enabled="true">
                  <stringProp name="FileSaver.filename">${__P(user.dir)}/failed_response/04_submit/${USERNAME}</stringProp>
                  <boolProp name="FileSaver.errorsonly">true</boolProp>
                  <boolProp name="FileSaver.successonly">false</boolProp>
                  <boolProp name="FileSaver.skipsuffix">false</boolProp>
                  <boolProp name="FileSaver.skipautonumber">true</boolProp>
                </ResultSaver>
                <hashTree/>
              </hashTree>
            </hashTree>
          </hashTree>
        </hashTree>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
