odata
Version:
o.js is a isomorphic Odata Javascript library to simplify the request of data. The main goal is to build a standalone, lightweight and easy to understand Odata lib.
653 lines (560 loc) • 17.4 kB
text/typescript
import { o, OBatch } from "./o";
import { OdataConfig } from "./OdataConfig";
import buildQuery from "odata-query";
describe("initialize a new oHandler", () => {
test("url with string", () => {
// given
const url = "http://odata.org/";
// when
const when = o(url);
// expect
expect((when.config.rootUrl as URL).href).toEqual(url);
});
test("url with string and root config", () => {
// given
const url = "http://odata.org/";
const config = { rootUrl: "http://a/" };
// when
const when = o(url, config);
// expect
expect((when.config.rootUrl as URL).href).toEqual(url);
});
test("only with rootUrl config", () => {
// given
const url = "";
const config = { rootUrl: "http://a/" };
// when
const when = o(url, config);
// expect
expect((when.config.rootUrl as URL).href).toEqual(config.rootUrl);
});
test("only with rootUrl and a given url", () => {
// given
const url = "foo";
const config = { rootUrl: "http://bar/" };
// when
const when = o(url, config);
// expect
expect((when.config.rootUrl as URL).href).toEqual(`${config.rootUrl}foo`);
});
test("no rootUrl given and url given not a valid url (should switch to window.location.href)", () => {
// given
const url = "foo";
// when
const when = o(url);
// expect
expect((when.config.rootUrl as URL).href).toEqual(`http://localhost/foo`);
});
test("config should be default if not given", () => {
// given
const url = "foo";
// when
const when = o(url);
// expect
expect(when.config).toBeDefined();
expect(when.config).toMatchSnapshot();
});
test("config should be extended if something given.", () => {
// given
const url = "foo";
const config: Partial<OdataConfig> = {
mode: "no-cors",
rootUrl: "http://bar.de/foo",
};
// when
const when = o(url, config);
// expect
expect(when.config).toBeDefined();
expect(when.config.mode).toBe("no-cors");
expect(when.config.referrer).toBe("client");
expect((when.config.rootUrl as URL).href).toBe("http://bar.de/foo/foo");
});
});
describe("Instant request", () => {
test("Request any thing that is put in the init request", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=2"
)
.get()
.query();
// expect
expect(data.length).toBe(2);
});
test("Still allow chaining and multiple requests", async () => {
// given
const [resource1, resource2] = ["People", "Airlines"];
// when
const data = await o("https://services.odata.org/V4/TripPinServiceRW/")
.get(resource1)
.get(resource2)
.query({ $top: 2 });
// expect
expect(data.length).toBe(2);
expect(data[0].length).toBe(2);
expect(data[1].length).toBe(2);
});
test("Attach the correct queries to the request", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=2",
{
query: { $top: 1, $filter: `FirstName eq 'john'` },
}
)
.get()
.fetch();
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"People?$top=1&$filter=FirstName eq 'john'"
);
});
test("Attach the correct queries to the request if a string is used", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=2"
)
.get()
.fetch("?$top=1&$filter=FirstName eq 'john'");
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"People?$top=1&$filter=FirstName eq 'john'"
);
});
test("Attach the correct queries to the request if a odata-query is used", async () => {
// given
const filter = {
not: {
and: [{ FirstName: 'John' }, { LastName: 'Foo' }],
},
};
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=2"
)
.get()
.fetch(buildQuery({ filter }));
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"$filter=not(((FirstName eq 'John') and (LastName eq 'Foo')))&$top=2"
);
});
test("Use buildQuery in get", async () => {
// given
const key = '1'
const filter = {
not: {
and: [{ FirstName: 'John' }, { LastName: 'Foo' }],
},
};
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/"
)
.get('People' + buildQuery({ key, filter, top: 3 }))
.fetch();
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"/People('1')?$filter=not(((FirstName eq 'John') and (LastName eq 'Foo')))&$top=3"
);
});
test("Check right URL Params override. query-parameter in fetch()/query() wins over query-config", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People",
{
query: { $top: 1 },
}
)
.get()
.fetch({ $top: 2 });
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"People?$top=2"
);
});
test("Check right URL Params override. query-parameter in fetch()/query() wins over baseUrl", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=1"
)
.get()
.fetch({ $top: 2 });
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"People?$top=2"
);
});
test("Check right URL Params override. query-config wins over baseUrl", async () => {
// when
const data = await o(
"https://services.odata.org/V4/TripPinServiceRW/People?$top=1",
{
query: { $top: 2 },
}
)
.get()
.fetch();
// expect
expect(decodeURIComponent((data as Response).url)).toContain(
"People?$top=2"
);
});
});
describe("Request handling", () => {
let oHandler;
beforeEach(() => {
oHandler = o("https://services.odata.org/V4/TripPinServiceRW/");
});
test("Queue requests", () => {
// given
const resource = "People";
// when
oHandler.get(resource);
// expect
expect(oHandler.pending).toBe(1);
// when
oHandler.get(resource).get(resource);
// expect
expect(oHandler.pending).toBe(3);
});
test("Clean queued request after query", async () => {
// given
const resource = "People";
// when
await oHandler.get(resource).get(resource).query();
// expect
expect(oHandler.pending).toBe(0);
});
test("Clean queued request after batch", async () => {
// given
const resource = "People";
// when
try {
await oHandler.get(resource).get(resource).query();
} catch (ex) {
console.log(ex);
// intended empty
}
// expect
expect(oHandler.pending).toBe(0);
});
test("Attach the correct queries to the request", async () => {
// given
const resource = "People";
// when
const requests = oHandler
.get(resource)
.get(resource)
.fetch({ $top: 1, $filter: `FirstName eq 'john'` });
const req = await requests;
// expect
expect(decodeURIComponent(req[0].url)).toContain(
"People?$top=1&$filter=FirstName eq 'john'"
);
});
test("On request to one resource only, don't return an array", async () => {
// given
const resource = "People";
// when
const requests = oHandler.get(resource).fetch();
const req = await requests;
// expect
expect(Array.isArray(req)).toBe(false);
});
});
describe("GET request", () => {
let oHandler;
beforeAll(() => {
oHandler = o("https://services.odata.org/V4/TripPinServiceRW/");
});
test("Request to a resource should return an array", async () => {
// given
const resource = "People";
// when
const data = await oHandler.get(resource).query({
$top: 4,
});
// expect
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(4);
});
test("Request to one entity should return a object", async () => {
// given
const resource = "People('russellwhyte')";
// when
const data = await oHandler.get(resource).query();
// expect
expect(Array.isArray(data)).toBe(false);
expect(typeof data).toBe("object");
});
test("Request to $top=1 should return a array", async () => {
// given
const resource = "People";
// when
const data = await oHandler.get(resource).query({ $top: 1 });
// expect
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(1);
});
test("Request multiple resources or entities", async () => {
// given
const resource1 = "People('russellwhyte')";
const resource2 = "People";
// when
const data = await oHandler.get(resource1).get(resource2).query();
// expect
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(2);
expect(Array.isArray(data[0])).toBe(false);
expect(Array.isArray(data)).toBe(true);
});
test("Request a entity that cannot be found", async () => {
// given
const resource = "People('foo')";
// when
try {
await oHandler.get(resource).query();
} catch (res) {
// expect
expect(res.status).toBe(404);
}
});
test("If one request fails in a query sequent, throw all", async () => {
// given
const resource1 = "People('russellwhyte')";
const resource2 = "People('unknown')";
// when
try {
await oHandler.get(resource1).get(resource2).query();
} catch (res) {
// expect
expect(res.status).toBe(404);
}
});
});
describe("Error handling", () => {
let oHandler;
let onError;
beforeEach(() => {
onError = jest.fn();
oHandler = o("https://services.odata.org/V4/TripPinServiceRW/", { onError });
});
afterEach(() => {
jest.restoreAllMocks();
});
test.each([
"query",
"fetch",
"batch",
])("Callback onError is called in %s when network call fails", async (method) => {
// given
const resource = "People";
const fetchError = new TypeError("Failed to fetch");
jest
.spyOn(window, "fetch")
.mockRejectedValue(fetchError);
// when / expect
await expect(async () => await oHandler.get(resource)[method]()).rejects.toBe(fetchError);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(oHandler, fetchError);
});
// Unlike fetch and batch methods, query is the only one that analyze the response status code and throws an error
// if the status code is not 2xx.
test("Callback onError is called in query when response code is 404", async () => {
// given
const resource = "UnknownResource";
// when / expect
await expect(async () => await oHandler.get(resource).query()).rejects.toMatchObject({ status: 404 });
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(oHandler, expect.objectContaining({ status: 404 }));
});
});
describe("Create, Update and Delete request", () => {
let oHandler;
beforeAll(async () => {
oHandler = o(
"https://services.odata.org/V4/TripPinServiceRW/(S(ojstest))/",
{
headers: { "If-match": "*", "Content-Type": "application/json" },
}
);
});
test("POST a person and request it afterwards", async () => {
// given
const resource = "People";
const data = {
FirstName: "Foo",
LastName: "Bar",
UserName: "barfoo" + Math.random(),
};
// when
const response = await oHandler
.post(resource, data)
.get(`${resource}('${data.UserName}')`)
.query();
// expect
expect(Array.isArray(response)).toBe(true);
expect(Array.isArray(response[0])).toBe(false);
expect(Array.isArray(response[1])).toBe(false);
expect(response[0].FirstName).toBe(data.FirstName);
expect(response[1].LastName).toBe(data.LastName);
expect(response[1].UserName).toBe(data.UserName);
});
test("POST a person and DELETE it afterwards", async () => {
// given
const resource = "People";
const data = {
FirstName: "Bar",
LastName: "Foo",
UserName: "foobar" + Math.random(),
};
// when
const response = await oHandler
.post(resource, data)
.delete(`${resource}('${data.UserName}')`)
.query();
// expect
expect(Array.isArray(response)).toBe(true);
expect(Array.isArray(response[0])).toBe(false);
expect(Array.isArray(response[1])).toBe(false);
expect(response[0].FirstName).toBe(data.FirstName);
expect(response[0].LastName).toBe(data.LastName);
expect(response[0].UserName).toBe(data.UserName);
expect(response[1].status).toBe(204);
});
test("POST a person and PATCH it afterwards", async () => {
// given
const resource = "People";
const data = {
FirstName: "Bar",
LastName: "Foo",
UserName: "foobar" + Math.random(),
};
// when
const response = await oHandler
.post(resource, data)
.patch(`${resource}('${data.UserName}')`, { FirstName: data.LastName })
.get(`${resource}('${data.UserName}')`)
.query();
// expect
expect(Array.isArray(response)).toBe(true);
expect(Array.isArray(response[0])).toBe(false);
expect(Array.isArray(response[1])).toBe(false);
expect(response[0].FirstName).toBe(data.FirstName);
expect(response[0].LastName).toBe(data.LastName);
expect(response[1].status).toBe(204);
expect(response[2].FirstName).toBe(data.LastName);
});
test("POST a person with FormData", async () => {
// given
const resource = "People";
const data = new FormData();
data.append("FirstName", "Bar");
// when
try {
const response = await oHandler.post(resource, data).query();
} catch (ex) {
// expect: FormData is not supported, so this error code is correct
expect(ex.status).toBe(415);
}
});
});
describe("Batching", () => {
let oHandler;
beforeAll(async () => {
// Use the non restier service as it has CORS enabled
const response: Response = (await o(
"https://services.odata.org/V4/TripPinServiceRW/"
)
.get()
.fetch()) as Response;
oHandler = o(response.url, {
headers: { "If-match": "*", "Content-Type": "application/json" },
});
});
test("Batch multiple GET requests", async () => {
// given
const [resource1, resource2] = ["People", "Airlines"];
// when
const data = await oHandler.get(resource1).get(resource2).batch();
// expect
expect(data.length).toBe(2);
expect(data[0].body.length).toBeDefined();
});
test("Batch multiple GET requests and allow to add a query", async () => {
// given
const [resource1, resource2] = ["People", "Airlines"];
// when
const data = await oHandler
.get(resource1)
.get(resource2)
.batch({ $top: 2 });
// expect
expect(data[0].body.length).toBe(2);
});
test("Batch multiple GET requests and patch something", async () => {
// given
const [resource1, resource2] = ["People", "Airlines('AA')"];
// when
const data = await oHandler
.get(resource1)
.patch(resource2, { Name: "New" })
.get(resource2)
.batch();
// expect
expect(data.length).toBe(3);
expect(data[1].status).toBe(204);
expect(data[2].body.Name).toBe("New");
});
test("Batch POST and PATCH with useChangeset=true", async () => {
oHandler.config.batch.useChangset = true;
// given
const [resource1, resource2] = ["People", "Airlines('AA')"];
const resouce1data = {
FirstName: "Bar",
LastName: "Foo is cool",
UserName: "foobar" + Math.random(),
};
// when
const request = oHandler
.post(resource1, resouce1data)
.patch(resource2, { Name: "New" });
const batch = new OBatch(request.requests, request.config, null);
const data = await request.batch();
// expect
expect(data.length).toBe(2);
expect(data[0].body.LastName).toBe(resouce1data.LastName);
expect(data[1].status).toBe(204);
});
test("Clean queued request after batch", async () => {
// given
const [resource1, resource2] = ["People", "Airlines"];
// when
await oHandler.get(resource1).get(resource2).batch();
// expect
expect(oHandler.pending).toBe(0);
});
// Content ID seems to have a problem in the test implementation (or I don't get the right implementation)
// tested with postman and I always get Resource not found for the segment '$1'
xtest("add something and directly patch it with Content-Id", async () => {
// given
oHandler.config.batch.useChangeset = true;
const resource = "People";
const data = {
FirstName: "Bar",
LastName: "Foo",
UserName: "foobar" + Math.random(),
};
// when
const result = await oHandler
.post(resource, data)
.patch("$1", { LastName: "Bar" })
.get(`${resource}('${data.UserName}')`)
.batch();
// expect
expect(result.length).toBe(3);
expect(result[1].status).toBe(204);
expect(result[2].body.LastName).toBe("Bar");
});
});