Compare commits

...
Author SHA1 Message Date
Dima May 3a5bcc1f04 Fix integration client session and CSRF handling for login 2026-09-29 11:12:02 +00:00
Ryan Mayo 030a8bd24a Merge pull request #289 from NationalSecurityAgency/xhr_withCredentials
Xhr with credentials
2026-09-23 15:19:21 -04:00
Dima May 532f33a8a7 fixed tests 2026-09-23 15:45:29 +00:00
Dima May b070e4058e only set xhr.withCredentials to true in PKI mode 2026-09-23 14:39:08 +00:00
Dima May 6b1ba358d8 Merge pull request #288 from NationalSecurityAgency/remove_scheuduled_ci_actions
updated skills-client CI actions to no longer run on a schedule
2026-09-15 11:46:54 -04:00
rmmayo e845fb7788 updated skills-client CI actions to no longer run on a schedule 2026-09-15 15:28:24 +00:00
rmmayo 599e838086 updated skills-client-js to add the current window.location.pathname as the default parentPath option 2026-09-15 15:11:02 +00:00
rmmayo ad5ab89439 creating_release_version_3.6.4 2026-09-14 19:38:41 +00:00
rmmayo 3c408f555f updated the dependencies 2026-09-14 19:11:24 +00:00
rmmayo ea523e9381 updated the versions for testing 2026-09-14 13:59:38 +00:00
rmmayo cea08a3dd5 set parentPath to window.location.pathname so that it is available to the skills-display iframe 2026-09-09 21:56:16 +00:00
rmmayo 731853a8a6 globalEventResult can be empty string or empty JSON object (after spring-boot 4 upgrade) 2026-08-26 16:46:03 +00:00
rmmayo 78c338fd36 updated spring-boot 2026-08-26 16:45:12 +00:00
rmmayo 80052f77ac remove unnecessary spring.autoconfigure.exclude props from tests since it's the default in the main application.yml 2026-08-26 16:45:00 +00:00
rmmayo b4e7718741 Updated the regex to match the completed array when it contains an object with:
- `"type": "Overall"`
- `"level": 1`

regardless of property order.
2026-08-26 14:59:35 +00:00
rmmayo 42c40ce92f Updated the regex to match the completed array when it contains an object with:
- `"type": "Overall"`
- `"level": 1`

regardless of property order.
2026-08-26 12:03:02 +00:00
Ryan Mayo 18ad1af38e Merge pull request #287 from NationalSecurityAgency/spring_4_upgrade
upgraded to spring boot 4.1.0
2026-08-12 16:11:07 -04:00
25 changed files with 105 additions and 69 deletions

No files matched your search

@@ -7,8 +7,11 @@ env:
JAVA_VERSION: 25
on:
schedule:
- cron: '0 8 * * *'
push:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -12,11 +12,6 @@ on:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
pull_request:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
-5
View File
@@ -12,11 +12,6 @@ on:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
pull_request:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -10,11 +10,6 @@ on:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
pull_request:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
jobs:
@@ -13,8 +13,6 @@ on:
pull_request:
paths-ignore:
- 'README.md'
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -85,7 +83,7 @@ jobs:
strategy:
fail-fast: false
matrix:
skillsJsVersion: [ '3.1.1', '3.2.0', '3.3.0', '3.3.1', '3.4.0', '3.4.1', '3.5.0']
skillsJsVersion: [ '3.5.0', '3.6.0', '3.6.1', '3.6.2', '3.6.3']
services:
postgres:
+2
View File
@@ -16,3 +16,5 @@
/skills-client-ng/dist/
/skills-client-ng/projects/skilltree/skills-client-ng/src/version.js
/skills-client-integration/skills-int-e2e-test/cypress/downloads/*
AGENTS.md
+2 -2
View File
@@ -7,13 +7,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<version>4.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<packaging>pom</packaging>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.4-SNAPSHOT</version>
<licenses>
<license>
@@ -1,6 +1,6 @@
{
"name": "skills-int-client-js",
"version": "3.6.3",
"version": "3.6.4",
"license": "Apache-2.0",
"description": "SkillTree Client Lib Integration Tests - Native JS",
"main": "index.js",
@@ -16,13 +16,13 @@
},
"author": "SkillTree Team",
"dependencies": {
"@skilltree/skills-client-js": "3.6.3",
"axios": "1.18.0",
"@skilltree/skills-client-js": "3.6.4",
"axios": "1.20.0",
"bootstrap": "5.3.8",
"jquery": "4.0.0"
},
"devDependencies": {
"fs-extra": "11.3.5",
"fs-extra": "11.4.0",
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"live-server": "1.2.2"
@@ -7,7 +7,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.4-SNAPSHOT</version>
</parent>
<artifactId>skills-int-client-js</artifactId>
@@ -20,6 +20,8 @@ const homePage = '/native/index.html'
context("Global Events Tests", () => {
const level1CompleteRegex = /"completed"\s*:\s*\[(?=[\s\S]*?\{[^{}]*?(?:"type"\s*:\s*"Overall"[^{}]*?"level"\s*:\s*1|"level"\s*:\s*1[^{}]*?"type"\s*:\s*"Overall")[^{}]*?\})[\s\S]*?\]/;
it("global event show correct results", () => {
cy.createDefaultProject();
@@ -34,7 +36,7 @@ context("Global Events Tests", () => {
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it("global event show correct results (skills reported directly to backend endpoint)", () => {
cy.createDefaultProject();
@@ -46,7 +48,7 @@ context("Global Events Tests", () => {
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it('global event does not update when skill reported for a different project', () => {
@@ -18,6 +18,8 @@ const homePage = '/native/index.html'
context("Token timeout JS Tests", () => {
const level1CompleteRegex = /"completed"\s*:\s*\[(?=[\s\S]*?\{[^{}]*?(?:"type"\s*:\s*"Overall"[^{}]*?"level"\s*:\s*1|"level"\s*:\s*1[^{}]*?"type"\s*:\s*"Overall")[^{}]*?\})[\s\S]*?\]/;
if (Cypress.env('timeout') === true || Cypress.env('timeout') === 'true') {
it("report skill with expired token, retry checker will resend with new token", () => {
cy.createDefaultProject();
@@ -28,14 +30,17 @@ context("Token timeout JS Tests", () => {
cy.wait(71000) // wait 71 seconds to ensure the token has expired
cy.get('[data-cy="exampleDirectiveClickEventButton"]').click();
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
// should be empty string or empty JSON object
cy.get('pre[data-cy=globalEventResult]')
.invoke('text')
.should('match', /^\s*$|^\s*\{\s*\}\s*$/);
cy.wait("@getToken", { timeout: 60000 }); // wait ~ another 50 seconds to all for the RetryChecker to resubmit
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it('skill display - navigate after token expires', () => {
@@ -52,8 +52,8 @@
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"pdf-parse": "1.1.1",
"semver": "7.7.2",
"wait-on": "8.0.3",
"semver": "7.8.5",
"wait-on": "9.1.0",
"wait-port": "1.1.0"
}
}
@@ -5,7 +5,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.4-SNAPSHOT</version>
</parent>
<artifactId>skills-int-e2e-test</artifactId>
<description>End to End tests for Skills client libs using skills-int-service app</description>
@@ -14,7 +14,6 @@
spring.datasource.username = postgres
spring.datasource.url = jdbc:postgresql://localhost:5432/skills
spring.datasource.password = skillsPassword
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
skills.config.ui.userCommunityUserTagKey=dragons
skills.config.ui.userCommunityUserTagValue=DivineDragon
@@ -5,7 +5,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.4-SNAPSHOT</version>
</parent>
<artifactId>skills-int-service</artifactId>
<description>SkillTree Client Lib Integration Tests</description>
@@ -128,6 +128,9 @@ public class Controller {
private void authIfNecessary() {
if (!skillsConfig.getAuthMode().equalsIgnoreCase("pki")) {
// Seed the session and XSRF-TOKEN before /performLogin, which requires CSRF validation.
// SecurityRestTemplateCustomizer retains the cookies and sends X-XSRF-TOKEN on later requests.
restTemplate.getForEntity(skillsConfig.getServiceUrl() + "/app/userInfo", String.class);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
@@ -16,6 +16,8 @@
package skills.example;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
@@ -45,12 +47,22 @@ public class SecurityRestTemplateCustomizer implements RestTemplateCustomizer {
// not by default keeps track of session. This method is called by the
// RestTemplateBuilder used in the skills.example.Controller constructor
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
HttpClient client = getHttpClient();
BasicCookieStore cookieStore = new BasicCookieStore();
HttpClient client = getHttpClient(cookieStore);
clientHttpRequestFactory.setHttpClient(client);
restTemplate.setRequestFactory(clientHttpRequestFactory);
restTemplate.getInterceptors().add((request, body, execution) -> {
for (Cookie cookie : cookieStore.getCookies()) {
if ("XSRF-TOKEN".equals(cookie.getName())) {
request.getHeaders().set("X-XSRF-TOKEN", cookie.getValue());
break;
}
}
return execution.execute(request, body);
});
}
private HttpClient getHttpClient() {
private HttpClient getHttpClient(BasicCookieStore cookieStore) {
try {
SSLContext sslContext = SSLContexts.createSystemDefault();
HostnameVerifier allowAllHosts = new NoopHostnameVerifier();
@@ -67,6 +79,7 @@ public class SecurityRestTemplateCustomizer implements RestTemplateCustomizer {
return HttpClients.custom()
.useSystemProperties()
.setDefaultCookieStore(cookieStore)
.setConnectionManager(poolingHttpClientConnectionManager)
.build();
} catch (Exception e) {
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@skilltree/skills-client-js",
"version": "3.6.3",
"version": "3.6.4",
"license": "Apache-2.0",
"description": "SkillTree Client Lib - Native JS Lib",
"main": "dist/skills-client-js.umd.min.js",
@@ -21,7 +21,7 @@
"buildSkipTests": "npm run lint && npm run clean && npm run build:umd && npm run build:esm",
"build:umd": "rollup --config --format umd --file dist/skills-client-js.umd.min.js --name SkillsClient --bundleConfigAsCjs",
"build:esm": "rollup --config --format esm --file dist/skills-client-js.esm.min.js --bundleConfigAsCjs",
"deploy-to-examples": "npm run build && cp ./dist/skills-display-js.umd.min.js ../skills-examples/skills-example-client-js/app/assets/js/@skilltree/skills-client-js/dist",
"deploy-to-examples": "npm run clean && npm run build:umd && npm run build:esm && rm ../skills-client-integration/skills-int-service/src/main/resources/public/native/assets/js/@skilltree/skills-client-js/dist/* && cp ./dist/* ../skills-client-integration/skills-int-service/src/main/resources/public/native/assets/js/@skilltree/skills-client-js/dist/",
"test": "NODE_ENV=test jest --coverage",
"test-watch": "npx jest --passWithNoTests --coverage --watch",
"prepublishOnly": "npm run build",
@@ -36,27 +36,27 @@
"@babel/preset-env": "7.29.5",
"@babel/runtime": "7.29.2",
"@eslint/js": "10.0.1",
"@rollup/plugin-babel": "7.0.0",
"@rollup/plugin-commonjs": "29.0.2",
"@rollup/plugin-babel": "7.1.0",
"@rollup/plugin-commonjs": "29.0.3",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-replace": "6.0.3",
"@rollup/plugin-terser": "1.0.0",
"axios-mock-adapter": "2.1.0",
"babel-jest": "30.4.1",
"eslint": "10.3.0",
"globals": "17.6.0",
"jest": "30.4.2",
"jest-environment-jsdom": "30.4.1",
"babel-jest": "30.5.1",
"eslint": "10.10.0",
"globals": "17.12.0",
"jest": "30.5.1",
"jest-environment-jsdom": "30.5.1",
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"regenerator-runtime": "0.14.1",
"rollup": "4.60.3",
"rollup": "4.63.3",
"xhr-mock": "2.5.1"
},
"dependencies": {
"@stomp/stompjs": "7.3.0",
"axios": "1.18.0",
"axios": "1.20.0",
"js-logger": "1.6.1",
"postmate": "1.5.2",
"sockjs-client": "1.6.1"
+1 -2
View File
@@ -58,7 +58,7 @@ export default {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${conf.getServiceUrl()}/api/projects/${conf.getProjectId()}/skillsClientVersion`);
xhr.withCredentials = true;
xhr.withCredentials = conf.isPKIMode();
if (!conf.isPKIMode()) {
xhr.setRequestHeader('Authorization', `Bearer ${conf.getAuthToken()}`);
}
@@ -111,7 +111,6 @@ export default {
xhr.open('GET', authenticator);
} else {
xhr.open('GET', `${serviceUrl}/api/projects/${projectId}/token`);
xhr.withCredentials = true;
}
xhr.onreadystatechange = () => {
@@ -28,7 +28,7 @@ const markedForDestruction = [];
export default class SkillsDisplayJS {
constructor({ options, theme, version, handleRouteChanged, userId } = {}) {
log.debug(`SkillsClient::SkillsDisplayJS::Constructing with options [${options}], theme [${theme}], version [${version}], userId [${userId}]`);
this._options = { ...{ }, ...options };
this._options = { parentPath: window.location.pathname, ...options };
this._theme = theme;
this._version = version;
this._handleRouteChanged = handleRouteChanged;
@@ -55,7 +55,7 @@ export default class SkillsLevelJS {
this._projectId = SkillsConfiguration.getProjectId();
log.debug(`SkillsClient::SkillsLevelJS::getting projectId from SkillsConfiguration: [${this._projectId}]`);
}
const requestConfig = { withCredentials: true };
const requestConfig = { withCredentials: SkillsConfiguration.isPKIMode() };
if (!SkillsConfiguration.isPKIMode()) {
const authToken = SkillsConfiguration.getAuthToken();
requestConfig.headers = { Authorization: `Bearer ${authToken}` };
@@ -138,7 +138,7 @@ const reportInternal = (resolve, reject, userSkillId, timestamp, isRetry, retryA
const xhr = new XMLHttpRequest();
xhr.open('POST', `${SkillsConfiguration.getServiceUrl()}/api/projects/${SkillsConfiguration.getProjectId()}/skills/${userSkillId}`);
xhr.withCredentials = true;
xhr.withCredentials = SkillsConfiguration.isPKIMode();
if (!SkillsConfiguration.isPKIMode()) {
xhr.setRequestHeader('Authorization', `Bearer ${SkillsConfiguration.getAuthToken()}`);
}
@@ -482,6 +482,7 @@ describe('SkillsDisplayJS', () => {
isSummaryOnly: false,
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
parentPath: '/',
};
mockVersion = `${Math.random()}`;
@@ -497,6 +498,7 @@ describe('SkillsDisplayJS', () => {
authenticator: 'pki',
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
@@ -538,6 +540,7 @@ describe('SkillsDisplayJS', () => {
authenticator: mockAuthenticator,
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -578,6 +581,7 @@ describe('SkillsDisplayJS', () => {
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
internalBackButton: false,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -618,6 +622,7 @@ describe('SkillsDisplayJS', () => {
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
internalBackButton: true,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -657,6 +662,7 @@ describe('SkillsDisplayJS', () => {
authenticator: mockAuthenticator,
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -695,7 +701,7 @@ describe('SkillsDisplayJS', () => {
describe('construction', () => {
describe('options', () => {
it('accepts options in the constructor', () => {
const mockOptions = { authenticator: 'option' };
const mockOptions = { authenticator: 'option', parentPath: 'non-default/' };
const client = new SkillsDisplayJS({
options: mockOptions,
});
@@ -704,14 +710,14 @@ describe('SkillsDisplayJS', () => {
it('supports null or undefined being passed', () => {
let client = new SkillsDisplayJS({ options: null });
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/' });
client = new SkillsDisplayJS({ options: undefined });
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/'} );
});
it('supports empty constructor', () => {
it('supports empty constructor, defaults parentPath to window.location.pathname', () => {
const client = new SkillsDisplayJS();
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/'} );
});
it('accepts valid options', () => {
@@ -18,21 +18,33 @@ import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
import { SkillsReporter } from '../../src/reporter/SkillsReporter.js';
// These tests exercise HTTP authentication, not the background websocket connection.
jest.mock('@stomp/stompjs');
describe('authPkiTests()', () => {
const mockServiceUrl = 'http://some.com';
const mockProjectId = 'proj1';
SkillsConfiguration.configure({
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
authenticator: 'pki',
// replace the real XHR object with the mock XHR object before each test
beforeEach(async () => {
mock.setup();
SkillsConfiguration.logout();
mock.get(/.*\/public\/status/, (req, res) => res.status(200).body('{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}'));
mock.post(/.*\/api\/projects\/proj1\/skillsClientVersion/, (req, res) => res.status(200).body('{"success":true,"explanation":null}'));
await SkillsConfiguration.configure({
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
authenticator: 'pki',
});
});
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => mock.setup());
// put the real XHR object back and clear the mocks after each test
afterEach(() => mock.teardown());
afterEach(() => {
SkillsReporter.cancelRetryChecker();
window.localStorage.removeItem('skillTreeRetryQueue');
SkillsConfiguration.logout();
mock.teardown();
});
it('successful call in pki mode', async () => {
expect.assertions(2);
@@ -55,6 +55,8 @@ describe('userSkillsService', () => {
});
afterEach(() => {
SkillsReporter.cancelRetryChecker();
jest.restoreAllMocks();
window.XMLHttpRequest = realXMLHttpRequest;
});
@@ -79,9 +81,16 @@ describe('userSkillsService', () => {
expect(MockXMLHttpRequest.setRequestHeader).toHaveBeenCalledWith('Authorization', `Bearer ${SkillsConfiguration.getAuthToken()}`);
});
it('enables withCredentials', () => {
it('disables withCredentials in token mode', () => {
SkillsReporter.reportSkill(mockUserSkillId);
expect(MockXMLHttpRequest.withCredentials).toBeTruthy();
expect(MockXMLHttpRequest.withCredentials).toBe(false);
});
it('enables withCredentials without an Authorization header in PKI mode', () => {
SkillsConfiguration.setAuthToken('pki');
SkillsReporter.reportSkill(mockUserSkillId);
expect(MockXMLHttpRequest.withCredentials).toBe(true);
expect(MockXMLHttpRequest.setRequestHeader).not.toHaveBeenCalledWith('Authorization', expect.anything());
});
it('sends out the ajax request', () => {