meteor/meteor

View on GitHub
packages/deprecated/http/httpcall_tests.js

Summary

Maintainability
D
2 days
Test Coverage
import { HTTP } from 'meteor/http';

// URL prefix for tests to talk to
let _XHR_URL_PREFIX = '/http_test_responder';

const url_base = function () {
  if (Meteor.isServer) {
    const address = WebApp.httpServer.address();
    return 'http://127.0.0.1:' + address.port;
  } else {
    return '';
  }
}

const url_prefix = function () {
  if (Meteor.isServer && _XHR_URL_PREFIX.indexOf('http') !== 0) {
    _XHR_URL_PREFIX = url_base() + _XHR_URL_PREFIX;
  }
  return _XHR_URL_PREFIX;
}

testAsyncMulti('httpcall - basic', [
  function (test, expect) {
    const basic_get = function (url, options, expected_url) {
      const callback = function (error, result) {
        test.isFalse(error);
        if (!error) {
          test.equal(typeof result, 'object');
          test.equal(result.statusCode, 200);

          const data = result.data;

          // allow dropping of final ? (which mobile browsers seem to do)
          const allowed = [expected_url];
          if (expected_url.slice(-1) === '?') {
            allowed.push(expected_url.slice(0, -1));
          }

          test.include(allowed, expected_url);
          test.equal(data.method, 'GET');
        }
      };

      HTTP.call('GET', url_prefix() + url, options, expect(callback));

      if (Meteor.isServer) {
        // test sync version
        try {
          const result = HTTP.call('GET', url_prefix() + url, options);
          callback(undefined, result);
        } catch (e) {
          callback(e, e.response);
        }
      }
    };

    basic_get('/foo', null, '/foo');
    basic_get('/foo?', null, '/foo?');
    basic_get('/foo?a=b', null, '/foo?a=b');
    basic_get('/foo', { params: { fruit: 'apple' } }, '/foo?fruit=apple');
    basic_get('/foo', {
      params: {
        fruit: 'apple',
        dog: 'Spot the dog'
      }
    }, '/foo?fruit=apple&dog=Spot+the+dog');
    basic_get('/foo?', {
      params: {
        fruit: 'apple',
        dog: 'Spot the dog'
      }
    }, '/foo?fruit=apple&dog=Spot+the+dog');
    basic_get('/foo?bar', {
      params: {
        fruit: 'apple',
        dog: 'Spot the dog'
      }
    }, '/foo?bar&fruit=apple&dog=Spot+the+dog');
    basic_get('/foo?bar', {
      params: { fruit: 'apple', dog: 'Spot the dog' },
      query: 'baz'
    }, '/foo?baz&fruit=apple&dog=Spot+the+dog');
    basic_get('/foo', {
      params: { fruit: 'apple', dog: 'Spot the dog' },
      query: 'baz'
    }, '/foo?baz&fruit=apple&dog=Spot+the+dog');
    basic_get('/foo?', {
      params: { fruit: 'apple', dog: 'Spot the dog' },
      query: 'baz'
    }, '/foo?baz&fruit=apple&dog=Spot+the+dog');
    basic_get('/foo?bar', { query: '' }, '/foo?');
    basic_get('/foo?bar', {
      params: { fruit: 'apple', dog: 'Spot the dog' },
      query: ''
    }, '/foo?fruit=apple&dog=Spot+the+dog');
  }]);

testAsyncMulti('httpcall - errors', [
  function (test, expect) {
    // Accessing unknown server (should fail to make any connection)
    const unknownServerCallback = function (error, result) {
      test.equal(!!error, true,'expected error');
      test.equal(!!result, false,'expected no result');
      test.equal(!!error.response, false, 'expected no response');
    }

    const invalidIp = '0.0.0.199';
    // This is an invalid destination IP address, and thus should always give an error.
    // If your ISP is intercepting DNS misses and serving ads, an obviously
    // invalid URL (http://asdf.asdf) might produce an HTTP response.
    HTTP.call('GET', `http://${invalidIp}/`, expect(unknownServerCallback));

    if (Meteor.isServer) {
      // test sync version
      try {
        const unknownServerResult = HTTP.call('GET', `http://${invalidIp}/`);
        unknownServerCallback(undefined, unknownServerResult);
      } catch (e) {
        unknownServerCallback(e, e.response);
      }
    }

    // Server serves 500
    const error500Callback = function (error, result) {
      test.equal(!!error, true, 'expect error');
      test.equal(error.message.includes('500'), true, 'expect 500'); // message has statusCode
      test.equal(error.message.includes(error.response.content.substring(0, 10)), true, 'expect res content in message'); // message has part of content

      test.isTrue(result);
      test.isTrue(!!error.response);
      test.equal(result, error.response);
      test.equal(error.response.statusCode, 500);

      // in test_responder.js we make a very long response body, to make sure
      // that we truncate messages. first of all, make sure we didn't make that
      // message too short, so that we can be sure we're verifying that we truncate.
      test.isTrue(error.response.content.length > 520);
      test.isTrue(error.message.length < 520); // make sure we truncate.
    }

    HTTP.call('GET', url_prefix() + '/fail', expect(error500Callback));

    if (Meteor.isServer) {
      // test sync version
      try {
        const error500Result = HTTP.call('GET', url_prefix() + '/fail');
        error500Callback(undefined, error500Result);
      } catch (e) {
        error500Callback(e, e.response);
      }
    }
  }
]);


testAsyncMulti('httpcall - timeout', [
  function (test, expect) {

    // Should time out
    const timeoutCallback = function (error, result) {
      test.isTrue(error);
      test.isFalse(result);
      test.isFalse(error.response);
    }
    const timeoutUrl = url_prefix() + '/slow-' + Random.id();
    HTTP.call(
      'GET', timeoutUrl,
      { timeout: 500 },
      expect(timeoutCallback));

    if (Meteor.isServer) {
      // test sync version
      try {
        const timeoutResult = HTTP.call('GET', timeoutUrl, { timeout: 500 });
        timeoutCallback(undefined, timeoutResult);
      } catch (e) {
        timeoutCallback(e, e.response);
      }
    }

    // Should not time out
    const noTimeoutCallback = function (error, result) {
      test.isFalse(error);
      test.isTrue(result);
      test.equal(result.statusCode, 200);

      const data = result.data;
      test.isTrue(!!data);
      test.equal(data.url.substring(0, 4), '/foo');
      test.equal(data.method, 'GET');
    }
    const noTimeoutUrl = url_prefix() + '/foo-' + Random.id();
    HTTP.call('GET', noTimeoutUrl, { timeout: 2000 }, expect(noTimeoutCallback));
    if (Meteor.isServer) {
      // test sync version
      try {
        const noTimeoutResult = HTTP.call('GET', noTimeoutUrl, { timeout: 2000 });
        noTimeoutCallback(undefined, noTimeoutResult);
      } catch (e) {
        noTimeoutCallback(e, e.response);
      }
    }
  }
]);

testAsyncMulti('httpcall - redirect', [

  function (test, expect) {
    // Test that we follow redirects by default
    HTTP.call('GET', url_prefix() + '/redirect', expect(
      function (error, result) {
        test.equal(!!error, false, 'expected no error');
        test.equal(!!result, true, 'expected result');

        // should be redirected transparently to /foo
        test.equal(result.statusCode, 200);
        const data = result.data;
        test.equal(data.url, '/foo');
        test.equal(data.method, 'GET');
      }))

    // followRedirect option; can't be false on client
    _.each([false, true], function (followRedirects) {
      const do_it = function (should_work) {
        const maybe_expect = should_work ? expect : _.identity;
        _.each(['GET', 'POST'], function (method) {
          HTTP.call(
            method, url_prefix() + '/redirect',
            { followRedirects: followRedirects },
            maybe_expect(function (error, result) {
              test.equal(!!error, false, 'expected no error');
              test.equal(!!result, true, 'expected result');

              if (followRedirects) {
                // should be redirected transparently to /foo
                test.equal(result.statusCode, 200);
                const data = result.data;
                test.equal(data.url, '/foo');
                // This is "GET" even when the initial request was a
                // POST because browsers follow redirects with a GET
                // even when the initial request was a different method.
                test.equal(data.method, 'GET');
              } else {
                // should see redirect
                test.equal(result.statusCode, 301);
              }
            }));
        });
      }
      if (Meteor.isClient && !followRedirects) {
        // not supported, should fail
        test.throws(do_it);
      } else {
        do_it(true);
      }
    })
  }

])

testAsyncMulti('httpcall - methods', [

  function (test, expect) {
    // non-get methods
    const test_method = function (meth, func_name) {
      func_name = func_name || meth.toLowerCase();
      HTTP[func_name](
        url_prefix() + '/foo',
        expect(function (error, result) {
          test.isFalse(error);
          test.isTrue(result);
          test.equal(result.statusCode, 200);
          const data = result.data;
          test.equal(data.url, '/foo');
          test.equal(data.method, meth);
        }));
    }

    test_method('GET');
    test_method('POST');
    test_method('PUT');
    test_method('DELETE', 'del');
    test_method('PATCH');
  },

  function (test, expect) {
    // contents and data
    HTTP.call(
      'POST', url_prefix() + '/foo',
      { content: 'Hello World!' },
      expect(function (error, result) {
        test.isFalse(error);
        test.isTrue(result);
        test.equal(result.statusCode, 200);
        const data = result.data;
        test.equal(data.body, 'Hello World!');
      }));

    HTTP.call(
      'POST', url_prefix() + '/data-test',
      { data: { greeting: 'Hello World!' } },
      expect(function (error, result) {
        test.isFalse(error);
        test.isTrue(result);
        test.equal(result.statusCode, 200);
        const data = result.data;
        test.equal(data.body, { greeting: 'Hello World!' });
        // nb: some browsers include a charset here too.
        test.matches(data.headers['content-type'], /^application\/json\b/);
      }));

    HTTP.call(
      'POST', url_prefix() + '/data-test-explicit',
      {
        data: { greeting: 'Hello World!' },
        headers: { 'Content-Type': 'text/stupid' }
      },
      expect(function (error, result) {
        test.isFalse(error);
        test.isTrue(result);
        test.equal(result.statusCode, 200);
        const data = result.data;
        test.equal(data.body, { greeting: 'Hello World!' });
        // nb: some browsers include a charset here too.
        test.matches(data.headers['content-type'], /^text\/stupid\b/);
      }));
  }
]);

testAsyncMulti('httpcall - http auth', [
  function (test, expect) {
    // Test basic auth

    // Unfortunately, any failed auth will result in a browser
    // password prompt.  So we don't test auth failure, only
    // success.

    // Random password breaks in Firefox, because Firefox incorrectly
    // uses cached credentials even if we supply different ones:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=654348
    const password = 'rocks';
    //const password = Random.id().replace(/[^0-9a-zA-Z]/g, '');
    HTTP.call(
      'GET', url_prefix() + '/login?' + password,
      { auth: 'meteor:' + password },
      expect(function (error, result) {
        // should succeed
        test.isFalse(error);
        test.isTrue(result);
        test.equal(result.statusCode, 200);
        const data = result.data;
        test.equal(data.url, '/login?' + password);
      }))

    // test fail on malformed username:password
    test.throws(function () {
      HTTP.call(
        'GET', url_prefix() + '/login?' + password,
        { auth: 'fooooo' },
        function () { throw new Error('can\'t get here'); })
    });
  }
]);

testAsyncMulti('httpcall - headers', [
  function (test, expect) {
    HTTP.call(
      'GET', url_prefix() + '/foo-with-headers',
      {
        headers: {
          'Test-header': 'Value',
          'another': 'Value2'
        }
      },
      expect(function (error, result) {
        test.equal(!!error, false);
        test.equal(!!result, true);

        test.equal(result.statusCode, 200);

        const data = result.data;
        test.equal(data.url, '/foo-with-headers');
        test.equal(data.method, 'GET');
        test.equal(data.headers['test-header'], 'Value');
        test.equal(data.headers['another'], 'Value2');
      }))

    HTTP.call(
      'GET', url_prefix() + '/headers',
      expect(function (error, result) {
        test.equal(!!error, false);
        test.equal(!!result, true);

        test.equal(result.statusCode, 201);
        test.equal(result.headers['a-silly-header'], 'Tis a');
        test.equal(result.headers['another-silly-header'], 'Silly place.');
      }))
  }
])

testAsyncMulti('httpcall - params', [
  function (test, expect) {
    const do_test = function (method, url, params, opt_opts, expect_url, expect_body) {
      let opts = {};
      if (typeof opt_opts === 'string') {
        // opt_opts omitted
        expect_body = expect_url;
        expect_url = opt_opts;
      } else {
        opts = opt_opts;
      }
      HTTP.call(
        method, url_prefix() + url,
        _.extend({ params: params }, opts),
        expect(function (error, result) {
          test.isFalse(error);
          test.isTrue(result);
          test.equal(result.statusCode, 200);
          if (method !== 'HEAD') {
            const data = result.data;
            test.equal(data.method, method);
            test.equal(data.url, expect_url);
            test.equal(data.body, expect_body, `${method} ${url} ${JSON.stringify(params)} - expect body`);
          }
        }))
    }

    do_test('GET', '/', { foo: 'bar', fruit: 'apple' }, '/?foo=bar&fruit=apple', '');
    do_test('GET', '/', { 'foo?': 'bang?' }, {}, '/?foo%3F=bang%3F', '');
    do_test('GET', '/blah', { foo: 'bar' }, '/blah?foo=bar', '');

    do_test('POST', '/', { foo: 'bar', fruit: 'apple' }, '/', 'foo=bar&fruit=apple');
    do_test('POST', '/', { 'foo?': 'bang?' }, {}, '/', 'foo%3F=bang%3F');
    do_test('POST', '/', { foo: 'bar', fruit: 'apple' }, { content: 'stuff!' }, '/?foo=bar&fruit=apple', 'stuff!');
    do_test('POST', '/', { foo: 'bar', greeting: 'Hello World' }, { content: 'stuff!' }, '/?foo=bar&greeting=Hello+World', 'stuff!');
    do_test('POST', '/foo', { foo: 'bar', greeting: 'Hello World' }, '/foo', 'foo=bar&greeting=Hello+World');

    do_test('HEAD', '/head', { foo: 'bar' }, '/head?foo=bar', '');

    do_test('PUT', '/put', { foo: 'bar' }, '/put', 'foo=bar');
  }
]);

Meteor.isClient && testAsyncMulti('httpcall - beforeSend', [
  function (test, expect) {
    let fired = false;
    const bSend = function (xhr) {
      test.isFalse(fired);
      fired = true;
      test.isTrue(xhr instanceof XMLHttpRequest);
    }

    HTTP.get(url_prefix() + '/', { beforeSend: bSend }, expect(function () {
      test.isTrue(fired);
    }));
  }
]);

if (Meteor.isServer) {
  // This is testing the server's static file sending code, not the http
  // package. It's here because it is very similar to the other tests
  // here, even though it is testing something else.
  //
  // client http library mangles paths before they are requested. only
  // run this test on the server.
  testAsyncMulti('httpcall - static file serving', [
    function (test, expect) {
      // Suppress error printing for this test (and for any other code that sets
      // the x-suppress-error header).
      WebApp.suppressConnectErrors();

      function do_test (path, code, match) {
        const prefix = Meteor.isModern
          ? '' // No prefix for web.browser (modern).
          : '/__browser.legacy';

        const options = { headers: { 'x-suppress-error': 'true' } };
        HTTP.get(url_base() + prefix + path, options, expect(function (error, result) {
          test.equal(result.statusCode, code, 'code');
          if (match) {
            test.matches(result.content, match, 'content match');
          }
        }));
      }

      // existing static file
      //do_test('/packages/local-test_http/test_static.serveme', 200, /static file serving/)

      // no such file, so return the default app HTML.
      const getsAppHtml = [
        // This file doesn't exist.
        '/nosuchfile',

        // Our static file serving doesn't process .. or its encoded version, so
        // any of these return the app HTML.
        '/../nosuchfile',
        '/%2e%2e/nosuchfile',
        '/%2E%2E/nosuchfile',
        '/%2d%2d/nosuchfile',
        '/packages/http/../http/test_static.serveme',
        '/packages/http/%2e%2e/http/test_static.serveme',
        '/packages/http/%2E%2E/http/test_static.serveme',
        '/packages/http/../../packages/http/test_static.serveme',
        '/packages/http/%2e%2e/%2e%2e/packages/http/test_static.serveme',
        '/packages/http/%2E%2E/%2E%2E/packages/http/test_static.serveme',

        // ... and they *definitely* shouldn't be able to escape the app bundle.
        '/packages/http/../../../../../../packages/http/test_static.serveme',
        '/../../../../../../../../../../../bin/ls',
        '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/bin/ls',
        '/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/bin/ls'
      ];

      _.each(getsAppHtml, function (x) {
        do_test(x, 200, /__meteor_runtime_config__ = JSON/);
      });
    }
  ]);
}

// TODO TEST/ADD:
// - full fetch api? fetch on the client?
// - https
// - cookies?
// - human-readable error reason/cause?
// - data parse error