elastic / elasticsearch-js-mock

Mock utility for the Elasticsearch's Node.js client
https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-testing.html
Apache License 2.0
47 stars 13 forks source link

Mocking a "scroll" search #16

Closed abrin closed 3 years ago

abrin commented 3 years ago

Hi, I really appreciate the mocking library, I've been trying to mock a scroll request... To test it, I want to return the _scroll_id. But, when I include it, I get the error below. Wondering if someone can either point me at an example of how to mock this, or suggest a way forward?

    ResponseError: Response Error

      at Class.<anonymous> (node_modules/@elastic/elasticsearch/lib/Transport.js:257:25)
      at endReadableNT (node_modules/readable-stream/lib/_stream_readable.js:1010:12)

Here's how I'm trying to send the mock in

    esmock.add(
      {
        method: ["GET", "POST"],
        path: "/:index/_search"
      },
      () => {
        return {
          _scroll_id:
            "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAQWSlpLQUZQbDNTSktXMWxWaDNJcnZZUQ==",
          hits: {
            total: { value: 1, relation: "eq" },
            max_score: 1.0,
            hits: [
              {
                _index: "test",
                _type: "doc",
                _id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a",
                _score: 1.0,
                _source: {
                  id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a"
                }
              }
            ]
          }
        };
      }
    );
    esmock.add(
      {
        method: ["GET", "POST"],
        path: "/_search/scroll"
      },
      () => {
        return {
          _scroll_id:
            "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAQWSlpLQUZQbDNTSktXMWxWaDNJcnZZUQ==",
          hits: { hits: [] }
        };
      }
    );
delvedor commented 3 years ago

Hello! It depends on how you are sending the scroll request. You can either send the _scroll_id as URL parameter, or in the body. I highly recommend using the body, as scroll ids can be quite long.

// send the scroll_id in the URL
client.scroll({
  scroll_id: '<id>',
  scroll: '30s'
}, console.log)

// send the scroll_id in the body
client.scroll({
  scroll: '30s',
  body: {
    scroll_id: '<id>'
  }
}, console.log)

If you want to use the URL params way, the mock should be updated as follows:

mock.add({
  method: 'GET',
  path: '/_search/scroll/:id'
}, () => {
  return {
    _scroll_id: 'id',
    hits: { hits: [] }
  }
})
abrin commented 3 years ago

@delvedor I gave that a try, but it doesn't seem to quite work... For clarification (using v7.9.1 of Elasticsearch and 0.3.0 of the mocking library), and following the pattern from here. Below is the full test-case.

// import * as elasticsearch from "./elasticsearch";
const Mock = require("@elastic/elasticsearch-mock");
import { Client } from "@elastic/elasticsearch";
const esmock = new Mock();

const esclient = new Client({
  node: "http://localhost:9200",
  Connection: esmock.getConnection()
});
// elasticsearch.setClient(esclient);

async function* scrollSearch(params) {
  let response = await esclient.search(params);
  while (true) {
    const sourceHits = response.body.hits.hits;

    if (sourceHits.length === 0) {
      break;
    }

    for (const hit of sourceHits) {
      yield hit._source;
    }

    if (!response.body._scroll_id) {
      break;
    }

    response = await esclient.scroll({
      scroll: params.scroll,
      body: {
        scroll_id: response.body._scroll_id
      }
    });
  }
}

describe("it tests elasticsearch", () => {
  it("tests scroll", async () => {
    esmock.add(
      {
        method: ["GET", "POST"],
        path: "/_search"
      },
      () => {
        return {
          _scroll_id: "<id>",
          hits: {
            total: { value: 1, relation: "eq" },
            max_score: 1.0,
            hits: [
              {
                _index: "test",
                _type: "doc",
                _id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a",
                _score: 1.0,
                _source: {
                  id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a"
                }
              }
            ]
          }
        };
      }
    );
    esmock.add(
      {
        method: "GET",
        path: "/_search/scroll/:id"
      },
      () => {
        return {
          hits: [
            {
              _index: "test",
              _type: "doc",
              _id: "component/aaaaaaa-25fe-5b68-9e8a-361b4e13185a",
              _score: 1.0,
              _source: {
                id: "component/aaaaaaa-25fe-5b68-9e8a-361b4e13185a"
              }
            }
          ]
        };
      }
    );
    let params = {
      scroll: "30s",
      size: 1,
      body: {
        query: {
          term: {
            rel_ids: "123"
          }
        }
      }
    };
    let count = 0;
    for await (const hit of scrollSearch(params)) {
      console.log(hit);
      count++;
    }
    expect(count).toEqual(2);
  });
});

The result in jest:

 FAIL  src/indexer/utils/elasticsearch.spec.js
  it tests elasticsearch
    ✕ tests scroll (41ms)

  ● it tests elasticsearch › tests scroll

    ResponseError: Response Error

      at Class.response.on (node_modules/@elastic/elasticsearch/lib/Transport.js:257:25)
      at endReadableNT (node_modules/readable-stream/lib/_stream_readable.js:1010:12)

  console.log src/indexer/utils/elasticsearch.spec.js:100
    { id: 'component/f745b673-25fe-5b68-9e8a-361b4e13185a' }
delvedor commented 3 years ago

@abrin you are now passing the scroll_id in the body, so you need to mock the POST method as well.

abrin commented 3 years ago

changing the mock to allow ['GET','POST'] still produces a transport the same error. Simply adding the _scroll_id in the initial search response seems to be what causes it:

    esmock.add(
      {
        method: ["GET", "POST"],
        path: "/_search",
      },
      () => {
        return {
          _scroll_id: "<id>",
          hits: {
delvedor commented 3 years ago

You must add the POST method to the scroll endpoint mock :)

abrin commented 3 years ago

Ok! got it... for posterity, and in case it's useful... here's the final constructed test before I cleanup and add local functionality. Thanks!

// setup the mock and the client
const Mock = require("@elastic/elasticsearch-mock");
import { Client } from "@elastic/elasticsearch";
const esmock = new Mock();

const esclient = new Client({
  node: "http://localhost:9200",
  Connection: esmock.getConnection(),
});

// the base scrollSearch method from the documentation
async function* scrollSearch(params) {
  let response = await esclient.search(params);
  while (true) {
    const sourceHits = response.body.hits.hits;

    if (sourceHits.length === 0) {
      break;
    }

    for (const hit of sourceHits) {
      yield hit._source;
    }

    if (!response.body._scroll_id) {
      break;
    }

    response = await esclient.scroll({
      scroll: params.scroll,
      body: {
        scroll_id: response.body._scroll_id,
      },
    });
  }
}

describe("it tests elasticsearch", () => {
  it("tests scroll", async () => {

   // mock the search call (which should be the 1st call
    esmock.add(
      {
        method: ["POST"],
        path: "/_search",
      },
      () => {
        return {
          _scroll_id: "<id>",
          hits: {
            total: { value: 1, relation: "eq" },
            max_score: 1.0,
            hits: [
              {
                _index: "test",
                _type: "doc",
                _id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a",
                _score: 1.0,
                _source: {
                  id: "component/f745b673-25fe-5b68-9e8a-361b4e13185a",
                },
              },
            ],
          },
        };
      }
    );

   // mock the subsequent "scroll" calls... we'll return a result the 1st time, and then the second return nothing.
    var calls = 0;
    esmock.add(
      {
        method: ["POST"],
        path: "/_search/scroll",
      },
      () => {
        if (calls > 0) {
          return {
            hits: { hits: [] },
            _scroll_id: "<id>",
            total: { value: 0, relation: "eq" },
          };
        }
        calls++;
        return {
          _scroll_id: "<id>",
          hits: {
            total: { value: 1, relation: "eq" },
            hits: [
              {
                _index: "test",
                _type: "doc",
                _id: "component/aaaaaaa-25fe-5b68-9e8a-361b4e13185a",
                _score: 1.0,
                _source: {
                  id: "component/aaaaaaa-25fe-5b68-9e8a-361b4e13185a",
                },
              },
            ],
          },
        };
      }
    );

//  this is the initial search
    let params = {
      scroll: "30s",
      size: 1,
      body: {
        query: {
          term: {
            rel_ids: "123",
          },
        },
      },
    };
    let count = 0;
    for await (const hit of scrollSearch(params)) {
      count++;
    }
    expect(count).toEqual(2);
  });
});