diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 000000000..21acd383e --- /dev/null +++ b/.github/workflows/jest.yml @@ -0,0 +1,28 @@ +# Runs jest based unit tests for the binderhub-client JS package +name: eslint + +on: + pull_request: + paths: + - "js/**" + push: + paths: + - "js/**" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + - "update-*" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - run: | + cd js/packages/binderhub-client + npm install + + - run: | + npm run jest diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 57eebf393..30398630c 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -167,7 +167,8 @@ function build(providerSpec, log, fitAddon, path, pathType) { $('.on-build').removeClass('hidden'); const buildToken = $("#build-token").data('token'); - const image = new BinderRepository(providerSpec, BASE_URL, buildToken); + const buildEndpointUrl = new URL("build", new URL(BASE_URL, window.location.origin)); + const image = new BinderRepository(providerSpec, buildEndpointUrl, buildToken); image.onStateChange('*', function(oldState, newState, data) { if (data.message !== undefined) { diff --git a/js/packages/binderhub-client/babel.config.json b/js/packages/binderhub-client/babel.config.json new file mode 120000 index 000000000..8ecc8f067 --- /dev/null +++ b/js/packages/binderhub-client/babel.config.json @@ -0,0 +1 @@ +../../../babel.config.json \ No newline at end of file diff --git a/js/packages/binderhub-client/lib/index.js b/js/packages/binderhub-client/lib/index.js index 19d239304..4a6f03148 100644 --- a/js/packages/binderhub-client/lib/index.js +++ b/js/packages/binderhub-client/lib/index.js @@ -10,13 +10,28 @@ export class BinderRepository { /** * * @param {string} providerSpec Spec of the form // to pass to the binderhub API. - * @param {string} baseUrl Base URL (including the trailing slash) of the binderhub installation to talk to. - * @param {string} buildToken Optional JWT based build token if this binderhub installation requires using build tokesn + * @param {URL} buildEndpointUrl API URL of the build endpoint to talk to + * @param {string} buildToken Optional JWT based build token if this binderhub installation requires using build tokens */ - constructor(providerSpec, baseUrl, buildToken) { + constructor(providerSpec, buildEndpointUrl, buildToken) { this.providerSpec = providerSpec; - this.baseUrl = baseUrl; - this.buildToken = buildToken; + // Make sure that buildEndpointUrl is a real URL - this ensures hostname is properly set + if(!(buildEndpointUrl instanceof URL)) { + throw new TypeError(`buildEndpointUrl must be a URL object, got ${buildEndpointUrl} instead`); + } + // We make a copy here so we don't modify the passed in URL object + this.buildEndpointUrl = new URL(buildEndpointUrl); + // The binderHub API is path based, so the buildEndpointUrl must have a trailing slash. We add + // it if it is not passed in here to us. + if(!this.buildEndpointUrl.pathname.endsWith('/')) { + this.buildEndpointUrl.pathname += "/"; + } + + // The actual URL we'll make a request to build this particular providerSpec + this.buildUrl = new URL(this.providerSpec, this.buildEndpointUrl); + if(buildToken) { + this.buildUrl.searchParams.append("build_token", buildToken); + } this.callbacks = {}; this.state = null; } @@ -25,12 +40,7 @@ export class BinderRepository { * Call the BinderHub API */ fetch() { - let apiUrl = this.baseUrl + "build/" + this.providerSpec; - if (this.buildToken) { - apiUrl = apiUrl + `?build_token=${this.buildToken}`; - } - - this.eventSource = new EventSource(apiUrl); + this.eventSource = new EventSource(this.buildUrl); this.eventSource.onerror = (err) => { console.error("Failed to construct event stream", err); this._changeState("failed", { diff --git a/js/packages/binderhub-client/lib/index.test.js b/js/packages/binderhub-client/lib/index.test.js new file mode 100644 index 000000000..a0b43f554 --- /dev/null +++ b/js/packages/binderhub-client/lib/index.test.js @@ -0,0 +1,31 @@ +import { BinderRepository } from "."; + +test('Passed in URL object is not modified', () => { + const buildEndpointUrl = new URL("https://test-binder.org/build") + const br = new BinderRepository('gh/test/test', buildEndpointUrl, "token"); + expect(br.buildEndpointUrl.toString()).not.toEqual(buildEndpointUrl.toString()) +}); + +test('Invalid URL errors out', () => { + expect(() => { + new BinderRepository('gh/test/test', '/build', "token"); + }).toThrow(TypeError); +}); + +test('Trailing slash added if needed', () => { + const buildEndpointUrl = new URL("https://test-binder.org/build") + const br = new BinderRepository('gh/test/test', buildEndpointUrl); + expect(br.buildEndpointUrl.toString()).toEqual("https://test-binder.org/build/") +}); + +test('Build URL correctly built from Build Endpoint', () => { + const buildEndpointUrl = new URL("https://test-binder.org/build") + const br = new BinderRepository('gh/test/test', buildEndpointUrl); + expect(br.buildUrl.toString()).toEqual("https://test-binder.org/build/gh/test/test"); +}); + +test('Build URL correctly built from Build Endpoint when used with token', () => { + const buildEndpointUrl = new URL("https://test-binder.org/build") + const br = new BinderRepository('gh/test/test', buildEndpointUrl, 'token'); + expect(br.buildUrl.toString()).toEqual("https://test-binder.org/build/gh/test/test?build_token=token"); +}); diff --git a/js/packages/binderhub-client/package.json b/js/packages/binderhub-client/package.json index e8c60b232..6d959a91d 100644 --- a/js/packages/binderhub-client/package.json +++ b/js/packages/binderhub-client/package.json @@ -15,5 +15,16 @@ "homepage": "https://github.com/jupyterhub/binderhub#readme", "dependencies": { "event-source-polyfill": "^1.0.31" + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "babel-jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + }, + "scripts": { + "jest": "jest" + }, + "jest": { + "testEnvironment": "jsdom" } }