{
  "info": {
    "name": "Lookup Service — Credit Data Aggregation",
    "_postman_id": "b7e41d2a-3c58-4f19-9a6d-0e2f8c134a55",
    "description": "Exercises the Lookup Service: one SSN in, one merged credit record out, with a cache governed by Cache-Control on both sides of the exchange.\n\n**Base URL** is a collection variable. It defaults to the deployed instance; switch it to `http://localhost:8080` to run against a local build.\n\n### Folders\n\n**1 · The service** — what it returns. Run these in any order.\n\n**2 · Cache behaviour** — these are order-dependent and share state. Use *Run collection* on this folder rather than firing them individually; each request stores what it saw so the next one can compare.\n\n### The trick these tests rely on\n\nThe upstream randomises the **house number** on every response it actually serves. Everything else — name, street, income, debt — is fixed. So if two calls return the same house number, the second was answered from the service's own storage; if it changes, the service went to the source. Every cache assertion below is that one comparison.\n\nTwo requests wait several seconds in a pre-request script to let entries age. That is deliberate, not a hang.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "baseUrl", "value": "https://cdas.weget.cc", "type": "string" },
    { "key": "emma",  "value": "424-11-9327", "type": "string" },
    { "key": "billy", "value": "553-25-8346", "type": "string" },
    { "key": "gail",  "value": "287-54-7823", "type": "string" },
    { "key": "lastAddress", "value": "", "type": "string" }
  ],
  "item": [
    {
      "name": "1 · The service",
      "item": [
        {
          "name": "Health check",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/ping", "host": ["{{baseUrl}}"], "path": ["ping"] },
            "description": "Runs a real query against the cache database rather than returning a constant, so it fails when storage is unreachable. A health check that cannot fail is worse than none."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('reports ok', () => pm.expect(pm.response.json().status).to.eql('ok'));"
            ]}
          }]
        },
        {
          "name": "Look up Emma",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "One request, one complete record — assembled behind the scenes from three separate upstream endpoints (personal details, debt, assessed income). The caller never sees that."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const b = pm.response.json();",
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('all six fields present', () => {",
              "  pm.expect(b).to.have.all.keys('first_name','last_name','address','assessed_income','balance_of_debt','complaints');",
              "});",
              "pm.test('identity from personal-details', () => {",
              "  pm.expect(b.first_name).to.eql('Emma');",
              "  pm.expect(b.last_name).to.eql('Gautrey');",
              "});",
              "pm.test('figures from debt and assessed-income', () => {",
              "  pm.expect(b.assessed_income).to.eql(60668);",
              "  pm.expect(b.balance_of_debt).to.eql(11585);",
              "  pm.expect(b.complaints).to.eql(true);",
              "});",
              "pm.test('cache state is reported', () => {",
              "  pm.expect(pm.response.headers.get('X-Cache')).to.be.oneOf(['HIT','MISS']);",
              "});",
              "console.log('X-Cache:', pm.response.headers.get('X-Cache'), '| Age:', pm.response.headers.get('Age') + 's');"
            ]}
          }]
        },
        {
          "name": "Look up Billy",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{billy}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{billy}}"] },
            "description": "A person with almost no debt and no complaints. His identity record carries max-age=5 from the source, so it is only reusable for five seconds."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const b = pm.response.json();",
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('is Billy Brinegar', () => pm.expect(b.first_name + ' ' + b.last_name).to.eql('Billy Brinegar'));",
              "pm.test('no complaints on file', () => pm.expect(b.complaints).to.eql(false));"
            ]}
          }]
        },
        {
          "name": "Look up Gail",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{gail}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{gail}}"] },
            "description": "Her identity record is marked no-store by the source, so that part is always fetched live — while her debt and income may still be served from storage. One record, two freshness states at once."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const b = pm.response.json();",
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('is Gail Shick', () => pm.expect(b.first_name + ' ' + b.last_name).to.eql('Gail Shick'));",
              "pm.test('never a full cache hit — identity is no-store', () => {",
              "  pm.expect(pm.response.headers.get('X-Cache')).to.eql('MISS');",
              "});"
            ]}
          }]
        },
        {
          "name": "Unknown person → 404",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/000-00-0000", "host": ["{{baseUrl}}"], "path": ["credit-data", "000-00-0000"] },
            "description": "Every source must answer for a record to exist. A partial answer would be worse than none, so aggregation is all-or-nothing. Negative results are never cached — someone unknown today may be on file tomorrow."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "pm.test('404 Not Found', () => pm.response.to.have.status(404));",
              "pm.test('machine-readable error', () => pm.expect(pm.response.json()).to.have.property('code'));"
            ]}
          }]
        },
        {
          "name": "Malformed SSN → 404",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/not-an-ssn", "host": ["{{baseUrl}}"], "path": ["credit-data", "not-an-ssn"] },
            "description": "Rejected on shape before any upstream call is made. An impossible identifier cannot have a record, so spending three network round-trips to confirm that would be waste."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "pm.test('404 Not Found', () => pm.response.to.have.status(404));"
            ]}
          }]
        }
      ]
    },
    {
      "name": "2 · Cache behaviour",
      "description": "Order-dependent — run the folder, don't fire these individually. Each request records the house number it saw so the next can compare.",
      "item": [
        {
          "name": "① Seed the cache",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "Establishes a stored entry and remembers the house number for the next request to compare against."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "pm.collectionVariables.set('lastAddress', addr);",
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "console.log('seeded with:', addr);"
            ]}
          }]
        },
        {
          "name": "② Repeat → served from storage",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "The source states Cache-Control: private with no max-age — no freshness lifetime at all. The service applies its own configured default, which is what makes this source cacheable in the first place. Same house number proves the response never left the service."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.test('house number unchanged → answered from storage', () => pm.expect(addr).to.eql(prev));",
              "pm.test('X-Cache reports HIT', () => pm.expect(pm.response.headers.get('X-Cache')).to.eql('HIT'));",
              "console.log(prev, '→', addr, '| X-Cache:', pm.response.headers.get('X-Cache'));"
            ]}
          }]
        },
        {
          "name": "③ Caller opts out → no-store",
          "request": {
            "method": "GET",
            "header": [
              { "key": "Cache-Control", "value": "no-store, 604800", "description": "Deliberately malformed: 604800 is a bare number, not max-age=604800. It must be ignored without breaking the directive beside it." }
            ],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "The caller instructs the service not to retain this exchange. It must bypass storage AND discard what was already held — otherwise the next caller receives data someone asked us not to keep."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.collectionVariables.set('lastAddress', addr);",
              "pm.test('bypassed storage → fetched fresh', () => pm.expect(addr).to.not.eql(prev));",
              "pm.test('X-Cache reports MISS', () => pm.expect(pm.response.headers.get('X-Cache')).to.eql('MISS'));"
            ]}
          }]
        },
        {
          "name": "④ …and left nothing behind",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "The follow-up carries no directives at all. It must still miss — proving the previous no-store exchange stored nothing and evicted what was there. This is the case a passing end-to-end suite can miss entirely."
          },
          "event": [{
            "listen": "test",
            "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.collectionVariables.set('lastAddress', addr);",
              "pm.test('still a miss → nothing was retained', () => pm.expect(addr).to.not.eql(prev));",
              "pm.test('X-Cache reports MISS', () => pm.expect(pm.response.headers.get('X-Cache')).to.eql('MISS'));"
            ]}
          }]
        },
        {
          "name": "⑤ Caller demands fresher (waits 4s)",
          "request": {
            "method": "GET",
            "header": [
              { "key": "Cache-Control", "value": "private, max-age=3", "description": "private is a response directive and is ignored here. max-age=3 means: nothing older than three seconds." }
            ],
            "url": { "raw": "{{baseUrl}}/credit-data/{{emma}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{emma}}"] },
            "description": "The stored entry is well within its lifetime, but this caller will not accept anything older than three seconds. After a four-second wait it must be fetched again.\n\nThe pre-request script pauses for 4s — that is deliberate, not a hang."
          },
          "event": [
            { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [
              "console.log('waiting 4s so the stored entry ages past max-age=3…');",
              "await new Promise(r => setTimeout(r, 4000));"
            ]}},
            { "listen": "test", "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.collectionVariables.set('lastAddress', addr);",
              "pm.test('entry was 4s old, caller allowed 3 → refetched', () => pm.expect(addr).to.not.eql(prev));"
            ]}}
          ]
        },
        {
          "name": "⑥ Source TTL expires (waits 6s)",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{billy}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{billy}}"] },
            "description": "Billy's identity record is granted five seconds of freshness by the source. This request seeds it, waits six seconds, then asks again — so the stored copy must be treated as stale.\n\nThe pre-request script performs the seed call and the 6s wait."
          },
          "event": [
            { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [
              "const url = pm.collectionVariables.get('baseUrl') + '/credit-data/' + pm.collectionVariables.get('billy');",
              "await new Promise(resolve => {",
              "  pm.sendRequest(url, (err, res) => {",
              "    if (!err) {",
              "      pm.collectionVariables.set('lastAddress', res.json().address);",
              "      console.log('seeded Billy with:', res.json().address, '— waiting 6s past his 5s lifetime…');",
              "    }",
              "    resolve();",
              "  });",
              "});",
              "await new Promise(r => setTimeout(r, 6000));"
            ]}},
            { "listen": "test", "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.test('lifetime elapsed → fetched again', () => pm.expect(addr).to.not.eql(prev));",
              "console.log(prev, '→', addr);"
            ]}}
          ]
        },
        {
          "name": "⑦ Source forbids storage outright",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/credit-data/{{gail}}", "host": ["{{baseUrl}}"], "path": ["credit-data", "{{gail}}"] },
            "description": "Gail's identity record is marked no-store by the source. However often it is asked, that part must come from the source every time. The pre-request script calls once first so this response can be compared against it."
          },
          "event": [
            { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [
              "const url = pm.collectionVariables.get('baseUrl') + '/credit-data/' + pm.collectionVariables.get('gail');",
              "await new Promise(resolve => {",
              "  pm.sendRequest(url, (err, res) => {",
              "    if (!err) pm.collectionVariables.set('lastAddress', res.json().address);",
              "    resolve();",
              "  });",
              "});"
            ]}},
            { "listen": "test", "script": { "type": "text/javascript", "exec": [
              "const addr = pm.response.json().address;",
              "const prev = pm.collectionVariables.get('lastAddress');",
              "pm.test('different every time → never stored', () => pm.expect(addr).to.not.eql(prev));",
              "pm.test('X-Cache reports MISS', () => pm.expect(pm.response.headers.get('X-Cache')).to.eql('MISS'));"
            ]}}
          ]
        }
      ]
    }
  ]
}
