#397 adds registerAuthCallback function using strategy pattern; adds tests
diff --git a/src/auth.js b/src/auth.js
new file mode 100644
index 0000000..cb8099a
--- /dev/null
+++ b/src/auth.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export let authCallback = null;
+
+/**
+ * Fetches the most up-to-date auth header string from the auth callback
+ * and updates the config object with the new value.
+ * @param {Object} config Configuration object to be updated.
+ * @param {Function} authCallback Callback used to fetch the newest header.
+ * @returns {void}
+ */
+export function updateAuthHeader(config) {
+ if (authCallback) {
+ try {
+ config.authHeader = authCallback();
+ } catch (e) {
+ // We should emit the error, but otherwise continue as this could be a temporary issue
+ // due to network connectivity or some logic inside the authCallback which is the user's
+ // responsibility.
+ console.error(`Error encountered while setting the auth header: ${e}`);
+ }
+ }
+}
+
+/**
+ * Registers the provided callback to be used when updating the auth header.
+ * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+ * @returns {boolean} Whether the operation succeeded.
+ */
+export function registerAuthCallback(callback) {
+ try {
+ verifyCallback(callback);
+ authCallback = callback;
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * Verify that the provided callback is a function which returns a string
+ * @param {Function} callback Callback used to fetch the newest header. Should return a string.
+ * @throws {Error} If the callback is not a function or does not return a string.
+ * @returns {void}
+ */
+export function verifyCallback(callback) {
+ if (typeof callback !== "function") {
+ throw new Error("Userale auth callback must be a function");
+ }
+ const result = callback();
+ if (typeof result !== "string") {
+ throw new Error("Userale auth callback must return a string");
+ }
+}
+
+/**
+ * Resets the authCallback to null. Used for primarily for testing, but could be used
+ * to remove the callback in production.
+ * @returns {void}
+ */
+export function resetAuthCallback() {
+ authCallback = null;
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 6d09c71..f34c080 100644
--- a/src/main.js
+++ b/src/main.js
@@ -32,6 +32,7 @@
export let started = false;
export {defineCustomDetails as details} from './attachHandlers.js';
+export {registerAuthCallback as registerAuthCallback} from './auth.js';
export {
addCallbacks as addCallbacks,
removeCallbacks as removeCallbacks,
diff --git a/src/sendLogs.js b/src/sendLogs.js
index c2da246..d420fa0 100644
--- a/src/sendLogs.js
+++ b/src/sendLogs.js
@@ -5,9 +5,9 @@
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import { updateAuthHeader } from "./auth.js";
+
let sendIntervalId = null;
/**
@@ -39,7 +41,7 @@
* @return {Number} The newly created interval id.
*/
export function sendOnInterval(logs, config) {
- return setInterval(function() {
+ return setInterval(function () {
if (!config.on) {
return;
}
@@ -57,8 +59,13 @@
* @param {Object} config Configuration object to be read from.
*/
export function sendOnClose(logs, config) {
- window.addEventListener('pagehide', function () {
+ window.addEventListener("pagehide", function () {
if (config.on && logs.length > 0) {
+ // NOTE: sendBeacon does not support auth headers,
+ // so this will fail if auth is required.
+ // The alternative is to use fetch() with keepalive: true
+ // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description
+ // https://stackoverflow.com/a/73062712/9263449
navigator.sendBeacon(config.url, JSON.stringify(logs));
logs.splice(0); // clear log queue
}
@@ -76,18 +83,18 @@
// @todo expose config object to sendLogs replate url with config.url
export function sendLogs(logs, config, retries) {
const req = new XMLHttpRequest();
-
- // @todo setRequestHeader for Auth
const data = JSON.stringify(logs);
- req.open('POST', config.url);
+ req.open("POST", config.url);
+
+ // Update headers
+ updateAuthHeader(config);
if (config.authHeader) {
- req.setRequestHeader('Authorization', config.authHeader)
+ req.setRequestHeader("Authorization", config.authHeader);
}
+ req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
- req.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
-
- req.onreadystatechange = function() {
+ req.onreadystatechange = function () {
if (req.readyState === 4 && req.status !== 200) {
if (retries > 0) {
sendLogs(logs, config, retries--);
diff --git a/test/auth_spec.js b/test/auth_spec.js
new file mode 100644
index 0000000..86029cb
--- /dev/null
+++ b/test/auth_spec.js
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {expect} from 'chai';
+import sinon from 'sinon';
+import {authCallback, registerAuthCallback, resetAuthCallback, updateAuthHeader, verifyCallback} from '../src/auth';
+
+describe('verifyCallback', () => {
+ it('should not throw error for valid callback', () => {
+ const validCallback = sinon.stub().returns('someString');
+ expect(() => verifyCallback(validCallback)).to.not.throw();
+ });
+
+ it('should throw error for non-function callback', () => {
+ const nonFunctionCallback = 'notAFunction';
+ expect(() => verifyCallback(nonFunctionCallback)).to.throw('Userale auth callback must be a function');
+ });
+
+ it('should throw error for non-string callback return', () => {
+ const invalidReturnCallback = sinon.stub().returns(123);
+ expect(() => verifyCallback(invalidReturnCallback)).to.throw('Userale auth callback must return a string');
+ });
+
+ it('should not throw error for valid callback with empty string return', () => {
+ const validCallback = sinon.stub().returns('');
+ expect(() => verifyCallback(validCallback)).to.not.throw();
+ });
+});
+
+describe('registerAuthCallback', () => {
+ afterEach(() => {
+ resetAuthCallback();
+ });
+
+ it('should register a valid callback', () => {
+ const validCallback = sinon.stub().returns('someString');
+ expect(registerAuthCallback(validCallback)).to.be.true;
+ expect(authCallback).to.equal(validCallback);
+ });
+
+ it('should not register a non-function callback', () => {
+ const nonFunctionCallback = 'notAFunction';
+ expect(registerAuthCallback(nonFunctionCallback)).to.be.false;
+ expect(authCallback).to.be.null;
+ });
+
+ it('should not register a callback with invalid return type', () => {
+ const invalidReturnCallback = sinon.stub().returns(123);
+ expect(registerAuthCallback(invalidReturnCallback)).to.be.false;
+ expect(authCallback).to.be.null;
+ });
+
+ it('should register a callback with empty string return', () => {
+ const validCallback = sinon.stub().returns('');
+ expect(registerAuthCallback(validCallback)).to.be.true;
+ expect(authCallback).to.equal(validCallback);
+ });
+});
+
+describe('updateAuthHeader', () => {
+ let config;
+
+ beforeEach(() => {
+ // Initialize config object before each test
+ config = { authHeader: null };
+ });
+
+ afterEach(() => {
+ resetAuthCallback();
+ });
+
+ it('should update auth header when authCallback is provided', () => {
+ const validCallback = sinon.stub().returns('someString');
+ registerAuthCallback(validCallback);
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.equal('someString');
+ });
+
+ it('should not update auth header when authCallback is not provided', () => {
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.be.null;
+ });
+
+ it('should not update auth header when authCallback returns non-string', () => {
+ const invalidReturnCallback = sinon.stub().returns(123);
+ registerAuthCallback(invalidReturnCallback);
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.be.null;
+ });
+
+ it('should update auth header with empty string return from authCallback', () => {
+ const validCallback = sinon.stub().returns('');
+ registerAuthCallback(validCallback);
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.equal('');
+ });
+
+ it('should handle errors thrown during authCallback execution', () => {
+ const errorThrowingCallback = sinon.stub().throws(new Error('Callback execution failed'));
+ registerAuthCallback(errorThrowingCallback);
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.be.null;
+ });
+
+ it('should not update auth header after unregistering authCallback', () => {
+ const validCallback = sinon.stub().returns('someString');
+ registerAuthCallback(validCallback);
+ updateAuthHeader(config, authCallback);
+ expect(config.authHeader).to.equal('someString');
+
+ // Unregister authCallback
+ updateAuthHeader(config, null);
+ expect(config.authHeader).to.equal('someString');
+ });
+ });
\ No newline at end of file
diff --git a/test/sendLogs_spec.js b/test/sendLogs_spec.js
index 35686e6..6059724 100644
--- a/test/sendLogs_spec.js
+++ b/test/sendLogs_spec.js
@@ -17,7 +17,8 @@
import {expect} from 'chai';
import {JSDOM} from 'jsdom';
import sinon from 'sinon';
-import {sendOnInterval, sendOnClose} from '../src/sendLogs';
+import {initSender, sendOnInterval, sendOnClose} from '../src/sendLogs';
+import {registerAuthCallback} from '../src/auth';
import 'global-jsdom/register'
describe('sendLogs', () => {
@@ -110,4 +111,42 @@
global.window.dispatchEvent(new window.CustomEvent('pagehide'))
sinon.assert.notCalled(sendBeaconSpy)
});
+
+ it('sends logs with proper auth header when using registerCallback', (done) => {
+ let requests = []
+ const originalXMLHttpRequest = global.XMLHttpRequest;
+ const conf = { on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1 };
+ const logs = [];
+ const clock = sinon.useFakeTimers();
+ const xhr = sinon.useFakeXMLHttpRequest();
+ global.XMLHttpRequest = xhr;
+ xhr.onCreate = (xhr) => {
+ requests.push(xhr);
+ };
+
+ // Mock the authCallback function
+ const authCallback = sinon.stub().returns('fakeAuthToken');
+
+ // Register the authCallback
+ registerAuthCallback(authCallback);
+
+ // Initialize sender with logs and config
+ initSender(logs, conf);
+
+ // Simulate log entry
+ logs.push({ foo: 'bar' });
+
+ // Trigger interval to send logs
+ clock.tick(conf.transmitInterval);
+
+ // Verify that the request has the proper auth header
+ expect(requests.length).to.equal(1);
+ expect(requests[0].requestHeaders.Authorization).to.equal('fakeAuthToken');
+
+ // Restore XMLHttpRequest and clock
+ xhr.restore();
+ clock.restore();
+ global.XMLHttpRequest = originalXMLHttpRequest;
+ done()
+ });
});