@platform/http
Version:
Tools for working with HTTP.
451 lines (450 loc) • 20.5 kB
JavaScript
import { Readable } from 'stream';
import { createServer } from 'http';
import { http, Http } from '..';
import { expect, fs, time, randomPort, util } from '../test';
describe('http', () => {
describe('default instance (singleton)', () => {
it('has methods', () => {
expect(http.head).to.be.an.instanceof(Function);
expect(http.get).to.be.an.instanceof(Function);
expect(http.put).to.be.an.instanceof(Function);
expect(http.post).to.be.an.instanceof(Function);
expect(http.patch).to.be.an.instanceof(Function);
expect(http.delete).to.be.an.instanceof(Function);
});
it('has empty headers (by default)', () => {
expect(http.headers).to.eql({});
});
});
describe('create', () => {
it('creates (default headers)', () => {
const client = http.create();
expect(client.headers).to.eql({});
});
it('creates with custom headers (passed through all calls)', () => {
const client = Http.create({ headers: { MyHeader: 'abc' } });
expect(client.headers.MyHeader).to.eql('abc');
});
it('clones headers when creating child', () => {
const client1 = Http.create({ headers: { foo: 'foo' } });
const client2 = client1.create({ headers: { bar: 'bar' } });
const client3 = client2.create({ headers: { foo: 'zoo', baz: 'baz' } });
expect(client1.headers).to.eql({ foo: 'foo' });
expect(client2.headers).to.eql({ foo: 'foo', bar: 'bar' });
expect(client3.headers).to.eql({ foo: 'zoo', bar: 'bar', baz: 'baz' });
});
});
describe('headers', () => {
it('headers immutable', () => {
const client = http.create({ headers: { foo: 'hello' } });
const res1 = client.headers;
const res2 = client.headers;
expect(res1).to.eql(res2);
expect(res1).to.not.equal(res2);
});
it('merges headers (client => method)', async () => {
const client = Http.create({ headers: { foo: 'one' } });
client.before$.subscribe(e => e.respond({ status: 200 }));
const res = await client.get('http://localhost/foo', { headers: { bar: 'two' } });
const headers = res.headers;
expect(headers.foo).to.eql('one');
expect(headers.bar).to.eql('two');
});
it('overrides headers (client => method)', async () => {
const client = http.create({ headers: { foo: 'one', bar: 'abc' } });
client.before$.subscribe(e => e.respond({ status: 200 }));
const res = await client.get('http://localhost/foo', { headers: { foo: 'two', new: '🌳' } });
const headers = res.headers;
expect(headers.foo).to.eql('two');
expect(headers.bar).to.eql('abc');
expect(headers.new).to.eql('🌳');
});
});
describe('events (observable)', () => {
it('BEFORE event', async () => {
const client = http.create();
const events = [];
client.before$.subscribe(e => e.respond({ status: 200 }));
client.before$.subscribe(e => events.push(e));
await client.get('http://localhost/foo');
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('GET');
expect(events[0].url).to.eql('http://localhost/foo');
});
it('AFTER event: respond sync (object/json)', async () => {
const client = http.create();
const events = [];
client.before$.subscribe(e => {
e.respond({
status: 202,
statusText: 'foobar',
data: { msg: 'hello' },
});
});
client.after$.subscribe(e => events.push(e));
const res1 = await client.get('http://localhost/foo');
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('GET');
expect(events[0].url).to.eql('http://localhost/foo');
const res2 = events[0].response;
expect(res2.status).to.eql(202);
expect(res2.statusText).to.eql('foobar');
expect(res1.text).to.eql(res2.text);
expect(res1.json).to.eql({ msg: 'hello' });
});
it('AFTER event: respond async function (object/json)', async () => {
const client = http.create();
const events = [];
client.before$.subscribe(e => {
e.respond(async () => {
await time.wait(20);
return {
status: 202,
statusText: 'foobar',
data: { msg: 'hello' },
};
});
});
client.after$.subscribe(e => events.push(e));
const res1 = await client.get('http://localhost/foo');
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('GET');
expect(events[0].url).to.eql('http://localhost/foo');
expect(events[0].ok).to.eql(true);
expect(events[0].status).to.eql(202);
const res2 = events[0].response;
expect(res2.status).to.eql(202);
expect(res2.statusText).to.eql('foobar');
expect(res1.text).to.eql(res2.text);
expect(res1.json).to.eql({ msg: 'hello' });
});
it('AFTER event: respond sync function (file/binary)', async () => {
const client = http.create();
const events = [];
const image1 = await fs.readFile(fs.resolve('src/test/assets/kitten.jpg'));
const image2 = await fs.readFile(fs.resolve('src/test/assets/bird.png'));
client.before$.subscribe(e => {
const data = new Readable();
data.push(image2);
data.push(null);
e.respond(() => ({ status: 202, statusText: 'foobar', data }));
});
client.after$.subscribe(e => events.push(e));
const res = await client.post('http://localhost/foo', image1);
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('POST');
expect(events[0].url).to.eql('http://localhost/foo');
expect(res.body).to.not.eql(undefined);
if (res.body) {
const path = fs.resolve('tmp/response-bird.png');
await fs.stream.save(path, res.body);
expect((await fs.readFile(path)).toString()).to.eql(image2.toString());
}
});
it('AFTER event: respond (string)', async () => {
const client = http.create();
const events = [];
client.before$.subscribe(e => {
e.respond({
status: 200,
headers: { 'content-type': 'text/plain' },
data: 'hello',
});
});
client.after$.subscribe(e => events.push(e));
const res1 = await client.get('http://localhost/foo');
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('GET');
expect(events[0].url).to.eql('http://localhost/foo');
const res2 = events[0].response;
expect(res2.status).to.eql(200);
expect(res2.statusText).to.eql('OK');
expect(res1.text).to.eql(res2.text);
expect(res1.text).to.eql('hello');
expect(res1.json).to.eql('');
});
it('AFTER event: respond (<empty>)', async () => {
const client = http.create();
const events = [];
client.before$.subscribe(e => {
e.respond({
status: 200,
data: undefined,
});
});
client.after$.subscribe(e => events.push(e));
const res1 = await client.get('http://localhost/foo');
expect(events.length).to.eql(1);
expect(events[0].method).to.eql('GET');
expect(events[0].url).to.eql('http://localhost/foo');
const res2 = events[0].response;
expect(res2.status).to.eql(200);
expect(res2.statusText).to.eql('OK');
expect(res1.text).to.eql(res2.text);
expect(res1.text).to.eql('');
expect(res1.json).to.eql('');
});
it('sends event identifier ("uid") that is shared between before/after events', async () => {
const client = http.create();
client.before$.subscribe(e => e.respond({ status: 200 }));
const events = [];
client.events$.subscribe(e => events.push(e));
await client.get('http://localhost/foo');
expect(events.length).to.eql(2);
expect(events[0].payload.uid).to.eql(events[1].payload.uid);
});
it('does not share events between instances', async () => {
const client1 = http.create();
const client2 = client1.create();
client1.before$.subscribe(e => e.respond({ status: 200 }));
client2.before$.subscribe(e => e.respond({ status: 200 }));
const events1 = [];
const events2 = [];
client1.events$.subscribe(e => events1.push(e));
client2.events$.subscribe(e => events2.push(e));
await client1.get('http://localhost/foo');
await client2.get('http://localhost/foo');
await client2.get('http://localhost/foo');
expect(events1.length).to.eql(2);
expect(events2.length).to.eql(4);
});
});
describe('verbs', () => {
let events = [];
let client;
beforeEach(() => {
events = [];
client = http.create();
client.events$.subscribe(e => events.push(e));
client.before$.subscribe(e => e.respond({ status: 200 }));
});
it('head', async () => {
await client.head('http://localhost/foo');
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('HEAD');
expect(events[1].payload.method).to.eql('HEAD');
});
it('get', async () => {
await client.get('http://localhost/foo');
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('GET');
expect(events[1].payload.method).to.eql('GET');
});
it('put', async () => {
await client.put('http://localhost/foo', { foo: 123 });
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('PUT');
expect(events[1].payload.method).to.eql('PUT');
});
it('post', async () => {
await client.post('http://localhost/foo', { foo: 123 });
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('POST');
expect(events[1].payload.method).to.eql('POST');
});
it('patch', async () => {
await client.patch('http://localhost/foo', { foo: 123 });
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('PATCH');
expect(events[1].payload.method).to.eql('PATCH');
});
it('delete', async () => {
await client.delete('http://localhost/foo');
expect(events.length).to.eql(2);
expect(events[0].payload.method).to.eql('DELETE');
expect(events[1].payload.method).to.eql('DELETE');
});
});
describe('fetch', () => {
it('http server: text', async () => {
const data = `console.log('hello');`;
const port = randomPort();
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(data);
res.end();
}).listen(port);
const res = await http.create().get(`http://localhost:${port}`);
server.close();
expect(res.status).to.eql(200);
expect(res.statusText).to.eql('OK');
expect(res.headers['content-type']).to.eql('text/javascript');
expect(res.contentType.is.binary).to.eql(false);
expect(res.contentType.is.json).to.eql(false);
expect(res.contentType.is.text).to.eql(true);
expect(res.text).to.eql(data);
expect(res.json).to.eql('');
expect(util.isStream(res.body)).to.eql(true);
});
it('http server: json', async () => {
const data = { msg: 'hello' };
const port = randomPort();
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(data));
res.end();
}).listen(port);
const res = await http.create().get(`http://localhost:${port}`);
server.close();
expect(res.status).to.eql(200);
expect(res.statusText).to.eql('OK');
expect(res.headers['content-type']).to.eql('application/json');
expect(res.contentType.is.binary).to.eql(false);
expect(res.contentType.is.json).to.eql(true);
expect(res.contentType.is.text).to.eql(false);
expect(res.text).to.eql('');
expect(res.json).to.eql(data);
expect(util.isStream(res.body)).to.eql(true);
});
it('http server: json (404)', async () => {
const data = { error: 'Fail' };
const port = randomPort();
const server = createServer((req, res) => {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(data));
res.end();
}).listen(port);
const res = await http.create().get(`http://localhost:${port}`);
server.close();
expect(res.status).to.eql(404);
expect(res.statusText).to.eql('Not Found');
});
it('http server: file/binary', async () => {
const path = fs.resolve('src/test/assets/kitten.jpg');
const image = await fs.readFile(path);
const port = randomPort();
const server = createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'image/jpeg',
'Content-Length': image.length,
});
fs.createReadStream(path).pipe(res);
}).listen(port);
const res = await http.create().get(`http://localhost:${port}`);
server.close();
expect(res.status).to.eql(200);
expect(res.statusText).to.eql('OK');
expect(res.headers['content-type']).to.eql('image/jpeg');
expect(res.contentType.is.binary).to.eql(true);
expect(res.contentType.is.json).to.eql(false);
expect(res.contentType.is.text).to.eql(false);
expect(res.text).to.eql('');
expect(res.json).to.eql('');
expect(util.isStream(res.body)).to.eql(true);
if (res.body) {
const path = fs.resolve('tmp/kitten.jpg');
await fs.stream.save(path, res.body);
expect((await fs.readFile(path)).toString()).to.eql(image.toString());
}
});
it('injected [fetch] function', async () => {
const requests = [];
const data = { msg: 'hello' };
const fetch = async (req) => {
requests.push(req);
await time.wait(10);
return {
status: 202,
headers: util.toRawHeaders({ foo: 'bar', 'Content-Type': 'application/json' }),
body: null,
text: async () => JSON.stringify(data),
json: async () => data,
};
};
const client = http.create({ fetch });
const res = await client.post(`http://localhost`, { send: true }, { headers: { foo: '123' } });
expect(res.status).to.eql(202);
expect(res.statusText).to.eql('OK');
expect(res.headers.foo).to.eql('bar');
expect(res.headers['content-type']).to.eql('application/json');
expect(res.contentType.is.binary).to.eql(false);
expect(res.contentType.is.json).to.eql(true);
expect(res.contentType.is.text).to.eql(false);
expect(res.json).to.eql(data);
expect(res.json).to.eql(data);
expect(res.text).to.eql('');
expect(res.body).to.eql(undefined);
expect(requests.length).to.eql(1);
expect(requests[0].url).to.eql('http://localhost');
expect(requests[0].mode).to.eql('cors');
expect(requests[0].method).to.eql('POST');
expect(requests[0].data).to.eql({ send: true });
});
});
describe('modify (HTTP server)', () => {
const testServer = () => {
const port = randomPort();
const instance = createServer((req, res) => {
api.headers = req.headers;
res.writeHead(200).end();
}).listen(port);
const api = {
port,
instance,
headers: {},
dispose: () => instance.close(),
};
return api;
};
it('header(key, value)', async () => {
const server = testServer();
const client = http.create({ headers: { foo: 'one' } });
const events = [];
client.before$.subscribe(e => {
events.push(e);
e.modify.header('foo', 'abc');
e.modify.header('bar', 'hello');
e.modify.header('baz', 'hello');
});
client.before$.subscribe(e => {
e.modify.header('foo', 'zoo');
e.modify.header('bar', '');
});
await client.get(`http://localhost:${server.port}`);
server.dispose();
expect(events[0].isModified).to.eql(true);
const headers = server.headers;
expect(headers.foo).to.eql('zoo');
expect(headers.bar).to.eql(undefined);
expect(headers.baz).to.eql('hello');
});
it('headers.merge (multiple times, cumulative)', async () => {
const server = testServer();
const client = http.create({ headers: { foo: 'one', bar: 'abc', zoo: 'zoo' } });
const events = [];
client.before$.subscribe(e => {
events.push(e);
e.modify.headers.merge({ foo: 'two', baz: 'baz' });
e.modify.headers.merge({ zoo: '' });
e.modify.headers.merge({ foo: 'three' });
});
await client.get(`http://localhost:${server.port}/foo`);
server.dispose();
expect(events[0].isModified).to.eql(true);
const headers = server.headers;
expect(headers.zoo).to.eql(undefined);
expect(headers.foo).to.eql('three');
expect(headers.bar).to.eql('abc');
expect(headers.baz).to.eql('baz');
});
it('headers.replace', async () => {
const server = testServer();
const client = http.create({ headers: { foo: 'one' } });
const events = [];
client.before$.subscribe(e => {
events.push(e);
e.modify.headers.replace({ foo: 'two', baz: 'baz' });
});
client.before$.subscribe(e => {
e.modify.headers.replace({ bar: 'bar', zoo: '' });
});
await client.get(`http://localhost:${server.port}`);
server.dispose();
expect(events[0].isModified).to.eql(true);
const headers = server.headers;
expect(headers.foo).to.eql(undefined);
expect(headers.zoo).to.eql(undefined);
expect(headers.bar).to.eql('bar');
});
});
});