UNPKG

@noggin/elastic-noggin-sdk

Version:
830 lines (710 loc) 27.6 kB
import { IEnSrvOptions } from "./IEnSrvOptions"; import { updateRequestOptions, send } from "./send"; import { EnoFactory } from "./EnoFactory"; import { switchMap, tap } from "rxjs/operators"; import nock from "nock"; import { firstValueFrom } from "rxjs"; import { OptionsOfTextResponseBody } from "got/dist/source"; import { ResponseHeaders } from "./models/types"; import { IQueryOption } from "./query"; import { AbortController } from "node-abort-controller"; describe("send", () => { let nockedSend: nock.Interceptor; beforeEach(() => { nock.cleanAll(); nockedSend = nock("http://example.com") .post("/ensrv/") .query({ ns: "myNameSpace" }); }); it("should cancel the send on abort signal", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(200, [eno]); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, abortController: new AbortController(), }; send([], testOptions).subscribe({ next: () => { fail(); done(); }, error: (err) => { expect(err?.message).toBe('Request aborted'); done(); }, }); testOptions.abortController?.abort(); }); it("should not attempt the send if already cancelled", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(200, [eno]); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, abortController: new AbortController(), }; testOptions.abortController?.abort(); send([], testOptions).subscribe({ next: () => { fail(); done(); }, error: (err) => { expect(err?.message).toBe('Request aborted'); done(); }, }); }); it("should cancel the send on unsubscribe", () => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.times(1).reply(200, [eno]); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, }; const obs = send([], testOptions); const sub1 = obs.subscribe({ next: () => fail(), error: () => fail(), }); const sub2 = obs.subscribe({ next: () => fail(), error: () => fail(), }); sub1.unsubscribe(); sub2.unsubscribe(); }); it("should return enos", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(200, [eno]); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, }; send([], testOptions).subscribe({ next: (batch) => { expect(batch.length).toBe(1); expect(batch[0].tip).toBe(eno.tip); done(); }, error: () => { fail(); done(); } }); }); it("should return error", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(400, "Some return content"); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, }; send([], testOptions).subscribe({ next: (batch) => { expect(batch.length).toBe(1); expect(batch[0].tip).toBe(eno.tip); fail(); }, error: (err) => { expect(err.message).toBe("Some return content"); expect(err.code).toBe(400); done(); }, }); }); it("should set session token", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(200, [eno], { "session-token": ["mynewtoken"] }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", sessionToken: "myoldtoken", useCurrentSession: false, }; send([], testOptions).subscribe({ next: (_) => { expect(testOptions.sessionToken).toBe("mynewtoken"); done(); }, }); }); it("should set session id", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(function () { expect(this.req.getHeader("Session-Id")).toBe("sample"); return [200, [eno], { "Session-Id": [this.req.getHeader("Session-Id")] }]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", sessionToken: "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LmV5SnpaWE56YVc5dVZHOXJaVzRpT2lKMFpYTjBMM05sYzNOcGIyNVViMnRsYmlJc0luTmxjM05wYjI1SlpDSTZJbk5oYlhCc1pTSXNJbTVoYldWemNHRmpaU0k2SW5SbGMzUXZibUZ0WlhOd1lXTmxJaXdpWTNWemRHOXRVR0Y1Ykc5aFpDSTZleUoxYzJWeVZHbHdJam9pZEdWemRDOTFjMlZ5THpFaUxDSndjbTltYVd4bFZHbHdJam9pWVhCd0wzQnliMlpwYkdVdllXUnRhVzVwYzNSeVlYUnZjaUo5ZlEuUFQ1TnFSbHZvREZLcnlYYUVXakd6ZFJDbUNid01ONmdMOTJ6Y2MyQURJOA==", }; send([], testOptions).subscribe({ next: () => done(), error: () => fail() }); }); it("should should use shared anonymous session", (done) => { nock("http://first") .post("/ensrv/") .query({ ns: "myNameSpace" }) .reply(function (url) { // Expect no session token expect(this.req.getHeader("Session-Token")).toBeUndefined(); return [200, [], { "Session-Token": "myFirstToken" }]; }); nock("http://second") .post("/ensrv/") .query({ ns: "myNameSpace" }) .reply(function (url) { // Expect reuse of anonymous token because same namespace expect(this.req.getHeader("Session-Token")).toEqual("myFirstToken"); return [ 200, [], { "Session-Token": this.req.getHeader("Session-Token") }, ]; }); nock("http://third") .post("/ensrv/") .query({ ns: "anotherNameSpace" }) .reply(function (url) { // Expect no session token because different namespace expect(this.req.getHeader("Session-Token")).toBeUndefined(); return [200, [], { "Session-Token": "mySecondToken" }]; }); const testOptions: { [key: string]: IEnSrvOptions } = { first: { enSrvUrl: "http://first/ensrv/", namespace: "myNameSpace", useSharedAnonymousSession: true, useCurrentSession: false, }, second: { enSrvUrl: "http://second/ensrv/", namespace: "myNameSpace", useSharedAnonymousSession: true, useCurrentSession: false, }, third: { enSrvUrl: "http://third/ensrv/", namespace: "anotherNameSpace", useSharedAnonymousSession: true, useCurrentSession: false, }, }; send([], testOptions.first) .pipe( tap((_) => expect(testOptions.first.sessionToken).toBeUndefined()), switchMap((_) => send([], testOptions.second)), tap((_) => expect(testOptions.second.sessionToken).toBeUndefined()), switchMap((_) => send([], testOptions.third)), tap((_) => expect(testOptions.third.sessionToken).toBeUndefined()) ) .subscribe({ next: () => done(), error: () => fail() }); }); it("should send client ip and via headers", async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); let calledHeaders: any; nockedSend.reply(function () { calledHeaders = this.req.headers; return [200, [eno], {}]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, clientIp: "1.2.3.4", clientVia: "my-via", }; await firstValueFrom(send([], testOptions)); expect(calledHeaders).toEqual( jasmine.objectContaining({ "encloud-clientip": ["1.2.3.4"], "encloud-via": ["my-via"], }) ); }); it("should use keep alives", async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); let req: any; nockedSend.reply(function () { req = this.req; return [200, [eno], {}]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, }; await firstValueFrom(send([], testOptions)); expect(req.options.agent.keepAlive).toBe(true); }); it('should reuse agents', async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, }; let req1: any; nockedSend.reply(function () { req1 = this.req; return [200, [eno], {}]; }); await firstValueFrom(send([], testOptions)); let req2: any; nockedSend.reply(function () { req2 = this.req; return [200, [eno], {}]; }); await firstValueFrom(send([], testOptions)); expect(req1.options.agent).toBe(req2.options.agent); }); it('should retry twice', async () => { const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, bulk: true, }; let numCalled = 0; // console.log(new Date().valueOf()); nockedSend.times(3).reply(() => { // console.log(new Date().valueOf()); numCalled++; return [502]; }); try { await firstValueFrom(send([], testOptions)); } catch (err) { expect(err.code).toBe(502); } expect(numCalled).toBe(3); }); it("should send the bulk header", async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); let calledHeaders: any; nockedSend.reply(function () { calledHeaders = this.req.headers; return [200, [eno], {}]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, bulk: true, }; await firstValueFrom(send([], testOptions)); expect(calledHeaders).toEqual( jasmine.objectContaining({ "encloud-bulk": ["true"], }) ); }); it("should send additional headers", async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); let calledHeaders: any; nockedSend.reply(function () { calledHeaders = this.req.headers; return [200, [eno], {}]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, additionalHeaders: { key1: ["value1"], key2: ["value2a", "value2b"], key3: [], }, }; await firstValueFrom(send([], testOptions)); expect(calledHeaders).toEqual( jasmine.objectContaining({ key1: ["value1"], key2: ["value2a", "value2b"], key3: [], }) ); }); it("should send additional query string parameters", async () => { const enoFactory = new EnoFactory("mytype", "security/policy/local"); const eno = enoFactory.makeEno(); let calledUrl: string = ''; nock("http://example.com") .post("/ensrv/") .query({ ns: "myNameSpace", key1: "value1", "ke&y2": "val&ue2", key3: "", }) .reply(function (url) { calledUrl = url; return [200, [eno], {}]; }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, additionalQueryString: { key1: "value1", "ke&y2": "val&ue2", key3: "", }, }; await firstValueFrom(send([], testOptions)); expect(calledUrl).toBe( "/ensrv/?ns=myNameSpace&key1=value1&ke%26y2=val%26ue2&key3=" ); }); it("should update headers values", (done) => { const enoFactory = new EnoFactory("mytype"); enoFactory.setSecurity("security/policy/local"); const eno = enoFactory.makeEno(); nockedSend.reply(200, [eno], { 'en_query_nextpage': "searchAfterToken", 'header2': "header2Value", 'session-token': ["mynewtoken"], }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", sessionToken: "myoldtoken", useCurrentSession: false, }; const queryOptions: IQueryOption = { extraAttributes: [{ label: "runtimeAttr1", formula: "TIP()" }], extraFilters: [{ label: "runtimeFilter1", formula: "TIP()" }], vars: { varKey1: ["varVal1a", "varVal1b"], varKey2: ["varVal2a", "varVal2b"], }, dimensionOptions: [ { label: "runtimeDim1", formula: "TIP()", sortby: ["TITLE()"], sortdir: ["asc"], limit: 1, }, ], responseHeadersToInclude: ['en_query_nextpage', 'header2', 'header3'] }; send([], testOptions, queryOptions).subscribe({ next: (_) => { expect(queryOptions.responseHeadersToInclude).toEqual([ { en_query_nextpage: "searchAfterToken" }, { header2: "header2Value" }, { header3: null }, ] as ResponseHeaders); done(); }, }); }); describe('#updateRequestOptions', () => { it('should use the query service', () => { const enoFactory = new EnoFactory("op/query", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useQueryService: true }; let requestOptions: OptionsOfTextResponseBody = { json: [eno] }; updateRequestOptions([eno], testOptions, requestOptions); expect(requestOptions.url).toBe('http://example.com/query/ensrv'); }); it('should not use the query service', () => { const enoFactory = new EnoFactory("op/query", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace" }; let requestOptions: OptionsOfTextResponseBody = { json: [eno] }; updateRequestOptions([eno], testOptions, requestOptions); expect(requestOptions.url).toBe('http://example.com/ensrv/op/query?ns=myNameSpace'); }); it('should use a thin dispatcher', () => { const enoFactory = new EnoFactory("op/pull", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace" }; let requestOptions: OptionsOfTextResponseBody = { json: [eno] }; updateRequestOptions([eno], testOptions, requestOptions); expect(requestOptions.url).toBe('http://example.com/ensrv/op/pull?ns=myNameSpace'); }); it('should use the fat dispatcher', () => { const enoFactory = new EnoFactory("op/process", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace" }; let requestOptions: OptionsOfTextResponseBody = { json: [eno] }; updateRequestOptions([eno], testOptions, requestOptions); expect(requestOptions.url).toBe('http://example.com/ensrv/?ns=myNameSpace'); }); it('should use the fat dispatcher because there is multiple enos in the batch', () => { const enoFactory = new EnoFactory("op/query", "security/policy/local"); const eno = enoFactory.makeEno(); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace" }; let requestOptions: OptionsOfTextResponseBody = { json: [eno, eno] }; updateRequestOptions([eno, eno], testOptions, requestOptions); expect(requestOptions.url).toBe('http://example.com/ensrv/?ns=myNameSpace'); }); }); describe("initial session token management", () => { let enoFactory: EnoFactory; let eno: any; beforeEach(() => { enoFactory = new EnoFactory("mytype", "security/policy/local"); eno = enoFactory.makeEno(); }); describe("maintainInitialSessionToken flag", () => { it("should handle all session token scenarios", async () => { const testCases = [ { description: "maintainInitialSessionToken is undefined (explicit)", inputOptions: { sessionToken: "existing-token", maintainInitialSessionToken: undefined, }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: undefined, expectedSessionToken: "response-token", }, { description: "maintainInitialSessionToken is undefined (implicit)", inputOptions: { sessionToken: "existing-token", }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: undefined, expectedSessionToken: "response-token", }, { description: "maintainInitialSessionToken is false", inputOptions: { sessionToken: "existing-token", maintainInitialSessionToken: false, }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: undefined, expectedSessionToken: "response-token", }, { description: "maintainInitialSessionToken is true with explicit sessionToken", inputOptions: { sessionToken: "existing-token", maintainInitialSessionToken: true, }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: "existing-token", expectedSessionToken: "response-token", }, { description: "populate from response header when no sessionToken", inputOptions: { maintainInitialSessionToken: true, }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: "response-token", expectedSessionToken: "response-token", }, { description: "preserve existing initialSessionToken (explicit sessionToken)", inputOptions: { sessionToken: "new-token", maintainInitialSessionToken: true, initialSessionToken: "preserved-token", }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: "preserved-token", expectedSessionToken: "response-token", }, { description: "preserve existing initialSessionToken (response only)", inputOptions: { maintainInitialSessionToken: true, initialSessionToken: "preserved-token", }, responseHeaders: { "session-token": ["response-token"] }, expectedInitialSessionToken: "preserved-token", expectedSessionToken: "response-token", }, { description: "populate from response header (string format)", inputOptions: { maintainInitialSessionToken: true, }, responseHeaders: { "session-token": "string-response-token" }, expectedInitialSessionToken: "string-response-token", expectedSessionToken: "string-response-token", }, { description: "populate from response header (array format)", inputOptions: { maintainInitialSessionToken: true, }, responseHeaders: { "session-token": ["token1"] }, expectedInitialSessionToken: "token1", expectedSessionToken: "token1", }, // Edge Cases { description: "handle undefined sessionToken", inputOptions: { sessionToken: undefined, maintainInitialSessionToken: true, }, responseHeaders: {}, expectedInitialSessionToken: undefined, expectedSessionToken: undefined, }, { description: "handle empty array response headers", inputOptions: { maintainInitialSessionToken: true, }, responseHeaders: { "session-token": [] }, expectedInitialSessionToken: undefined, expectedSessionToken: undefined, }, ]; for (const testCase of testCases) { // Setup fresh nock for each test case nock.cleanAll(); const testNock = nock("http://example.com") .post("/ensrv/") .query({ ns: "myNameSpace" }) .reply(200, [eno], testCase.responseHeaders); // Create test options by merging base options with test case input const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, ...testCase.inputOptions, }; await firstValueFrom(send([], testOptions)); // Assert both initialSessionToken and sessionToken values expect(testOptions.initialSessionToken) .withContext(`${testCase.description} - initialSessionToken`) .toBe(testCase.expectedInitialSessionToken); expect(testOptions.sessionToken) .withContext(`${testCase.description} - sessionToken`) .toBe(testCase.expectedSessionToken); } }); it("should handle shared anonymous session scenarios", async () => { const sessionTokenCache = require("./sessionTokenCache"); // Test case: populate from cached token with shared anonymous session spyOn(sessionTokenCache, "hasToken").and.returnValue(true); spyOn(sessionTokenCache, "getToken").and.returnValue("cached-token"); spyOn(sessionTokenCache, "setToken"); nock.cleanAll(); nock("http://example.com") .post("/ensrv/") .query({ ns: "myNameSpace" }) .reply(200, [eno], { "session-token": ["response-token"] }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, maintainInitialSessionToken: true, useSharedAnonymousSession: true, }; await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBe("cached-token"); expect(testOptions.sessionToken).toBeUndefined(); // sessionToken stays undefined with useSharedAnonymousSession expect(sessionTokenCache.setToken).toHaveBeenCalledWith("myNameSpace", "response-token"); }); it("should ignore cache when maintainInitialSessionToken is false", async () => { const sessionTokenCache = require("./sessionTokenCache"); spyOn(sessionTokenCache, "hasToken").and.returnValue(true); spyOn(sessionTokenCache, "getToken").and.returnValue("cached-token"); spyOn(sessionTokenCache, "setToken"); nock.cleanAll(); nock("http://example.com") .post("/ensrv/") .query({ ns: "myNameSpace" }) .reply(200, [eno], { "session-token": ["response-token"] }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, maintainInitialSessionToken: false, useSharedAnonymousSession: true, }; await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBeUndefined(); expect(testOptions.sessionToken).toBeUndefined(); // sessionToken stays undefined with useSharedAnonymousSession expect(sessionTokenCache.setToken).toHaveBeenCalledWith("myNameSpace", "response-token"); }); }); describe("Specialized Scenarios", () => { let sessionTokenCache: any; beforeEach(() => { sessionTokenCache = require("./sessionTokenCache"); spyOn(sessionTokenCache, "hasToken").and.returnValue(false); spyOn(sessionTokenCache, "getToken"); spyOn(sessionTokenCache, "setToken"); }); it("should preserve initialSessionToken across multiple requests", async () => { // First request nockedSend.reply(200, [eno], { "session-token": ["first-response-token"] }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", sessionToken: "initial-token", useCurrentSession: false, maintainInitialSessionToken: true, }; await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBe("initial-token"); // Second request with different response token nockedSend.reply(200, [eno], { "session-token": ["second-response-token"] }); await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBe("initial-token"); expect(sessionTokenCache.getToken).not.toHaveBeenCalled(); expect(sessionTokenCache.setToken).not.toHaveBeenCalled(); }); it("should preserve initialSessionToken when cache changes", async () => { sessionTokenCache.hasToken.and.returnValue(true); sessionTokenCache.getToken.and.returnValue("original-cached-token"); nockedSend.reply(200, [eno], { "session-token": ["response-token"] }); const testOptions: IEnSrvOptions = { enSrvUrl: "http://example.com/ensrv/", namespace: "myNameSpace", useCurrentSession: false, maintainInitialSessionToken: true, useSharedAnonymousSession: true, }; // First request await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBe("original-cached-token"); // Simulate cache change sessionTokenCache.getToken.and.returnValue("new-cached-token"); // Second request nockedSend.reply(200, [eno], { "session-token": ["another-response-token"] }); await firstValueFrom(send([], testOptions)); expect(testOptions.initialSessionToken).toBe("original-cached-token"); }); }); }); });