@noggin/elastic-noggin-sdk
Version:
Elastic Noggin SDK
657 lines (598 loc) • 21.4 kB
text/typescript
import { startProcess, getProcessStatus } from "./process";
import { Batch } from "./models/types";
import { EnoFactory } from "./EnoFactory";
import { IEnSrvOptions } from "./IEnSrvOptions";
import * as Send from "./send";
import { delay, map, of, switchMap, throwError, TimeoutError } from "rxjs";
const testEnSrvOptions: IEnSrvOptions = {
enSrvUrl: "http://example.com",
namespace: "myNameSpace",
};
describe("process", () => {
it("Should start a process", (done) => {
let callCount = 0;
spyOn(Send, "send").and.callFake((batch: Batch) => {
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
}
expect(batch.length).toBe(1);
expect(batch[0].getType()).toBe("op/process");
expect(batch[0].getFieldStringValue("op/process/process")).toBe(
"test-process-tip"
);
expect(batch[0].getFieldJsonValue("op/process/inline-vars")).toEqual({
myinputkey: ["myinputval"],
});
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/inline-vars", [
JSON.stringify({ myoutputkey: ["myoutputval"] }),
]);
enoFactory.setField("response/process/finished", ["true"]);
const responseEno = enoFactory.makeEno();
return of([responseEno]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
inputVars: { myinputkey: ["myinputval"] },
}).subscribe({
next: (response) => {
expect(response.isFinished).toBeTruthy();
expect(response.outputVars).toEqual({ myoutputkey: ["myoutputval"] });
done();
},
error: (err) => {
throw err;
},
});
});
it("Should not start a non-existent process", (done) => {
const enoFactory = new EnoFactory("error");
enoFactory.setSecurity("security/policy/local");
enoFactory.setField("error/message/tip", ["error/message/eno/not-found"]);
const eno = enoFactory.makeEno();
let callCount = 0;
spyOn(Send, "send").and.callFake((batch: Batch) => {
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
}
return of([eno]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
inputVars: { myinputkey: ["myinputval"] },
}).subscribe({
next: (_) => {
throw "Should not have executed process";
},
error: (err) => {
expect(err.message).toContain("error/message/eno/not-found");
done();
},
});
});
it("Should get status of a process", (done) => {
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", ["test-process-op-tip"]);
enoFactory.setField("response/process/inline-vars", [
JSON.stringify({ myoutputkey: ["myoutputval"] }),
]);
enoFactory.setField("response/process/finished", ["true"]);
const responseEno = enoFactory.makeEno();
const sendSpy = spyOn(Send, "send").and.returnValue(of([responseEno]));
getProcessStatus("test-process-op-tip", testEnSrvOptions).subscribe({
next: (processResponse) => {
expect(sendSpy).toHaveBeenCalledWith(
jasmine.arrayContaining([
jasmine.objectContaining({
source: jasmine.objectContaining({
type: "op/process/status",
security: "security/policy/op",
field: [
{
tip: "op/process/status:op-tip",
value: ["test-process-op-tip"],
},
],
}),
}),
]),
testEnSrvOptions
);
expect(processResponse).toEqual({
operationTip: "test-process-op-tip",
responseTip: responseEno.tip,
outputVars: { myoutputkey: ["myoutputval"] },
isFinished: true,
});
done();
},
error: (err) => {
console.error(err);
fail();
done();
},
});
});
it("Should fail to get status of a process", (done) => {
spyOn(Send, "send").and.returnValue(of([]));
getProcessStatus("test-process-op-tip", testEnSrvOptions).subscribe({
next: () => {
fail();
done();
},
error: (err) => {
expect(err.message).toBe("error/message/server/internal");
done();
},
});
});
it("Should retry when not finished", (done) => {
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
} else if (callCount === 1) {
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 2) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 3) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["true"]);
}
callCount++;
return of([enoFactory.makeEno()]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
retryDelayMs: 1,
retryAttempts: 5,
}).subscribe({
next: (response) => {
expect(sendSpy).toHaveBeenCalledTimes(4);
expect(response.isFinished).toBeTrue();
done();
},
error: (err) => {
console.error(err);
fail();
done();
},
});
});
it("Should exhaust attempts when not finished", (done) => {
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
} else if (callCount === 1) {
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 2) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 3) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["true"]);
}
callCount++;
return of([enoFactory.makeEno()]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
retryDelayMs: 1,
retryAttempts: 1,
}).subscribe({
next: () => {
fail();
done();
},
error: (err) => {
expect(sendSpy).toHaveBeenCalledTimes(3);
expect(err.message).toBe(
"Too many attempts waiting for process to finish"
);
done();
},
});
});
it("Should not retry if not waiting", (done) => {
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
} else if (callCount === 1) {
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 2) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 3) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["true"]);
}
callCount++;
return of([enoFactory.makeEno()]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: false,
retryDelayMs: 1,
retryAttempts: 5,
}).subscribe({
next: (response) => {
expect(sendSpy).toHaveBeenCalledTimes(2);
expect(response.isFinished).toBeFalse();
done();
},
error: (err) => {
console.error(err);
fail();
done();
},
});
});
it("Should fetch the status if the process started but failed to respond", (done) => {
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
} else if (callCount === 1) {
callCount++;
return throwError(() => new Error("Deliberate error"));
} else if (callCount === 2) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["false"]);
} else if (callCount === 3) {
enoFactory.setField(
"response/process/op-tip",
batch[0].getFieldValues("op/process/status:op-tip")
);
enoFactory.setField("response/process/finished", ["true"]);
}
callCount++;
return of([enoFactory.makeEno()]);
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
retryDelayMs: 1,
retryAttempts: 5,
}).subscribe({
next: (response) => {
expect(sendSpy).toHaveBeenCalledTimes(4);
expect(response.isFinished).toBeTrue();
done();
},
error: (err) => {
console.error(err);
fail();
done();
},
});
});
it("Should fetch the status if the process did not start and failed to respond", (done) => {
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
}
return throwError(() => new Error("Deliberate error"));
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: true,
retryDelayMs: 1,
retryAttempts: 5,
}).subscribe({
next: (response) => {
fail();
done();
},
error: (err) => {
expect(sendSpy).toHaveBeenCalledTimes(3);
done();
},
});
});
it("Should fetch the status if the op/process started but timed out", (done) => {
let callCount = 0;
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
const sendSpy = spyOn(Send, "send").and.callFake((batch) => {
if (callCount === 0) {
// Empty batch for session initialization
expect(batch.length).toBe(0);
callCount++;
return of([]);
} else if (callCount === 1 && batch[0].getType() === 'op/process') {
enoFactory.setField('response/process/op-tip', [batch[0].tip]);
callCount++;
return of(null).pipe(
delay(3000),
map(() => [enoFactory.makeEno()])
);
} else if (batch[0].getType() === 'op/process/status') {
return of([enoFactory.makeEno()]);
}
return throwError(() => new Error('Unexpected operation'));
});
startProcess("test-process-tip", testEnSrvOptions, {
waitForFinish: false,
retryDelayMs: 1,
retryAttempts: 1,
timeoutMs: 2000,
}).subscribe({
next: (response) => {
expect(sendSpy).toHaveBeenCalledTimes(3);
done();
},
error: (err) => {
fail();
done();
},
});
});
// Table-driven test for session initialization failures
[
{
description: "timeout error",
errorFactory: () => throwError(() => new TimeoutError()),
},
{
description: "deliberate exception",
errorFactory: () => throwError(() => new Error("Deliberate session initialization error")),
},
{
description: "503 status code with internal server error ENO",
errorFactory: () => {
const enoFactory = new EnoFactory("error");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("error/message/tip", ["error/message/server/internal"]);
const errorEno = enoFactory.makeEno();
return of([errorEno]);
},
isEnoResponse: true,
},
].forEach(({ errorFactory, description, isEnoResponse }) => {
it(`Should proceed with process execution on session initialization failure: ${description}`, (done) => {
const opts = { enSrvUrl: "http://example.com", namespace: "myNameSpace" };
let callCount = 0;
const sendSpy = spyOn(Send, "send").and.callFake((batch: Batch, options: any) => {
if (callCount === 0) {
expect(batch.length).toBe(0);
expect(options.maintainInitialSessionToken).toBe(true);
callCount++;
return errorFactory();
}
// Second call should be the actual process execution
expect(batch.length).toBe(1);
expect(batch[0].getType()).toBe("op/process");
expect(batch[0].getFieldStringValue("op/process/process")).toBe("test-process-tip");
expect(options.maintainInitialSessionToken).toBe(true);
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["true"]);
const responseEno = enoFactory.makeEno();
return of([responseEno]);
});
startProcess("test-process-tip", opts, { waitForFinish: true }).subscribe({
next: (response) => {
// Verify that both calls were made: session init (failed) and process execution (succeeded)
expect(sendSpy).toHaveBeenCalledTimes(2);
// Verify first call was session initialization (empty batch)
expect(sendSpy.calls.argsFor(0)[0]).toEqual([]);
// Verify second call was process execution
expect(sendSpy.calls.argsFor(1)[0].length).toBe(1);
expect(sendSpy.calls.argsFor(1)[0][0].getType()).toBe("op/process");
expect(response.isFinished).toBeTruthy();
done();
},
error: (err) => {
fail(`Should not have failed: ${err.message}`);
done();
},
});
});
});
it("Should skip session initialization when sessionToken is already set", (done) => {
const opts = {
enSrvUrl: "http://example.com",
namespace: "myNameSpace",
sessionToken: "existing-session-token",
};
const sendSpy = spyOn(Send, "send").and.callFake((batch: Batch, options: any) => {
// The batch is non-empty. Empty batch is only sent for session initialization.
expect(batch.length).toBe(1);
expect(batch[0].getType()).toBe("op/process");
expect(batch[0].getFieldStringValue("op/process/process")).toBe("test-process-tip");
expect(options.maintainInitialSessionToken).toBe(true);
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["true"]);
return of([enoFactory.makeEno()]);
});
startProcess("test-process-tip", opts, { waitForFinish: true }).subscribe({
next: (response) => {
// Assert there was no session initialization
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(sendSpy.calls.argsFor(0)[0].length).toBe(1);
expect(sendSpy.calls.argsFor(0)[0][0].getType()).toBe("op/process");
expect(response.isFinished).toBeTruthy();
done();
},
error: (err) => {
fail(`Should not have failed: ${err.message}`);
done();
},
});
});
// Table-driven test for maintainInitialSessionToken reversion
[false, true, undefined].forEach((originalValue) => {
[
{
description: "successful process completion",
setupSendSpy: (sendSpy: jasmine.Spy) => {
let callCount = 0;
sendSpy.and.callFake((batch: Batch) => {
if (callCount === 0) {
// Empty batch for session initialization
callCount++;
return of([]);
}
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["true"]);
return of([enoFactory.makeEno()]);
});
},
expectError: false,
},
{
description: "process execution error",
setupSendSpy: (sendSpy: jasmine.Spy) => {
let callCount = 0;
sendSpy.and.callFake((batch: Batch) => {
if (callCount === 0) {
// Empty batch for session initialization
callCount++;
return of([]);
}
return throwError(() => new Error("Process execution failed"));
});
},
expectError: true,
},
{
description: "timeout during process execution",
setupSendSpy: (sendSpy: jasmine.Spy) => {
let callCount = 0;
sendSpy.and.callFake((batch: Batch) => {
if (callCount === 0) {
// Empty batch for session initialization
callCount++;
return of([]);
}
return of(null).pipe(
delay(3000),
map(() => [])
);
});
},
expectError: true,
},
{
description: "existing session token present",
setupSendSpy: (sendSpy: jasmine.Spy) => {
sendSpy.and.callFake((batch: Batch) => {
// No session initialization - directly process execution
const enoFactory = new EnoFactory("response/process");
enoFactory.setSecurity("security/policy/everyone");
enoFactory.setField("response/process/op-tip", [batch[0].tip]);
enoFactory.setField("response/process/finished", ["true"]);
return of([enoFactory.makeEno()]);
});
},
expectError: false,
hasExistingSessionToken: true,
},
].forEach(({ description, setupSendSpy, expectError, hasExistingSessionToken }) => {
it(`Should revert maintainInitialSessionToken on ${description} with original ${originalValue}`, (done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500000;
const opts: any = {
enSrvUrl: "http://example.com",
namespace: "myNameSpace",
};
if (originalValue !== undefined) {
opts.maintainInitialSessionToken = originalValue;
}
if (hasExistingSessionToken) {
opts.sessionToken = "existing-session-token";
}
const sendSpy = spyOn(Send, "send");
setupSendSpy(sendSpy);
const checkAndComplete = () => {
setTimeout(() => {
expect(opts.maintainInitialSessionToken).toBe(originalValue);
done();
}, 0);
};
startProcess("test-process-tip", opts, {
waitForFinish: false,
timeoutMs: 1000,
}).subscribe({
next: () => {
if (expectError) {
fail("Expected an error but got success");
}
},
error: () => {
if (!expectError) {
fail("Expected success but got an error");
}
checkAndComplete();
},
complete: () => {
if (!expectError) {
checkAndComplete();
}
},
});
});
});
});
});