feat: everything
This commit is contained in:
270
tests/integration.test.ts
Normal file
270
tests/integration.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { executeGraphQL, X3RequestError } from "../src/graphql/client.js";
|
||||
import { getConfig } from "../src/config.js";
|
||||
import type { X3Config } from "../src/types/index.js";
|
||||
|
||||
function sandboxConfig(): X3Config {
|
||||
return {
|
||||
url: "https://api-devna.dev-sagex3.com/demo/service/X3CLOUDV2_SEED/api",
|
||||
endpoint: undefined,
|
||||
clientId: undefined,
|
||||
secret: undefined,
|
||||
user: undefined,
|
||||
tokenLifetime: 600,
|
||||
mode: "sandbox",
|
||||
tlsRejectUnauthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("X3RequestError", () => {
|
||||
it("stores statusCode and message", () => {
|
||||
const err = new X3RequestError("test error", 401);
|
||||
expect(err.message).toBe("test error");
|
||||
expect(err.statusCode).toBe(401);
|
||||
expect(err.name).toBe("X3RequestError");
|
||||
expect(err.details).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stores optional details", () => {
|
||||
const details = { errors: [{ message: "bad query" }] };
|
||||
const err = new X3RequestError("GraphQL Error", 400, details);
|
||||
expect(err.statusCode).toBe(400);
|
||||
expect(err.details).toEqual(details);
|
||||
});
|
||||
|
||||
it("is an instance of Error", () => {
|
||||
const err = new X3RequestError("test", 500);
|
||||
expect(err instanceof Error).toBe(true);
|
||||
expect(err instanceof X3RequestError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executeGraphQL - mutation rejection", () => {
|
||||
it("rejects mutations with X3RequestError (statusCode 0)", async () => {
|
||||
const config = sandboxConfig();
|
||||
|
||||
try {
|
||||
await executeGraphQL(config, "mutation { deleteAll { success } }");
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect(e.statusCode).toBe(0);
|
||||
expect(e.message).toContain("Mutations are not supported");
|
||||
expect(e.message).toContain("read-only");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects mutations with leading whitespace", async () => {
|
||||
const config = sandboxConfig();
|
||||
|
||||
try {
|
||||
await executeGraphQL(config, " \n mutation { update { id } }");
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
expect((error as X3RequestError).statusCode).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects case-insensitive MUTATION keyword", async () => {
|
||||
const config = sandboxConfig();
|
||||
|
||||
try {
|
||||
await executeGraphQL(config, "MUTATION { doSomething { result } }");
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT reject queries that contain the word 'mutation' in a string", async () => {
|
||||
const config = sandboxConfig();
|
||||
const query = '{ __type(name: "mutation") { name } }';
|
||||
|
||||
try {
|
||||
await executeGraphQL(config, query);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
// If we get here, it should be a network/auth error, NOT a mutation rejection
|
||||
expect((error as X3RequestError).statusCode).not.toBe(0);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("executeGraphQL - network error handling", () => {
|
||||
it("wraps connection errors in X3RequestError with statusCode 0", async () => {
|
||||
const config: X3Config = {
|
||||
...sandboxConfig(),
|
||||
url: "https://localhost:1/nonexistent",
|
||||
};
|
||||
|
||||
try {
|
||||
await executeGraphQL(config, "{ __typename }");
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect(e.statusCode).toBe(0);
|
||||
expect(e.message).toContain("Network error");
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("Sandbox endpoint - graceful degradation", () => {
|
||||
const config = sandboxConfig();
|
||||
|
||||
it("introspect_schema: handles endpoint error gracefully", async () => {
|
||||
const introspectionQuery = `{
|
||||
__schema {
|
||||
queryType { name }
|
||||
types { name kind }
|
||||
}
|
||||
}`;
|
||||
|
||||
try {
|
||||
const result = await executeGraphQL(config, introspectionQuery);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.data).toBeDefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
// Sandbox is down (401 expired password) or network error — both are acceptable
|
||||
expect([0, 401, 403, 500]).toContain(e.statusCode);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
it("query_entities: builds correct query and handles endpoint error", async () => {
|
||||
const query = `{ xtremX3MasterData { businessPartner { query(first: 5) { edges { node { code description1 } } pageInfo { endCursor hasNextPage startCursor hasPreviousPage } } } } }`;
|
||||
|
||||
try {
|
||||
const result = await executeGraphQL(config, query);
|
||||
expect(result).toBeDefined();
|
||||
if (result.data) {
|
||||
expect(result.data).toBeDefined();
|
||||
}
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect([0, 401, 403, 500]).toContain(e.statusCode);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
it("read_entity: builds correct read query with _id parameter", async () => {
|
||||
const query = `{ xtremX3MasterData { businessPartner { read(_id: "AE003") { code description1 } } } }`;
|
||||
|
||||
try {
|
||||
const result = await executeGraphQL(config, query);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect([0, 401, 403, 500]).toContain(e.statusCode);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
it("aggregate_entities: builds correct readAggregate query", async () => {
|
||||
const query = `{ xtremX3Products { product { readAggregate { productWeight { min max count } } } } }`;
|
||||
|
||||
try {
|
||||
const result = await executeGraphQL(config, query);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect([0, 401, 403, 500]).toContain(e.statusCode);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
it("aggregate_entities: builds correct query with filter", async () => {
|
||||
const query = `{ xtremX3Products { product { readAggregate(filter: "{productCategory: {code: {_eq: 'RAW'}}}") { productWeight { min max } } } } }`;
|
||||
|
||||
try {
|
||||
const result = await executeGraphQL(config, query);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(X3RequestError);
|
||||
const e = error as X3RequestError;
|
||||
expect([0, 401, 403, 500]).toContain(e.statusCode);
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
describe("Query shape verification", () => {
|
||||
it("query_entities query follows expected structure", () => {
|
||||
const rootType = "xtremX3MasterData";
|
||||
const entity = "businessPartner";
|
||||
const fields = "code description1";
|
||||
const args = 'first: 10, filter: "{code: {_eq: \'ABC\'}}"';
|
||||
|
||||
const query = `{ ${rootType} { ${entity} { query(${args}) { edges { node { ${fields} } } pageInfo { endCursor hasNextPage startCursor hasPreviousPage } } } } }`;
|
||||
|
||||
expect(query).toContain("xtremX3MasterData");
|
||||
expect(query).toContain("businessPartner");
|
||||
expect(query).toContain("query(");
|
||||
expect(query).toContain("first: 10");
|
||||
expect(query).toContain("edges { node {");
|
||||
expect(query).toContain("pageInfo {");
|
||||
});
|
||||
|
||||
it("read_entity query includes _id parameter", () => {
|
||||
const rootType = "xtremX3MasterData";
|
||||
const entity = "businessPartner";
|
||||
const id = "AE003";
|
||||
const fields = "code description1 country { code }";
|
||||
|
||||
const query = `{ ${rootType} { ${entity} { read(_id: "${id}") { ${fields} } } } }`;
|
||||
|
||||
expect(query).toContain(`read(_id: "${id}")`);
|
||||
expect(query).toContain("country { code }");
|
||||
});
|
||||
|
||||
it("aggregate_entities query uses readAggregate", () => {
|
||||
const rootType = "xtremX3Products";
|
||||
const entity = "product";
|
||||
const aggregateSelection = "productWeight { min max count }";
|
||||
|
||||
const query = `{ ${rootType} { ${entity} { readAggregate { ${aggregateSelection} } } } }`;
|
||||
|
||||
expect(query).toContain("readAggregate");
|
||||
expect(query).toContain("productWeight { min max count }");
|
||||
});
|
||||
|
||||
it("aggregate_entities query includes filter when provided", () => {
|
||||
const filter = "{category: {_eq: 'RAW'}}";
|
||||
const query = `{ xtremX3Products { product { readAggregate(filter: "${filter}") { productWeight { min max } } } } }`;
|
||||
|
||||
expect(query).toContain(`filter: "${filter}"`);
|
||||
expect(query).toContain("readAggregate(");
|
||||
});
|
||||
|
||||
it("nested fields expand correctly in query shape", () => {
|
||||
const fields = "code productCategory { code name } header { name type }";
|
||||
const query = `{ xtremX3Products { product { query(first: 5) { edges { node { ${fields} } } } } } }`;
|
||||
|
||||
expect(query).toContain("productCategory { code name }");
|
||||
expect(query).toContain("header { name type }");
|
||||
expect(query).toContain("code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConfig integration", () => {
|
||||
it("default config points to sandbox URL", () => {
|
||||
const savedUrl = process.env.SAGE_X3_URL;
|
||||
const savedMode = process.env.SAGE_X3_MODE;
|
||||
delete process.env.SAGE_X3_URL;
|
||||
delete process.env.SAGE_X3_MODE;
|
||||
delete process.env.SAGE_X3_CLIENT_ID;
|
||||
delete process.env.SAGE_X3_SECRET;
|
||||
delete process.env.SAGE_X3_USER;
|
||||
|
||||
try {
|
||||
const config = getConfig();
|
||||
expect(config.url).toBe("https://api-devna.dev-sagex3.com/demo/service/X3CLOUDV2_SEED/api");
|
||||
expect(config.mode).toBe("sandbox");
|
||||
} finally {
|
||||
if (savedUrl) process.env.SAGE_X3_URL = savedUrl;
|
||||
if (savedMode) process.env.SAGE_X3_MODE = savedMode;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user