Compare commits

...
39 Commits
Author SHA1 Message Date
rmmayo 1925c31934 upgrade spring-boot to 4.1.2 2026-10-23 14:13:44 +00:00
Ryan Mayo 48ff8e052c Merge pull request #291 from NationalSecurityAgency/tests_fix
Tests fix
2026-10-08 08:14:07 -04:00
Dima May 037009f8a8 Fixed OAuth Cypress login by probing session instead of project token 2026-10-08 10:29:31 +00:00
Dima May dca695d685 Increased maximum log requests during a one-minute window 2026-10-07 18:28:00 +00:00
Dima May 24905e9a42 Fix cypress tests client CSRF handling and backed user creation process 2026-10-05 10:28:14 +00:00
Dima May 5a98e7fd2c creating_release_version_3.6.5 2026-10-02 11:43:21 +00:00
Ryan Mayo 897d1b9b1c Merge pull request #290 from NationalSecurityAgency/int_client_session
Fix integration client session and CSRF handling for login
2026-09-29 09:04:35 -04:00
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
Dima May 58f6265836 upgraded to spring boot 4.1.0 2026-08-12 19:35:43 +00:00
Ryan Mayo e3f299376c Merge pull request #285 from arpitjain099/fix/encode-oauth-redirect-uri
Encode the redirect target before interpolating it into the authenticator URL
2026-07-31 10:09:11 -04:00
Arpit Jain 2b66377a30 Encode the redirect target before interpolating it into the authenticator URL
getAuthenticationToken built the OAuth redirect by interpolating
window.location straight into the query string, so any reserved
character already present in the current URL was carried through as
structure rather than data. A page reached with an ampersand in its
query string could therefore append parameters to the authenticator
URL, including a second skillsRedirectUri.

Use encodeURIComponent(window.location.href) so the value is passed as
a single opaque parameter. Closes #284.

Signed-off-by: Arpit Jain <[email protected]>
2026-07-31 00:06:02 +09:00
Dima May b88c19d0c7 Merge pull request #283 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.18.0
Bump axios from 1.17.0 to 1.18.0 in /skills-client-integration/skills-int-client-js
2026-07-21 07:21:08 -04:00
dependabot[bot] 558cecacdd Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-21 10:55:51 +00:00
Dima May dbcc95f951 Merge pull request #282 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.18.0
Bump axios from 1.17.0 to 1.18.0 in /skills-client-js
2026-07-21 06:54:48 -04:00
dependabot[bot] 50ac5c413f Bump axios from 1.17.0 to 1.18.0 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-20 20:13:33 +00:00
Don Walizer cf0961c435 Merge pull request #280 from NationalSecurityAgency/upgrades_gh-actions_node_java
upgrade gh actions, node and java
2026-07-06 09:38:03 -04:00
Dima May 56cb884fdb Merge pull request #279 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/babel/core-7.29.6
Bump @babel/core from 7.29.0 to 7.29.6 in /skills-client-js
2026-07-06 08:02:34 -04:00
Dima May 6af2944841 upgrade gh actions, node and java 2026-07-06 11:49:49 +00:00
dependabot[bot] a301650700 Bump @babel/core from 7.29.0 to 7.29.6 in /skills-client-js
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.6/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.29.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-25 14:51:08 +00:00
rmmayo d19947c814 upgraded spring boot to v3.5.16 2026-06-25 14:49:46 +00:00
Dima May 361bae4289 spring boot upgrade; set license-maven-plugin to use the latest version 2026-06-11 14:50:16 +00:00
Dima May aae9cc7ee9 Merge pull request #278 from NationalSecurityAgency/t#277/upgrade_axios
#277 - upgrade axios
2026-06-08 15:24:47 -04:00
rmmayo 60c3d5edb1 #277 - upgrade axios 2026-06-08 14:48:22 +00:00
32 changed files with 339 additions and 199 deletions

No files matched your search

@@ -2,9 +2,16 @@
name: Continuous Integration expired token tests
env:
NODE_VERSION: 26
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
@@ -34,16 +41,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ vars.JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -53,7 +60,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -61,7 +68,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -79,7 +86,7 @@ jobs:
# cd ..
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
@@ -134,7 +141,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
+13 -14
View File
@@ -2,17 +2,16 @@
name: Continuous Integration with OAuth
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
paths-ignore:
- '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
@@ -43,16 +42,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ vars.JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -62,7 +61,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -70,7 +69,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -86,7 +85,7 @@ jobs:
# cd ..
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
@@ -165,7 +164,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
+12 -13
View File
@@ -2,17 +2,16 @@
name: Continuous Integration
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
paths-ignore:
- '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
@@ -43,16 +42,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ vars.JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -62,7 +61,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -71,7 +70,7 @@ jobs:
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
@@ -112,7 +111,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
@@ -1,16 +1,15 @@
name: Client Libs Backward Compatibility
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
paths-ignore:
- '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:
@@ -37,16 +36,16 @@ jobs:
build_skills_client_int:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ vars.JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -56,7 +55,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -64,7 +63,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -83,7 +82,7 @@ jobs:
cd ../
- name: upload service jar
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: service jar
path: ./skills-client-integration/skills-int-service/target/skills-int-service-*.jar
@@ -117,16 +116,16 @@ jobs:
matrix:
version: ${{ fromJSON(needs.get_skills_service_versions.outputs.versions) }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ vars.JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -136,7 +135,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -144,7 +143,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -160,7 +159,7 @@ jobs:
cd ..
- name: download integration test apps
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: service jar
path: ./skills-client-integration/skills-int-service/target/
@@ -182,7 +181,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result artifacts for ${{ matrix.version }}
path: |
@@ -2,6 +2,9 @@
name: Skills-service Backward Compatibility
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
@@ -10,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
@@ -21,18 +22,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
11
${{ vars.JAVA_VERSION }}
${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -42,7 +43,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -50,7 +51,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -58,7 +59,7 @@ jobs:
${{ runner.os }}-node-
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
@@ -70,7 +71,7 @@ jobs:
cd ..
- name: upload service jar
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: service jar
path: ./skills-service/service/target/skills-service-*.jar
@@ -82,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:
@@ -105,18 +106,18 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION }}
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
11
${{ vars.JAVA_VERSION }}
${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -126,7 +127,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -134,7 +135,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -152,7 +153,7 @@ jobs:
# cd ..
# - name: Checkout specific branch fo skills-service
# uses: actions/checkout@v4
# uses: actions/checkout@v6
# with:
# repository: NationalSecurityAgency/skills-service
# path: skills-service
@@ -165,7 +166,7 @@ jobs:
# ls ./
# cd ..
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: service jar
path: ./skills-service/
@@ -203,7 +204,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result artifacts for [${{ matrix.skillsJsVersion }}] version
path: |
@@ -229,7 +230,7 @@ jobs:
#
# - name: upload result artifacts
# if: ${{ always() }}
# uses: actions/upload-artifact@v4
# uses: actions/upload-artifact@v7
# with:
# name: result artifacts
# path: |
+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 -6
View File
@@ -7,13 +7,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.14</version>
<version>4.1.2</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.5-SNAPSHOT</version>
<licenses>
<license>
@@ -31,10 +31,6 @@
<properties>
<!--suppress UnresolvedMavenProperty -->
<nexusServer>${nexus.url}</nexusServer>
<frontend-maven-plugin.version>1.10.3</frontend-maven-plugin.version>
<!-- TODO: Temporary override spring boot versions, remove once spring boot upgrades these versions -->
<commons-lang3.version>3.19.0</commons-lang3.version>
</properties>
</project>
@@ -1,6 +1,6 @@
{
"name": "skills-int-client-js",
"version": "3.6.3",
"version": "3.6.5",
"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.16.0",
"@skilltree/skills-client-js": "3.6.5",
"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.5-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', () => {
@@ -45,6 +45,44 @@ const baseUrl = Cypress.config().baseUrl;
const skillsDisplayHomePage = '/native/clientDisplay.html'
// cy.request does not add Axios's X-XSRF-TOKEN header automatically. Keep the
// cookie and header in sync for requests to the Skills Service, including
// registration and login requests made before the browser visits the dashboard.
Cypress.Commands.overwrite('request', (originalFn, ...args) => {
const options = typeof args[0] === 'object'
? { ...args[0] }
: typeof args[1] === 'string'
? { method: args[0], url: args[1], body: args[2] }
: { url: args[0], body: args[1] };
const method = (options.method || 'GET').toUpperCase();
const url = new URL(options.url, Cypress.config('baseUrl'));
const backend = new URL(Cypress.config('baseUrl'));
const isSkillsService = url.origin === backend.origin ||
(url.hostname === backend.hostname && url.port === '8080');
if (!isSkillsService || ['GET', 'HEAD', 'OPTIONS', 'TRACE'].includes(method)) {
return originalFn(...args);
}
return cy.getCookie('XSRF-TOKEN').then((cookie) => {
if (cookie) {
return originalFn({
...options,
headers: { ...options.headers, 'X-XSRF-TOKEN': cookie.value },
});
}
// The first request can be registration, logout, or login. A GET loads
// the token without requiring an authenticated session.
return cy.request({ url: `${url.origin}/app/userInfo` })
.then(() => cy.getCookie('XSRF-TOKEN').should('exist'))
.then((xsrfCookie) => originalFn({
...options,
headers: { ...options.headers, 'X-XSRF-TOKEN': xsrfCookie.value },
}));
});
});
Cypress.Commands.add('onlyOn', (enabled) => {
if (enabled !== true) {
cy.state('runnable').ctx.skip()
@@ -59,15 +97,20 @@ Cypress.Commands.add("backendRegister", (user, pass, grantRoot) => {
return cy.request(`${backend}/app/users/validExistingDashboardUserId/${user}`)
.then((response) => {
if (response.body !== true) {
cy.log(`Creating user [${user}]`)
cy.request('PUT', `${backend}/createAccount`, {
const userBody = {
firstName: 'Firstname',
lastName: 'LastName',
email: user,
password: pass,
});
}
if (grantRoot) {
cy.request('POST', `${backend}/grantFirstRoot`);
cy.log(`Creating root user [${user}]`)
cy.request('PUT', `${backend}/createRootAccount`, userBody)
} else {
cy.log(`Creating user [${user}]`)
cy.request('PUT', `${backend}/createAccount`, userBody);
}
cy.backendLogout()
} else {
@@ -230,18 +273,23 @@ Cypress.Commands.add('skillsLog', (message) => {
});
});
Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
Cypress.Commands.add('loginBySingleSignOn', () => {
Cypress.log({
name: 'loginBySingleSignOn',
})
// first try to get a skills token,
cy.request({
url: `http://localhost:8080/api/projects/${projId}/token`,
// Check the session without saving a protected request for Spring to replay
// after OAuth login, before the test has created any projects.
return cy.request({
url: `${backend}/app/userInfo`,
failOnStatusCode: false,
}).then((tokenResp) => {
if (tokenResp.status === 401) {
cy.skillsLog('Skills token request failed, authenticating with OAuth provider...');
followRedirect: false,
}).then((sessionResp) => {
const requiresLogin = sessionResp.status === 401 ||
(sessionResp.status === 200 && !sessionResp.body?.userId) ||
([301, 302, 303, 307, 308].includes(sessionResp.status) && Boolean(sessionResp.headers.location));
if (requiresLogin) {
cy.skillsLog('Skills session requires authentication, authenticating with OAuth provider...');
cy.request({
url: 'http://localhost:8080/oauth2/authorization/hydra',
qs: { skillsRedirectUri: baseUrl, },
@@ -253,10 +301,14 @@ Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
const $html = Cypress.$(resp.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
expect(authenticityToken, 'OAuth login CSRF token').to.be.a('string').and.not.be.empty;
expect(challenge, 'OAuth login challenge').to.be.a('string').and.not.be.empty;
const options = {
method: 'POST',
url: 'http://localhost:3000/login',
form: true, // we are submitting a regular form body
followRedirect: false,
failOnStatusCode: false,
body: {
_csrf: authenticityToken,
challenge,
@@ -268,38 +320,53 @@ Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
};
cy.request(options).then((resp2) => {
expect(resp2.status).to.eq(200)
expect(
resp2.status,
`OAuth login: ${JSON.stringify({
location: resp2.headers.location,
body: resp2.body,
})}`,
).to.eq(302);
if (resp2.redirects[resp2.redirects.length-1].includes('/consent?consent_challenge')) {
cy.skillsLog('Granting consent with OAuth provider...');
const $html = Cypress.$(resp2.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
// const consentUrl = resp2.redirects.filter(r => r.includes('/consent?consent_challenge'))[0].split(' ')[1]
const options = {
method: 'POST',
url: 'http://localhost:3000/consent',
form: true, // we are submitting a regular form body
qs: { consent_challenge: challenge },
body: {
_csrf: authenticityToken,
challenge,
grant_scope: 'openid',
// grant_scope: 'offline',
submit: 'Allow access',
remember: '1',
},
failOnStatusCode: false,
};
expect(resp2.headers.location, 'OAuth login redirect').to.be.a('string').and.not.be.empty;
const redirectUrl = new URL(resp2.headers.location, options.url).href;
return cy.request(redirectUrl).then((resp2) => {
expect(resp2.status).to.eq(200)
cy.request(options).then((resp3) => {
expect(resp3.status).to.eq(200)
})
}
if ((resp2.redirects || []).some(redirect => redirect.includes('/consent?consent_challenge')) &&
Cypress.$(resp2.body).find('input[name=challenge]').length > 0) {
cy.skillsLog('Granting consent with OAuth provider...');
const $html = Cypress.$(resp2.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
const options = {
method: 'POST',
url: 'http://localhost:3000/consent',
form: true, // we are submitting a regular form body
qs: { consent_challenge: challenge },
body: {
_csrf: authenticityToken,
challenge,
grant_scope: 'openid',
submit: 'Allow access',
remember: '1',
},
failOnStatusCode: false,
};
cy.request(options).then((resp3) => {
expect(resp3.status).to.eq(200)
})
}
})
})
})
} else {
cy.skillsLog('Received Skills token, already authenticated with OAuth provider.');
expect(sessionResp.status, 'Skills session response status').to.eq(200)
cy.skillsLog('Skills session already authenticated with OAuth provider.');
}
})
});
}).then(() => cy.request(`${backend}/app/userInfo`).then((resp) => {
expect(resp.status, 'Authenticated Skills session status').to.eq(200)
expect(resp.body.userId, 'Authenticated Skills user').to.be.a('string').and.not.be.empty;
}))
});
@@ -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.5-SNAPSHOT</version>
</parent>
<artifactId>skills-int-e2e-test</artifactId>
<description>End to End tests for Skills client libs using skills-int-service app</description>
@@ -104,7 +104,6 @@
<!-- mvn license:add-third-party-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>default-cli</id>
@@ -146,7 +145,7 @@
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0</licenseMerge>
<licenseMerge>MIT License|The MIT License</licenseMerge>
<licenseMerge>MIT License|MIT</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0|EPL-2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|Eclipse Public License - v 2.0</licenseMerge>
<licenseMerge>The GNU General Public License (GPL) Version 2 with the Classpath Exception|GPL2 w/ CPE</licenseMerge>
</licenseMerges>
@@ -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
@@ -38,3 +37,5 @@ logging.level.skills=DEBUG
skills.config.client.loggingEnabled=true
skills.config.client.loggingLevel=TRACE
skills.config.client.loggingMaxRequestsPerMinute=5000
@@ -26,4 +26,6 @@ skills.config.ui.userCommunityParagraphValidationMessage=May not contain divined
skills.config.ui.userCommunityRestrictedDescriptor=Divine Dragon
skills.config.ui.userCommunityBeforeLabel=For
skills.config.ui.userCommunityAfterLabel=Nation
skills.config.ui.defaultCommunityDescriptor=All Dragons
skills.config.ui.defaultCommunityDescriptor=All Dragons
skills.config.client.loggingMaxRequestsPerMinute=5000
@@ -5,7 +5,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.6.3-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
</parent>
<artifactId>skills-int-service</artifactId>
<description>SkillTree Client Lib Integration Tests</description>
@@ -23,6 +23,10 @@
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
</dependencies>
<build>
@@ -57,7 +61,6 @@
<!-- mvn license:add-third-party-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>default-cli</id>
@@ -99,7 +102,7 @@
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License 1.0</licenseMerge>
<licenseMerge>The 3-Clause BSD License|New BSD License</licenseMerge>
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0|EPL-2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|Eclipse Public License - v 2.0</licenseMerge>
<licenseMerge>The GNU General Public License (GPL) Version 2 with the Classpath Exception|GPL2 w/ CPE</licenseMerge>
<licenseMerge>MIT License|MIT</licenseMerge>
@@ -114,7 +117,6 @@
mvn com.mycila:license-maven-plugin:3.0:format-->
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>3.0</version>
<configuration>
<header>${basedir}/../license-add/LICENSE-HEADER.txt</header>
<excludes>
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,10 +15,8 @@
*/
package skills.example;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.util.LinkedMultiValueMap;
@@ -27,6 +25,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import skills.example.SkillsExampleApplication.SkillsConfig;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
@@ -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<>();
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -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;
@@ -23,11 +25,9 @@ import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@@ -47,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();
@@ -69,6 +79,7 @@ public class SecurityRestTemplateCustomizer implements RestTemplateCustomizer {
return HttpClients.custom()
.useSystemProperties()
.setDefaultCookieStore(cookieStore)
.setConnectionManager(poolingHttpClientConnectionManager)
.build();
} catch (Exception e) {
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,15 +18,18 @@ package skills.example.filters;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(SecurityProperties.DEFAULT_FILTER_ORDER-1)
@Order(VueEntryPointFilter.BEFORE_SECURITY_FILTER_ORDER)
public class VueEntryPointFilter implements Filter {
// -100 (Default Security Order) - 1
public static final int BEFORE_SECURITY_FILTER_ORDER = -101;
@Autowired
private VueEntryPointFilterUtils vueEntryPointFilterUtils;
@@ -1,4 +1,4 @@
/**
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@skilltree/skills-client-js",
"version": "3.6.3",
"version": "3.6.5",
"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",
@@ -30,33 +30,33 @@
"author": "SkillTree Team",
"devDependencies": {
"@babel/cli": "7.28.6",
"@babel/core": "7.29.0",
"@babel/core": "7.29.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/polyfill": "7.12.1",
"@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.16.0",
"axios": "1.20.0",
"js-logger": "1.6.1",
"postmate": "1.5.2",
"sockjs-client": "1.6.1"
+2 -3
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 = () => {
@@ -119,7 +118,7 @@ export default {
if (xhr.status !== 200) {
if (isOAuthMode && oauthRedirect && xhr.status === 401) {
// if we get 401 and we are using OAuth, then redirect to the OAuth Provider
const oauthAuthenticator = `${authenticator}?skillsRedirectUri=${window.location}`;
const oauthAuthenticator = `${authenticator}?skillsRedirectUri=${encodeURIComponent(window.location.href)}`;
log.info(`SkillsClient::SkillService::unable to get oAuth token, navigating to [${oauthAuthenticator}]`);
this.assignWindowLocation(oauthAuthenticator);
resolve();
@@ -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()}`);
}
+17 -1
View File
@@ -45,7 +45,23 @@ describe('OAuth auto redirect tests', () => {
await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId, true);
expect(SkillsService.assignWindowLocation).toHaveBeenCalledTimes(1)
expect(SkillsService.assignWindowLocation).toHaveBeenCalledWith(`${authEndpoint}?skillsRedirectUri=http://localhost/`)
expect(SkillsService.assignWindowLocation).toHaveBeenCalledWith(`${authEndpoint}?skillsRedirectUri=${encodeURIComponent('http://localhost/')}`)
});
it('oauth redirect encodes the current location so it cannot add query parameters', async () => {
const oauthTokenEndpoint = `${mockServiceUrl}/api/projects/${mockProjectId}/token`;
mock.get(oauthTokenEndpoint, (req, res) => res.status(401));
window.history.pushState({}, '', '/page?a=1&skillsRedirectUri=http://evil.example.com/');
const injected = window.location.href;
await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId, true);
const [redirectedTo] = SkillsService.assignWindowLocation.mock.calls[0];
expect(redirectedTo).toBe(`${authEndpoint}?skillsRedirectUri=${encodeURIComponent(injected)}`);
// the injected separators must not survive as separators
expect(redirectedTo.split('?')).toHaveLength(2);
expect(redirectedTo).not.toContain('&skillsRedirectUri=');
});
it('oauth does not auto redirect when oauthRedirect=false', async () => {
@@ -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', () => {