test(coverage): 100% test coverage & refactor to vitest and msw (#6)
* chore(refactor): tests use vitest and msw * fix(directory): Ensure filenames conform to eslint requirements * chore(yarn): Specify node engine version * fix(yarn): use the proper nodelinker * fix(GitHub workflow): yarn install with caching * docs(README): update to match changes --------- Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
This commit is contained in:
@@ -11,6 +11,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.4.0
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
- name: Yarn Install
|
||||
run: yarn install
|
||||
- uses: actions/cache/save@v3
|
||||
id: cache-save
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: |
|
||||
yarn run run:mockserver & # run mockserver in background, will be killed when test finishes
|
||||
- uses: ./
|
||||
|
||||
@@ -39,3 +39,6 @@ node_modules
|
||||
lib
|
||||
|
||||
coverage/
|
||||
|
||||
# asdf config file
|
||||
.tool-versions
|
||||
|
||||
Generated
-2042
File diff suppressed because it is too large
Load Diff
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Test with YARN",
|
||||
"request": "launch",
|
||||
"console": "integratedTerminal",
|
||||
"runtimeArgs": ["test"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node"
|
||||
// "envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"kubernetes://schema/speckle.systems/v1alpha1@specklefunction": "file:///Users/iainsproat/workspace/speckle-automate-github-action/examples/minimal/specklefunction.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.0.cjs
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
This repository contains the source code for the Speckle Automate GitHub Action. It is a GitHub Action that builds a Speckle Automate Function from your source code.
|
||||
This repository contains the source code for the Speckle Automate GitHub Action. It is a GitHub Action that publishes a Speckle Automate Function to Speckle Automate your source code.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -26,27 +26,48 @@ If you believe your token has been compromised, please revoke it immediately on
|
||||
|
||||
Please note that this is not a Speckle Account token, but a **Speckle Automate API** token. You can create one by logging into the [Speckle Automate Server](https://automate.speckle.xyz) and going to the [API Tokens](https://automate.speckle.xyz/tokens) page.
|
||||
|
||||
`speckle_function_path`
|
||||
|
||||
The path to the Speckle Automate Function to publish. This path is relative to the root of the repository. If you provide a path to a directory, your Speckle Automate Function must be in a file named `specklefunction.yaml` within that directory.
|
||||
|
||||
`function_id`
|
||||
|
||||
*Optional.* If you have already registered a Speckle Function, you can use the ID of that Speckle Function to ensure that any changes are associated with it.
|
||||
If you do not provide a Function Id, we will attempt to determine the Function ID based on the GitHub server, GitHub repository, Reference (branch), and the Speckle Function Path.
|
||||
|
||||
Providing a Speckle Function ID allows you to change one of those values, and update the original Function instead of creating a new one.
|
||||
|
||||
Your Speckle Token must have write permissions for the Speckle Function with this ID, otherwise the publish will fail.
|
||||
|
||||
### Outputs
|
||||
|
||||
#### `function_id`
|
||||
|
||||
The unique ID of the published function.
|
||||
The unique ID of the published function. This will be the same as the `function_id` input if it was provided.
|
||||
|
||||
#### `version_id`
|
||||
|
||||
The unique ID of this version of the published function.
|
||||
|
||||
#### `image_name`
|
||||
|
||||
The name of the Docker image that you now need to build and push. Your Speckle Automate Token has been granted write permissions for publishing this image to the Speckle Automate Server.
|
||||
|
||||
### Example usage
|
||||
|
||||
Speckle Automate GitHub Action will register a Speckle Function with Speckle Automate. This is a necessary, but not sufficient, step in publishing your Speckle Function. You must also build and push the Docker image that contains your Speckle Function.
|
||||
|
||||
#### Publish a function to automate.speckle.xyz
|
||||
|
||||
```yaml
|
||||
uses: actions/speckle-automate-github-action@0.1.0
|
||||
with:
|
||||
# speckle_server_url is optional and defaults to https://automate.speckle.xyz
|
||||
# speckle_server_url: https://automate.speckle.xyz
|
||||
# The speckle_token is a secret and must be stored in GitHub as an encrypted secret
|
||||
# https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow
|
||||
speckle_token: ${{ secrets.speckle_token }}
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
# speckle_function_path is optional and defaults to ./specklefunction.yaml
|
||||
# function_id is optional and will be auto-generated if not provided
|
||||
```
|
||||
|
||||
#### Publish a function to a self-hosted server
|
||||
@@ -57,7 +78,70 @@ with:
|
||||
# please update to the url of your self-hosted server
|
||||
speckle_server_url: https://example.org
|
||||
# https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow
|
||||
speckle_token: ${{ secrets.speckle_token }}
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
# speckle_function_path is optional and defaults to ./specklefunction.yaml
|
||||
# function_id is optional and will be auto-generated if not provided
|
||||
```
|
||||
|
||||
### Example usage within an entire GitHub Actions Workflow
|
||||
|
||||
#### Publish a Speckle Function, and build the Docker Image using Docker
|
||||
|
||||
Docker is the original, and still one of the most popular, ways to build and publish Docker images. It does require that you provide a [Dockerfile](https://docs.docker.com/engine/reference/builder/), which includes instructions to Docker for building your image.
|
||||
|
||||
Find out more about Docker and Dockerfiles by following Docker's [Get Started Guide](https://docs.docker.com/get-started/).
|
||||
|
||||
You can learn more about Docker's GitHub Action from their documentation in the [GitHub Actions Marketplace](https://github.com/marketplace/actions/build-and-push-docker-images).
|
||||
|
||||
```yaml
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
# Checkout the code
|
||||
# Docker's GitHub Action does not require this step
|
||||
# but Speckle Automate does
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
id: speckle
|
||||
name: Register Speckle Function
|
||||
uses: actions/speckle-automate-github-action@0.1.0
|
||||
with:
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Docker needs to login to Speckle Automate
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.SPECKLE_USERNAME }}
|
||||
password: ${{ secrets.SPECKLE_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
# ## file is optional and defaults to {context}/Dockerfile
|
||||
# file: ./Dockerfile
|
||||
# ## context is optional and defaults to the root directory '.' of the repository
|
||||
# context: .
|
||||
# ## platforms is optional and defaults to linux/amd64
|
||||
# ## platforms must match the platforms that you have registered with Speckle Automate, which also defaults to linux/amd64.
|
||||
# platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.speckle.outputs.image_name }}
|
||||
```
|
||||
|
||||
## Developing & Debugging
|
||||
@@ -73,32 +157,8 @@ with:
|
||||
#### Building
|
||||
|
||||
1. Clone this repository
|
||||
1. Run `yarn install` to install dependencies
|
||||
1. Run `yarn build`
|
||||
|
||||
#### Running built image
|
||||
|
||||
1. Set the Speckle Server URL and Speckle Token environment variables, and run the image:
|
||||
|
||||
```bash
|
||||
export SPECKLE_SERVER_URL="https://automate.speckle.xyz" # (or your self-hosted server, which may be `localhost:3000` if running a development server)
|
||||
export SPECKLE_TOKEN="ABCD1234" #(replace with your token)
|
||||
docker run --rm \
|
||||
-e SPECKLE_SERVER_URL="${SPECKLE_SERVER_URL}" \
|
||||
-e SPECKLE_TOKEN="${SPECKLE_TOKEN}" \
|
||||
-e SPECKLE_FUNCTION_PATH="./examples/basic" \
|
||||
-e GITHUB_WORKSPACE="/home/runner/work/specklesystems/speckle-automate-github-action" \
|
||||
-e GITHUB_OUTPUT="/output/github_output" \
|
||||
-v "$(pwd):/home/runner/work/specklesystems/speckle-automate-github-action" \
|
||||
-v "/tmp:/output" \
|
||||
speckle/speckle-automate-github-action:local
|
||||
```
|
||||
|
||||
1. Inspect the output at `/tmp/github_output`:
|
||||
|
||||
```bash
|
||||
cat /tmp/github_output
|
||||
```
|
||||
1. Run `yarn install` to install dependencies.
|
||||
1. Run `yarn run all` to validate, build, and test the project.
|
||||
|
||||
### Developing
|
||||
|
||||
@@ -115,22 +175,22 @@ with:
|
||||
|
||||
#### Testing
|
||||
|
||||
1. Run unit tests:
|
||||
1. Run unit tests with coverage:
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
1. Run integration tests:
|
||||
1. Run unit tests and watch for changes while developing:
|
||||
|
||||
```bash
|
||||
yarn test:e2e
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
#### Linting
|
||||
|
||||
1. Run `yarn pre-commit` to run all pre-commit hooks.
|
||||
1. Address all linting errors prior to committing changes.
|
||||
1. You must address all linting errors prior to committing changes. The CI will fail if there are any linting errors, and you will be unable to merge your PR.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
export const id = 37;
|
||||
export const ids = [37];
|
||||
export const modules = {
|
||||
|
||||
/***/ 4037:
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "toFormData": () => (/* binding */ toFormData)
|
||||
/* harmony export */ });
|
||||
/* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2777);
|
||||
/* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010);
|
||||
|
||||
|
||||
|
||||
let s = 0;
|
||||
const S = {
|
||||
START_BOUNDARY: s++,
|
||||
HEADER_FIELD_START: s++,
|
||||
HEADER_FIELD: s++,
|
||||
HEADER_VALUE_START: s++,
|
||||
HEADER_VALUE: s++,
|
||||
HEADER_VALUE_ALMOST_DONE: s++,
|
||||
HEADERS_ALMOST_DONE: s++,
|
||||
PART_DATA_START: s++,
|
||||
PART_DATA: s++,
|
||||
END: s++
|
||||
};
|
||||
|
||||
let f = 1;
|
||||
const F = {
|
||||
PART_BOUNDARY: f,
|
||||
LAST_BOUNDARY: f *= 2
|
||||
};
|
||||
|
||||
const LF = 10;
|
||||
const CR = 13;
|
||||
const SPACE = 32;
|
||||
const HYPHEN = 45;
|
||||
const COLON = 58;
|
||||
const A = 97;
|
||||
const Z = 122;
|
||||
|
||||
const lower = c => c | 0x20;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
class MultipartParser {
|
||||
/**
|
||||
* @param {string} boundary
|
||||
*/
|
||||
constructor(boundary) {
|
||||
this.index = 0;
|
||||
this.flags = 0;
|
||||
|
||||
this.onHeaderEnd = noop;
|
||||
this.onHeaderField = noop;
|
||||
this.onHeadersEnd = noop;
|
||||
this.onHeaderValue = noop;
|
||||
this.onPartBegin = noop;
|
||||
this.onPartData = noop;
|
||||
this.onPartEnd = noop;
|
||||
|
||||
this.boundaryChars = {};
|
||||
|
||||
boundary = '\r\n--' + boundary;
|
||||
const ui8a = new Uint8Array(boundary.length);
|
||||
for (let i = 0; i < boundary.length; i++) {
|
||||
ui8a[i] = boundary.charCodeAt(i);
|
||||
this.boundaryChars[ui8a[i]] = true;
|
||||
}
|
||||
|
||||
this.boundary = ui8a;
|
||||
this.lookbehind = new Uint8Array(this.boundary.length + 8);
|
||||
this.state = S.START_BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
*/
|
||||
write(data) {
|
||||
let i = 0;
|
||||
const length_ = data.length;
|
||||
let previousIndex = this.index;
|
||||
let {lookbehind, boundary, boundaryChars, index, state, flags} = this;
|
||||
const boundaryLength = this.boundary.length;
|
||||
const boundaryEnd = boundaryLength - 1;
|
||||
const bufferLength = data.length;
|
||||
let c;
|
||||
let cl;
|
||||
|
||||
const mark = name => {
|
||||
this[name + 'Mark'] = i;
|
||||
};
|
||||
|
||||
const clear = name => {
|
||||
delete this[name + 'Mark'];
|
||||
};
|
||||
|
||||
const callback = (callbackSymbol, start, end, ui8a) => {
|
||||
if (start === undefined || start !== end) {
|
||||
this[callbackSymbol](ui8a && ui8a.subarray(start, end));
|
||||
}
|
||||
};
|
||||
|
||||
const dataCallback = (name, clear) => {
|
||||
const markSymbol = name + 'Mark';
|
||||
if (!(markSymbol in this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
callback(name, this[markSymbol], i, data);
|
||||
delete this[markSymbol];
|
||||
} else {
|
||||
callback(name, this[markSymbol], data.length, data);
|
||||
this[markSymbol] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
for (i = 0; i < length_; i++) {
|
||||
c = data[i];
|
||||
|
||||
switch (state) {
|
||||
case S.START_BOUNDARY:
|
||||
if (index === boundary.length - 2) {
|
||||
if (c === HYPHEN) {
|
||||
flags |= F.LAST_BOUNDARY;
|
||||
} else if (c !== CR) {
|
||||
return;
|
||||
}
|
||||
|
||||
index++;
|
||||
break;
|
||||
} else if (index - 1 === boundary.length - 2) {
|
||||
if (flags & F.LAST_BOUNDARY && c === HYPHEN) {
|
||||
state = S.END;
|
||||
flags = 0;
|
||||
} else if (!(flags & F.LAST_BOUNDARY) && c === LF) {
|
||||
index = 0;
|
||||
callback('onPartBegin');
|
||||
state = S.HEADER_FIELD_START;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (c !== boundary[index + 2]) {
|
||||
index = -2;
|
||||
}
|
||||
|
||||
if (c === boundary[index + 2]) {
|
||||
index++;
|
||||
}
|
||||
|
||||
break;
|
||||
case S.HEADER_FIELD_START:
|
||||
state = S.HEADER_FIELD;
|
||||
mark('onHeaderField');
|
||||
index = 0;
|
||||
// falls through
|
||||
case S.HEADER_FIELD:
|
||||
if (c === CR) {
|
||||
clear('onHeaderField');
|
||||
state = S.HEADERS_ALMOST_DONE;
|
||||
break;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (c === HYPHEN) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (c === COLON) {
|
||||
if (index === 1) {
|
||||
// empty header field
|
||||
return;
|
||||
}
|
||||
|
||||
dataCallback('onHeaderField', true);
|
||||
state = S.HEADER_VALUE_START;
|
||||
break;
|
||||
}
|
||||
|
||||
cl = lower(c);
|
||||
if (cl < A || cl > Z) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case S.HEADER_VALUE_START:
|
||||
if (c === SPACE) {
|
||||
break;
|
||||
}
|
||||
|
||||
mark('onHeaderValue');
|
||||
state = S.HEADER_VALUE;
|
||||
// falls through
|
||||
case S.HEADER_VALUE:
|
||||
if (c === CR) {
|
||||
dataCallback('onHeaderValue', true);
|
||||
callback('onHeaderEnd');
|
||||
state = S.HEADER_VALUE_ALMOST_DONE;
|
||||
}
|
||||
|
||||
break;
|
||||
case S.HEADER_VALUE_ALMOST_DONE:
|
||||
if (c !== LF) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = S.HEADER_FIELD_START;
|
||||
break;
|
||||
case S.HEADERS_ALMOST_DONE:
|
||||
if (c !== LF) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback('onHeadersEnd');
|
||||
state = S.PART_DATA_START;
|
||||
break;
|
||||
case S.PART_DATA_START:
|
||||
state = S.PART_DATA;
|
||||
mark('onPartData');
|
||||
// falls through
|
||||
case S.PART_DATA:
|
||||
previousIndex = index;
|
||||
|
||||
if (index === 0) {
|
||||
// boyer-moore derrived algorithm to safely skip non-boundary data
|
||||
i += boundaryEnd;
|
||||
while (i < bufferLength && !(data[i] in boundaryChars)) {
|
||||
i += boundaryLength;
|
||||
}
|
||||
|
||||
i -= boundaryEnd;
|
||||
c = data[i];
|
||||
}
|
||||
|
||||
if (index < boundary.length) {
|
||||
if (boundary[index] === c) {
|
||||
if (index === 0) {
|
||||
dataCallback('onPartData', true);
|
||||
}
|
||||
|
||||
index++;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
} else if (index === boundary.length) {
|
||||
index++;
|
||||
if (c === CR) {
|
||||
// CR = part boundary
|
||||
flags |= F.PART_BOUNDARY;
|
||||
} else if (c === HYPHEN) {
|
||||
// HYPHEN = end boundary
|
||||
flags |= F.LAST_BOUNDARY;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
} else if (index - 1 === boundary.length) {
|
||||
if (flags & F.PART_BOUNDARY) {
|
||||
index = 0;
|
||||
if (c === LF) {
|
||||
// unset the PART_BOUNDARY flag
|
||||
flags &= ~F.PART_BOUNDARY;
|
||||
callback('onPartEnd');
|
||||
callback('onPartBegin');
|
||||
state = S.HEADER_FIELD_START;
|
||||
break;
|
||||
}
|
||||
} else if (flags & F.LAST_BOUNDARY) {
|
||||
if (c === HYPHEN) {
|
||||
callback('onPartEnd');
|
||||
state = S.END;
|
||||
flags = 0;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
// when matching a possible boundary, keep a lookbehind reference
|
||||
// in case it turns out to be a false lead
|
||||
lookbehind[index - 1] = c;
|
||||
} else if (previousIndex > 0) {
|
||||
// if our boundary turned out to be rubbish, the captured lookbehind
|
||||
// belongs to partData
|
||||
const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength);
|
||||
callback('onPartData', 0, previousIndex, _lookbehind);
|
||||
previousIndex = 0;
|
||||
mark('onPartData');
|
||||
|
||||
// reconsider the current character even so it interrupted the sequence
|
||||
// it could be the beginning of a new sequence
|
||||
i--;
|
||||
}
|
||||
|
||||
break;
|
||||
case S.END:
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected state entered: ${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
dataCallback('onHeaderField');
|
||||
dataCallback('onHeaderValue');
|
||||
dataCallback('onPartData');
|
||||
|
||||
// Update properties for the next call
|
||||
this.index = index;
|
||||
this.state = state;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
end() {
|
||||
if ((this.state === S.HEADER_FIELD_START && this.index === 0) ||
|
||||
(this.state === S.PART_DATA && this.index === this.boundary.length)) {
|
||||
this.onPartEnd();
|
||||
} else if (this.state !== S.END) {
|
||||
throw new Error('MultipartParser.end(): stream ended unexpectedly');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _fileName(headerValue) {
|
||||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
|
||||
const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = m[2] || m[3] || '';
|
||||
let filename = match.slice(match.lastIndexOf('\\') + 1);
|
||||
filename = filename.replace(/%22/g, '"');
|
||||
filename = filename.replace(/&#(\d{4});/g, (m, code) => {
|
||||
return String.fromCharCode(code);
|
||||
});
|
||||
return filename;
|
||||
}
|
||||
|
||||
async function toFormData(Body, ct) {
|
||||
if (!/multipart/i.test(ct)) {
|
||||
throw new TypeError('Failed to fetch');
|
||||
}
|
||||
|
||||
const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
||||
|
||||
if (!m) {
|
||||
throw new TypeError('no or bad content-type header, no multipart boundary');
|
||||
}
|
||||
|
||||
const parser = new MultipartParser(m[1] || m[2]);
|
||||
|
||||
let headerField;
|
||||
let headerValue;
|
||||
let entryValue;
|
||||
let entryName;
|
||||
let contentType;
|
||||
let filename;
|
||||
const entryChunks = [];
|
||||
const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct();
|
||||
|
||||
const onPartData = ui8a => {
|
||||
entryValue += decoder.decode(ui8a, {stream: true});
|
||||
};
|
||||
|
||||
const appendToFile = ui8a => {
|
||||
entryChunks.push(ui8a);
|
||||
};
|
||||
|
||||
const appendFileToFormData = () => {
|
||||
const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType});
|
||||
formData.append(entryName, file);
|
||||
};
|
||||
|
||||
const appendEntryToFormData = () => {
|
||||
formData.append(entryName, entryValue);
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
decoder.decode();
|
||||
|
||||
parser.onPartBegin = function () {
|
||||
parser.onPartData = onPartData;
|
||||
parser.onPartEnd = appendEntryToFormData;
|
||||
|
||||
headerField = '';
|
||||
headerValue = '';
|
||||
entryValue = '';
|
||||
entryName = '';
|
||||
contentType = '';
|
||||
filename = null;
|
||||
entryChunks.length = 0;
|
||||
};
|
||||
|
||||
parser.onHeaderField = function (ui8a) {
|
||||
headerField += decoder.decode(ui8a, {stream: true});
|
||||
};
|
||||
|
||||
parser.onHeaderValue = function (ui8a) {
|
||||
headerValue += decoder.decode(ui8a, {stream: true});
|
||||
};
|
||||
|
||||
parser.onHeaderEnd = function () {
|
||||
headerValue += decoder.decode();
|
||||
headerField = headerField.toLowerCase();
|
||||
|
||||
if (headerField === 'content-disposition') {
|
||||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
|
||||
const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);
|
||||
|
||||
if (m) {
|
||||
entryName = m[2] || m[3] || '';
|
||||
}
|
||||
|
||||
filename = _fileName(headerValue);
|
||||
|
||||
if (filename) {
|
||||
parser.onPartData = appendToFile;
|
||||
parser.onPartEnd = appendFileToFormData;
|
||||
}
|
||||
} else if (headerField === 'content-type') {
|
||||
contentType = headerValue;
|
||||
}
|
||||
|
||||
headerValue = '';
|
||||
headerField = '';
|
||||
};
|
||||
|
||||
for await (const chunk of Body) {
|
||||
parser.write(chunk);
|
||||
}
|
||||
|
||||
parser.end();
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
|
||||
//# sourceMappingURL=37.index.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+378
-376
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
+28552
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+1087
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+2900
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+13
-13
@@ -1,7 +1,7 @@
|
||||
import './sourcemap-register.cjs';import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "module";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 3748:
|
||||
/***/ 2182:
|
||||
/***/ ((module, exports, __nccwpck_require__) => {
|
||||
|
||||
|
||||
@@ -389,7 +389,7 @@ var __webpack_exports__ = {};
|
||||
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
|
||||
(() => {
|
||||
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/ufo-npm-1.1.1-5caba43c85-6bd210ed93.zip/node_modules/ufo/dist/index.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/ufo/dist/index.mjs
|
||||
const n = /[^\0-\x7E]/;
|
||||
const t = /[\x2E\u3002\uFF0E\uFF61]/g;
|
||||
const o = {
|
||||
@@ -861,7 +861,7 @@ function stringifyParsedURL(parsed) {
|
||||
|
||||
|
||||
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/radix3-npm-1.0.0-a7f8c376b3-91086baa4d.zip/node_modules/radix3/dist/index.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/radix3/dist/index.mjs
|
||||
const NODE_TYPES = {
|
||||
NORMAL: 0,
|
||||
WILDCARD: 1,
|
||||
@@ -1073,7 +1073,7 @@ function _routerNodeToTable(initialPath, initialNode) {
|
||||
|
||||
|
||||
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/defu-npm-6.1.2-65c0503295-2ec0ff8414.zip/node_modules/defu/dist/defu.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/defu/dist/defu.mjs
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === "object";
|
||||
}
|
||||
@@ -1130,7 +1130,7 @@ const defuArrayFn = createDefu((object, key, currentValue) => {
|
||||
|
||||
|
||||
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/h3-npm-1.6.2-8f413e9ff2-3a876f414d.zip/node_modules/h3/dist/index.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/h3/dist/index.mjs
|
||||
|
||||
|
||||
|
||||
@@ -2582,7 +2582,7 @@ const external_node_os_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import
|
||||
;// CONCATENATED MODULE: external "tty"
|
||||
const external_tty_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("tty");
|
||||
var external_tty_namespaceObject_0 = /*#__PURE__*/__nccwpck_require__.t(external_tty_namespaceObject, 2);
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/colorette-npm-2.0.19-f73dfe6a4e-888cf5493f.zip/node_modules/colorette/index.js
|
||||
;// CONCATENATED MODULE: ./node_modules/colorette/index.js
|
||||
|
||||
|
||||
const {
|
||||
@@ -2736,7 +2736,7 @@ const {
|
||||
|
||||
;// CONCATENATED MODULE: external "node:net"
|
||||
const external_node_net_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:net");
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/get-port-please-npm-3.0.1-b00ea347f9-a5de771314.zip/node_modules/get-port-please/dist/index.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/get-port-please/dist/index.mjs
|
||||
|
||||
|
||||
|
||||
@@ -2989,13 +2989,13 @@ async function findPort(ports, host, _verbose = false, _random = true) {
|
||||
|
||||
|
||||
|
||||
// EXTERNAL MODULE: ./.yarn/cache/http-shutdown-npm-1.2.2-e4fdf6986c-5dccd94f4f.zip/node_modules/http-shutdown/index.js
|
||||
var http_shutdown = __nccwpck_require__(3748);
|
||||
// EXTERNAL MODULE: ./node_modules/http-shutdown/index.js
|
||||
var http_shutdown = __nccwpck_require__(2182);
|
||||
;// CONCATENATED MODULE: external "node:child_process"
|
||||
const external_node_child_process_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:child_process");
|
||||
// EXTERNAL MODULE: external "node:path"
|
||||
var external_node_path_ = __nccwpck_require__(9411);
|
||||
;// CONCATENATED MODULE: ./.yarn/cache/listhen-npm-1.0.4-9812273d67-9ab54b9d23.zip/node_modules/listhen/dist/index.mjs
|
||||
;// CONCATENATED MODULE: ./node_modules/listhen/dist/index.mjs
|
||||
|
||||
|
||||
|
||||
@@ -3144,7 +3144,7 @@ const baseOpen = async (options) => {
|
||||
try {
|
||||
(0,external_node_fs_namespaceObject.writeFileSync)(
|
||||
(0,external_node_path_.join)(external_node_os_namespaceObject.tmpdir(), "xdg-open"),
|
||||
await __nccwpck_require__.e(/* import() */ 230).then(__nccwpck_require__.bind(__nccwpck_require__, 4230)).then((r) => r.xdgOpenScript()),
|
||||
await __nccwpck_require__.e(/* import() */ 358).then(__nccwpck_require__.bind(__nccwpck_require__, 4358)).then((r) => r.xdgOpenScript()),
|
||||
"utf8"
|
||||
);
|
||||
(0,external_node_fs_namespaceObject.chmodSync)(
|
||||
@@ -3414,7 +3414,7 @@ async function listen(handle, options_ = {}) {
|
||||
return (0,external_node_util_.promisify)(server.shutdown)();
|
||||
};
|
||||
if (options_.clipboard) {
|
||||
const clipboardy = await __nccwpck_require__.e(/* import() */ 951).then(__nccwpck_require__.bind(__nccwpck_require__, 3951)).then((r) => r.default || r);
|
||||
const clipboardy = await __nccwpck_require__.e(/* import() */ 902).then(__nccwpck_require__.bind(__nccwpck_require__, 9902)).then((r) => r.default || r);
|
||||
await clipboardy.write(getURL()).catch(() => {
|
||||
options_.clipboard = false;
|
||||
});
|
||||
@@ -3473,7 +3473,7 @@ async function resolveCert(options, host) {
|
||||
cert: await r(options.cert)
|
||||
};
|
||||
}
|
||||
const { generateCA, generateSSLCert } = await __nccwpck_require__.e(/* import() */ 728).then(__nccwpck_require__.bind(__nccwpck_require__, 1728));
|
||||
const { generateCA, generateSSLCert } = await __nccwpck_require__.e(/* import() */ 32).then(__nccwpck_require__.bind(__nccwpck_require__, 6032));
|
||||
const ca = await generateCA();
|
||||
const cert = await generateSSLCert({
|
||||
caCert: ca.cert,
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+8
-9
@@ -11,15 +11,18 @@
|
||||
"all": "yarn run build && yarn run prettier:check && yarn run lint && yarn run package && yarn run package:mockserver && yarn run test",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"build:image": "docker build -t speckle/speckle-automate-github-action:local .",
|
||||
"coverage": "vitest run --coverage --run",
|
||||
"lint": "eslint **/*.ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"package": "ncc build --target es2020 --source-map --license licenses.txt -o dist/action src/main.ts",
|
||||
"package:mockserver": "ncc build --target es2020 --source-map --license licenses.txt -o dist/testing/mockserver src/tests/mock-server.ts",
|
||||
"precommit": "pre-commit run --all-files",
|
||||
"prettier:check": "prettier --check '**/*.ts'",
|
||||
"prettier:fix": "prettier --write '**/*.ts'",
|
||||
"run:mockserver": "node dist/testing/mockserver/index.js",
|
||||
"test": "vitest --run"
|
||||
"test": "vitest --run --coverage",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
@@ -30,13 +33,10 @@
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"@types/sinon-chai": "^3.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"@vitest/coverage-c8": "^0.29.7",
|
||||
"chai": "^4.3.7",
|
||||
"@vitest/coverage-istanbul": "^0.29.7",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
@@ -51,11 +51,10 @@
|
||||
"h3": "^1.6.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"listhen": "^1.0.4",
|
||||
"msw": "^1.2.0",
|
||||
"pino": "^8.11.0",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"prettier": "^2.8.6",
|
||||
"sinon": "^15.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.2.1",
|
||||
"vitest": "^0.29.7"
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { rest } from 'msw'
|
||||
import { getMinimalSpeckleFunctionExample } from '../schema/specklefunction.spec.js'
|
||||
import httpClient from './client.js'
|
||||
import { getLogger } from '../tests/logger.js'
|
||||
import { SpeckleFunction } from '../schema/specklefunction.js'
|
||||
import { ValidationError } from 'zod-validation-error'
|
||||
|
||||
describe('client', () => {
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: 'error' })
|
||||
})
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
})
|
||||
describe('postManifest', () => {
|
||||
describe('valid input', () => {
|
||||
it('should respond with image name, function id, and version id', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://success.automate.speckle.example.org/api/v1/functions',
|
||||
async (req, res, ctx) => {
|
||||
expect(await req.json()).toStrictEqual({
|
||||
functionId: null,
|
||||
url: 'https://github.com/specklesystems/speckle-automate-examples',
|
||||
path: 'examples/minimal',
|
||||
ref: 'main',
|
||||
commitSha: '1234567890',
|
||||
manifest: getMinimalSpeckleFunctionExample()
|
||||
})
|
||||
expect(req.headers.get('Authorization')).to.equal('Bearer supersecret')
|
||||
const response = await res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
functionId: 'minimalfunctionid',
|
||||
versionId: 'minimalversionid',
|
||||
imageName: 'speckle/minimalfunctionid:minimalversionid'
|
||||
})
|
||||
)
|
||||
return response
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const test = httpClient.postManifest(
|
||||
'https://success.automate.speckle.example.org',
|
||||
'supersecret',
|
||||
{
|
||||
functionId: null,
|
||||
path: 'examples/minimal',
|
||||
url: 'https://github.com/specklesystems/speckle-automate-examples',
|
||||
ref: 'main',
|
||||
commitSha: '1234567890',
|
||||
manifest: getMinimalSpeckleFunctionExample() as SpeckleFunction
|
||||
},
|
||||
getLogger()
|
||||
)
|
||||
|
||||
await expect(test).resolves.toStrictEqual({
|
||||
functionId: 'minimalfunctionid',
|
||||
versionId: 'minimalversionid',
|
||||
imageName: 'speckle/minimalfunctionid:minimalversionid'
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('server responds with a 500 HTTP Status Code', () => {
|
||||
it('should throw an error', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://failure.automate.speckle.example.org/api/v1/functions',
|
||||
async (req, res, ctx) => {
|
||||
const response = await res(ctx.status(500))
|
||||
return response
|
||||
}
|
||||
)
|
||||
)
|
||||
await expect(
|
||||
httpClient.postManifest(
|
||||
'https://failure.automate.speckle.example.org',
|
||||
'sometoken',
|
||||
{
|
||||
functionId: null,
|
||||
path: '',
|
||||
url: '',
|
||||
ref: '',
|
||||
commitSha: '',
|
||||
manifest: getMinimalSpeckleFunctionExample() as SpeckleFunction
|
||||
},
|
||||
getLogger()
|
||||
)
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
describe('server responds with an unexpected response body', () => {
|
||||
it('should throw an error', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://unexpectedresponse.automate.speckle.example.org/api/v1/functions',
|
||||
async (req, res, ctx) => {
|
||||
const response = await res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
unexpected: 'unexpected'
|
||||
})
|
||||
)
|
||||
return response
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
await expect(
|
||||
httpClient.postManifest(
|
||||
'https://unexpectedresponse.automate.speckle.example.org',
|
||||
'sometoken',
|
||||
{
|
||||
functionId: null,
|
||||
path: '',
|
||||
url: '',
|
||||
ref: '',
|
||||
commitSha: '',
|
||||
manifest: getMinimalSpeckleFunctionExample() as SpeckleFunction
|
||||
},
|
||||
getLogger()
|
||||
)
|
||||
).rejects.toThrow(ValidationError)
|
||||
})
|
||||
})
|
||||
describe('invalid input', () => {
|
||||
describe('empty url', () => {
|
||||
it('should throw an error', async () => {
|
||||
expect(
|
||||
httpClient.postManifest(
|
||||
'',
|
||||
'supersecret',
|
||||
{
|
||||
functionId: null,
|
||||
path: 'examples/minimal',
|
||||
url: 'https://github.com/specklesystems/speckle-automate-examples',
|
||||
ref: 'main',
|
||||
commitSha: '1234567890',
|
||||
manifest: getMinimalSpeckleFunctionExample() as SpeckleFunction
|
||||
},
|
||||
getLogger()
|
||||
)
|
||||
).rejects.toThrow(Error)
|
||||
})
|
||||
})
|
||||
describe('empty token', () => {
|
||||
it('should throw an error', async () => {
|
||||
expect(
|
||||
httpClient.postManifest(
|
||||
'https://example.org',
|
||||
'',
|
||||
{
|
||||
functionId: null,
|
||||
path: 'examples/minimal',
|
||||
url: 'https://github.com/specklesystems/speckle-automate-examples',
|
||||
ref: 'main',
|
||||
commitSha: '1234567890',
|
||||
manifest: getMinimalSpeckleFunctionExample() as SpeckleFunction
|
||||
},
|
||||
getLogger()
|
||||
)
|
||||
).rejects.toThrow(Error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SpeckleFunctionPostRequestBody
|
||||
} from './schema.js'
|
||||
import { URL } from 'url'
|
||||
import { handleZodError } from '../utils/errors.js'
|
||||
import { handleZodError } from '../schema/errors.js'
|
||||
import { Logger } from '../logging/logger.js'
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
@@ -13,15 +13,14 @@ export default {
|
||||
url: string,
|
||||
token: string,
|
||||
body: SpeckleFunctionPostRequestBody,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
_fetch: typeof fetch = fetch
|
||||
): Promise<SpeckleFunctionPostResponseBody> => {
|
||||
if (!url) throw new Error('Speckle Server URL is required')
|
||||
if (!token) throw new Error('Speckle Token is required')
|
||||
if (!body) throw new Error('Speckle Function Post Request Body is required')
|
||||
if (!logger) throw new Error('Logger is required')
|
||||
|
||||
const endpointUrl = new URL('/api/v1/functions', url)
|
||||
const response = await fetch(endpointUrl.href, {
|
||||
const response = await _fetch(endpointUrl.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -31,7 +30,11 @@ export default {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to register Speckle Function: ${response.statusText}`)
|
||||
throw new Error(
|
||||
`Failed to register Speckle Function. Status Code: ${
|
||||
response.status
|
||||
}. Status Text: ${response.statusText}. Response Body: ${await response.text()}`
|
||||
) //FIXME use a more specific error type
|
||||
}
|
||||
|
||||
let responseBody: SpeckleFunctionPostResponseBody
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { SpeckleFunctionPostRequestBodySchema } from './schema.js'
|
||||
import { ZodError } from 'zod'
|
||||
import {
|
||||
getMinimalSpeckleFunctionExample,
|
||||
NonConformantSpeckleFunction
|
||||
} from '../schema/specklefunction.spec.js'
|
||||
|
||||
type NonConformanSpeckleFunctionPostRequestBody = {
|
||||
functionId: string | null | undefined
|
||||
url: string | undefined
|
||||
path: string | undefined
|
||||
ref: string | undefined
|
||||
commitSha: string | undefined
|
||||
manifest: NonConformantSpeckleFunction | undefined
|
||||
}
|
||||
|
||||
function getMinimumRequestBody(): NonConformanSpeckleFunctionPostRequestBody {
|
||||
return {
|
||||
functionId: null,
|
||||
url: 'https://example.org',
|
||||
path: '.',
|
||||
ref: 'main',
|
||||
commitSha: 'sha123',
|
||||
manifest: getMinimalSpeckleFunctionExample()
|
||||
}
|
||||
}
|
||||
|
||||
describe('schema', () => {
|
||||
let minimal: NonConformanSpeckleFunctionPostRequestBody
|
||||
|
||||
beforeEach(() => {
|
||||
minimal = getMinimumRequestBody()
|
||||
})
|
||||
|
||||
describe('Speckle Function Post Request Body Schema', () => {
|
||||
it('cannot be empty', async () => {
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse('')).toThrow(ZodError)
|
||||
})
|
||||
describe('functionId', () => {
|
||||
it('can be null', () => {
|
||||
minimal.functionId = null
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).not.toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
it('cannot be an empty string', () => {
|
||||
minimal.functionId = ''
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('url', () => {
|
||||
it('cannot be missing', () => {
|
||||
minimal.url = ''
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
it('cannot be a url with a username', () => {
|
||||
minimal.url = 'https://user@example.org'
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
it('cannot be a url with a password', () => {
|
||||
minimal.url = 'https://:password@example.org'
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
it('cannot have a url and password', () => {
|
||||
minimal.url = 'https://user:password@example.org'
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('path', () => {
|
||||
it('cannot be empty', () => {
|
||||
minimal.path = ''
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('ref', () => {
|
||||
it('cannot be empty', () => {
|
||||
minimal.ref = ''
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('commitSha', () => {
|
||||
it('cannot be empty', () => {
|
||||
minimal.commitSha = ''
|
||||
expect(() => SpeckleFunctionPostRequestBodySchema.parse(minimal)).toThrow(
|
||||
ZodError
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
import { SpeckleFunctionSchema } from '../schema/speckleFunction.js'
|
||||
import { SpeckleFunctionSchema } from '../schema/specklefunction.js'
|
||||
import { URL } from 'url'
|
||||
|
||||
export const SpeckleFunctionPostRequestBodySchema = z.object({
|
||||
functionId: z.string().optional(),
|
||||
functionId: z.union([z.string().nonempty(), z.null()]),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
@@ -29,6 +29,7 @@ export const SpeckleFunctionPostRequestBodySchema = z.object({
|
||||
),
|
||||
path: z
|
||||
.string()
|
||||
.nonempty()
|
||||
.refine(
|
||||
() => true, //TODO validate it is a path, and the path is not a directory traversal attack out of the source code directory (such as ../../etc/passwd). We can take much of the directory traversal code from build-instructions-step.
|
||||
{
|
||||
@@ -36,8 +37,8 @@ export const SpeckleFunctionPostRequestBodySchema = z.object({
|
||||
}
|
||||
)
|
||||
.default('.'),
|
||||
ref: z.string(),
|
||||
commitSha: z.string(),
|
||||
ref: z.string().nonempty(),
|
||||
commitSha: z.string().nonempty(),
|
||||
manifest: SpeckleFunctionSchema
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { promises as fsPromises } from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
export type FileSystem = {
|
||||
loadYaml: (filePath: string) => Promise<unknown>
|
||||
}
|
||||
|
||||
const fileUtil = {
|
||||
loadYaml: async (filePath: string) => {
|
||||
return yaml.load(await fsPromises.readFile(filePath, 'utf8'))
|
||||
@@ -0,0 +1,64 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getMinimalSpeckleFunctionExample } from '../schema/specklefunction.spec.js'
|
||||
import { getLogger } from '../tests/logger.js'
|
||||
import { findAndParseManifest } from './parser.js'
|
||||
import { ValidationError } from 'zod-validation-error'
|
||||
|
||||
describe('filesystem/parser', () => {
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
describe('No Yaml file', () => {
|
||||
it('should throw', async () => {
|
||||
expect(async () =>
|
||||
findAndParseManifest('doesNotExist', {
|
||||
logger: getLogger(),
|
||||
fileSystem: {
|
||||
loadYaml: async () => {
|
||||
throw new Error('File does not exist')
|
||||
}
|
||||
}
|
||||
})
|
||||
).rejects.toThrow(Error)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('Minimal yaml file', () => {
|
||||
it('should parse', async () => {
|
||||
const speckleFunction = await findAndParseManifest('examples/minimal', {
|
||||
logger: getLogger(),
|
||||
fileSystem: { loadYaml: async () => getMinimalSpeckleFunctionExample() }
|
||||
})
|
||||
expect(speckleFunction).toBeDefined()
|
||||
expect(speckleFunction.metadata.name).toBe('minimal')
|
||||
})
|
||||
describe('with filename in path', () => {
|
||||
it('should parse', async () => {
|
||||
const speckleFunction = await findAndParseManifest(
|
||||
'examples/minimal/specklefunction.yaml',
|
||||
{
|
||||
logger: getLogger(),
|
||||
fileSystem: {
|
||||
loadYaml: async () => getMinimalSpeckleFunctionExample()
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(speckleFunction).toBeDefined()
|
||||
expect(speckleFunction.metadata.name).toBe('minimal')
|
||||
})
|
||||
})
|
||||
describe('Invalid yaml file', () => {
|
||||
it('should throw', async () => {
|
||||
expect(async () =>
|
||||
findAndParseManifest('src/tests/data/invalid', {
|
||||
logger: getLogger(),
|
||||
fileSystem: {
|
||||
loadYaml: async () => {
|
||||
return 'invalid: yaml'
|
||||
}
|
||||
}
|
||||
})
|
||||
).rejects.toThrow(ValidationError)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FileSystem } from './files.js'
|
||||
import { SpeckleFunction, SpeckleFunctionSchema } from '../schema/specklefunction.js'
|
||||
import * as path from 'path'
|
||||
import { Logger } from '../logging/logger.js'
|
||||
import { handleZodError } from '../schema/errors.js'
|
||||
|
||||
export type ParserOptions = {
|
||||
logger: Logger
|
||||
fileSystem: FileSystem
|
||||
}
|
||||
|
||||
export async function findAndParseManifest(
|
||||
pathToSpeckleFunctionFile: string,
|
||||
opts: ParserOptions
|
||||
): Promise<SpeckleFunction> {
|
||||
if (!pathToSpeckleFunctionFile.toLocaleLowerCase().endsWith('specklefunction.yaml')) {
|
||||
pathToSpeckleFunctionFile = path.join(
|
||||
pathToSpeckleFunctionFile,
|
||||
'specklefunction.yaml'
|
||||
)
|
||||
}
|
||||
|
||||
const speckleFunctionRaw = await opts.fileSystem.loadYaml(pathToSpeckleFunctionFile)
|
||||
|
||||
let speckleFunction: SpeckleFunction
|
||||
try {
|
||||
speckleFunction = await SpeckleFunctionSchema.parseAsync(speckleFunctionRaw)
|
||||
} catch (err) {
|
||||
throw handleZodError(err, opts.logger)
|
||||
}
|
||||
return speckleFunction
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import { registerSpeckleFunction } from './speckleautomate.js'
|
||||
import { registerSpeckleFunction } from './registerspecklefunction.js'
|
||||
import fileUtil from './filesystem/files.js'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
@@ -8,17 +9,21 @@ async function run(): Promise<void> {
|
||||
core.setSecret(speckleTokenRaw)
|
||||
const speckleFunctionPathRaw = core.getInput('speckle_function_path')
|
||||
const speckleFunctionIdRaw = core.getInput('speckle_function_id')
|
||||
const gitServerUrl = process.env.GITHUB_SERVER_URL
|
||||
const gitRepository = process.env.GITHUB_REPOSITORY
|
||||
const gitRefRaw = process.env.GITHUB_REF_NAME
|
||||
const gitCommitShaRaw = process.env.GITHUB_SHA
|
||||
|
||||
const { imageName, functionId, versionId } = await registerSpeckleFunction({
|
||||
speckleServerUrl: speckleServerUrlRaw,
|
||||
speckleToken: speckleTokenRaw,
|
||||
speckleFunctionRepositoryUrl: `${gitServerUrl}/${gitRepository}.git`,
|
||||
speckleFunctionPath: speckleFunctionPathRaw,
|
||||
speckleFunctionId: speckleFunctionIdRaw,
|
||||
ref: gitRefRaw,
|
||||
commitsha: gitCommitShaRaw,
|
||||
logger: core
|
||||
logger: core,
|
||||
fileSystem: fileUtil
|
||||
})
|
||||
|
||||
core.setOutput('function_id', functionId)
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import client from './client/client.js'
|
||||
import { Logger } from './logging/logger.js'
|
||||
import fileUtil from './utils/files.js'
|
||||
import {
|
||||
SpeckleServerUrlSchema,
|
||||
SpeckleTokenSchema,
|
||||
SpeckleFunctionPathSchema,
|
||||
SpeckleFunctionIdSchema,
|
||||
GitRefSchema,
|
||||
GitCommitShaSchema
|
||||
GitCommitShaSchema,
|
||||
SpeckleFunctionRepositorySchema
|
||||
} from './schema/inputs.js'
|
||||
import { SpeckleFunction, SpeckleFunctionSchema } from './schema/speckleFunction.js'
|
||||
import * as path from 'path'
|
||||
import { handleZodError } from './utils/errors.js'
|
||||
|
||||
import { handleZodError } from './schema/errors.js'
|
||||
import { SpeckleFunctionPostRequestBody } from './client/schema.js'
|
||||
import { findAndParseManifest } from './filesystem/parser.js'
|
||||
import { FileSystem } from './filesystem/files.js'
|
||||
|
||||
type ProcessOptions = {
|
||||
speckleServerUrl: string | undefined
|
||||
speckleToken: string
|
||||
speckleFunctionRepositoryUrl: string
|
||||
ref: string | undefined
|
||||
commitsha: string | undefined
|
||||
speckleFunctionPath: string | undefined
|
||||
speckleFunctionId?: string | undefined
|
||||
logger: Logger
|
||||
fileSystem: FileSystem
|
||||
}
|
||||
|
||||
type ProcessResult = {
|
||||
@@ -35,6 +38,7 @@ export async function registerSpeckleFunction(
|
||||
): Promise<ProcessResult> {
|
||||
let speckleServerUrl: string
|
||||
let speckleToken: string
|
||||
let speckleFunctionRepositoryUrl: string
|
||||
let speckleFunctionPath: string
|
||||
let speckleFunctionId: string | undefined
|
||||
let gitRef: string
|
||||
@@ -42,6 +46,9 @@ export async function registerSpeckleFunction(
|
||||
try {
|
||||
speckleServerUrl = SpeckleServerUrlSchema.parse(opts.speckleServerUrl)
|
||||
speckleToken = SpeckleTokenSchema.parse(opts.speckleToken)
|
||||
speckleFunctionRepositoryUrl = SpeckleFunctionRepositorySchema.parse(
|
||||
opts.speckleFunctionRepositoryUrl
|
||||
)
|
||||
speckleFunctionPath = SpeckleFunctionPathSchema.parse(opts.speckleFunctionPath)
|
||||
speckleFunctionId = SpeckleFunctionIdSchema.parse(opts.speckleFunctionId)
|
||||
gitRef = GitRefSchema.parse(opts.ref)
|
||||
@@ -50,16 +57,19 @@ export async function registerSpeckleFunction(
|
||||
throw handleZodError(err, opts.logger)
|
||||
}
|
||||
|
||||
opts.logger.info(`Speckle Server URL: ${speckleServerUrl}`)
|
||||
opts.logger.info(`Speckle Server URL: '${speckleServerUrl}'`)
|
||||
//token is masked in the logs, so no need to print it here.
|
||||
opts.logger.info(`Speckle Function Path: ${speckleFunctionPath}`)
|
||||
opts.logger.info(`Speckle Function ID: ${speckleFunctionId}`)
|
||||
opts.logger.info(`Speckle Function Path: '${speckleFunctionPath}'`)
|
||||
opts.logger.info(`Speckle Function ID (optional): '${speckleFunctionId}'`)
|
||||
|
||||
const manifest = await findAndParseManifest(opts.logger, speckleFunctionPath)
|
||||
const manifest = await findAndParseManifest(speckleFunctionPath, {
|
||||
logger: opts.logger,
|
||||
fileSystem: opts.fileSystem
|
||||
})
|
||||
|
||||
const body: SpeckleFunctionPostRequestBody = {
|
||||
functionId: speckleFunctionId || undefined,
|
||||
url: speckleServerUrl,
|
||||
functionId: speckleFunctionId || null,
|
||||
url: speckleFunctionRepositoryUrl,
|
||||
path: speckleFunctionPath,
|
||||
ref: gitRef,
|
||||
commitSha: gitCommitSha,
|
||||
@@ -77,30 +87,3 @@ export async function registerSpeckleFunction(
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
async function findAndParseManifest(logger: Logger, pathToSpeckleFunctionFile: string) {
|
||||
if (!pathToSpeckleFunctionFile.toLocaleLowerCase().endsWith('specklefunction.yaml')) {
|
||||
pathToSpeckleFunctionFile = path.join(
|
||||
pathToSpeckleFunctionFile,
|
||||
'specklefunction.yaml'
|
||||
)
|
||||
}
|
||||
|
||||
let speckleFunctionRaw: unknown
|
||||
try {
|
||||
speckleFunctionRaw = await fileUtil.loadYaml(pathToSpeckleFunctionFile)
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
logger.error(err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
let speckleFunction: SpeckleFunction
|
||||
try {
|
||||
speckleFunction = await SpeckleFunctionSchema.parseAsync(speckleFunctionRaw)
|
||||
} catch (err) {
|
||||
throw handleZodError(err, logger)
|
||||
}
|
||||
return speckleFunction
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { handleZodError } from './errors.js'
|
||||
import { ZodError } from 'zod'
|
||||
import { ValidationError } from 'zod-validation-error'
|
||||
|
||||
describe('errors', () => {
|
||||
describe('with ZodError', () => {
|
||||
it('logs and throws', async () => {
|
||||
const errorFn = vi.fn()
|
||||
const logger = { error: errorFn, info: vi.fn(), warn: vi.fn(), debug: vi.fn() }
|
||||
expect(() => handleZodError(new ZodError([]), logger)).toThrow(ValidationError)
|
||||
expect(errorFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
describe('with unknown error', () => {
|
||||
it('throws', async () => {
|
||||
const errorFn = vi.fn()
|
||||
const logger = { error: errorFn, info: vi.fn(), warn: vi.fn(), debug: vi.fn() }
|
||||
expect(() => handleZodError(new Error('unknown'), logger)).toThrow(Error)
|
||||
expect(errorFn).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,46 +1,39 @@
|
||||
import { expect } from 'chai'
|
||||
import { describe, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
SpeckleFunctionPathSchema,
|
||||
SpeckleServerUrlSchema,
|
||||
SpeckleTokenSchema
|
||||
} from './schema/inputs.js'
|
||||
} from './inputs.js'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
describe('schema', () => {
|
||||
describe('Speckle Server URL', () => {
|
||||
it('cannot be empty', async () => {
|
||||
expect(SpeckleServerUrlSchema.parse.bind(SpeckleServerUrlSchema, '')).to.throw(
|
||||
ZodError
|
||||
)
|
||||
expect(() => SpeckleServerUrlSchema.parse('')).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Speckle Token', () => {
|
||||
it('cannot be empty', async () => {
|
||||
expect(SpeckleTokenSchema.parse.bind(SpeckleTokenSchema, '')).to.throw(ZodError)
|
||||
expect(() => SpeckleTokenSchema.parse('')).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Speckle Function Path', () => {
|
||||
it('cannot be empty', async () => {
|
||||
expect(
|
||||
SpeckleFunctionPathSchema.parse.bind(SpeckleFunctionPathSchema, '')
|
||||
).to.throw(ZodError)
|
||||
expect(() => SpeckleFunctionPathSchema.parse('')).toThrow(ZodError)
|
||||
})
|
||||
it('cannot be an absolute path', async () => {
|
||||
expect(
|
||||
SpeckleFunctionPathSchema.parse.bind(SpeckleFunctionPathSchema, '/')
|
||||
).to.throw(ZodError)
|
||||
expect(() => SpeckleFunctionPathSchema.parse('/')).toThrow(ZodError)
|
||||
})
|
||||
it('can be a nested relative path', async () => {
|
||||
expect(SpeckleFunctionPathSchema.parse('src/main.ts')).to.equal('src/main.ts')
|
||||
expect(SpeckleFunctionPathSchema.parse('src/main.ts')).toBe('src/main.ts')
|
||||
})
|
||||
it('can have a leading dot slash', async () => {
|
||||
expect(SpeckleFunctionPathSchema.parse('./src/main.ts')).to.equal('./src/main.ts')
|
||||
expect(SpeckleFunctionPathSchema.parse('./src/main.ts')).toBe('./src/main.ts')
|
||||
})
|
||||
it('can be at the current directory', async () => {
|
||||
expect(SpeckleFunctionPathSchema.parse('.')).to.equal('.')
|
||||
expect(SpeckleFunctionPathSchema.parse('.')).toBe('.')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { z } from 'zod'
|
||||
|
||||
export const SpeckleServerUrlSchema = z.string().url().nonempty()
|
||||
export const SpeckleTokenSchema = z.string().nonempty()
|
||||
export const SpeckleFunctionRepositorySchema = z.string().nonempty() //TODO validate this as a git+https, https, or ssh url
|
||||
export const SpeckleFunctionPathSchema = z
|
||||
.string()
|
||||
.nonempty()
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { SpeckleFunctionSchema } from './specklefunction.js'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export type NonConformantSpeckleFunction = {
|
||||
apiVersion: string | undefined
|
||||
kind: string | undefined
|
||||
metadata:
|
||||
| {
|
||||
name: string | undefined
|
||||
}
|
||||
| undefined
|
||||
spec:
|
||||
| {
|
||||
steps:
|
||||
| [
|
||||
{
|
||||
name: string | undefined
|
||||
}
|
||||
]
|
||||
| []
|
||||
| undefined
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
export function getMinimalSpeckleFunctionExample(): NonConformantSpeckleFunction {
|
||||
return {
|
||||
apiVersion: 'speckle.systems/v1alpha1',
|
||||
kind: 'SpeckleFunction',
|
||||
metadata: {
|
||||
name: 'minimal'
|
||||
},
|
||||
spec: {
|
||||
steps: [
|
||||
{
|
||||
name: 'step1'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('speckle function schema', () => {
|
||||
let minimal: NonConformantSpeckleFunction
|
||||
|
||||
beforeEach(() => {
|
||||
minimal = getMinimalSpeckleFunctionExample()
|
||||
})
|
||||
|
||||
describe('Speckle Function', () => {
|
||||
it('cannot be empty', async () => {
|
||||
expect(() => SpeckleFunctionSchema.parse({})).toThrow(ZodError)
|
||||
})
|
||||
|
||||
describe('apiVersion', () => {
|
||||
it('cannot be missing-', async () => {
|
||||
minimal.apiVersion = ''
|
||||
expect(() => SpeckleFunctionSchema.parse('{}')).toThrow(ZodError)
|
||||
})
|
||||
it('cannot be invalid', async () => {
|
||||
minimal.apiVersion = 'invalid'
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('kind', () => {
|
||||
it('cannot be missing', async () => {
|
||||
minimal.kind = ''
|
||||
expect(() => SpeckleFunctionSchema.parse('/')).toThrow(ZodError)
|
||||
})
|
||||
it('cannot be invalid', async () => {
|
||||
minimal.kind = 'invalid'
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('metadata', () => {
|
||||
it('cannot be missing a metadata', async () => {
|
||||
minimal.metadata = undefined
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
describe('name', () => {
|
||||
it('cannot be missing', async () => {
|
||||
if (minimal.metadata === undefined) throw new Error('metadata is undefined') // for typescript
|
||||
minimal.metadata.name = ''
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('spec', () => {
|
||||
it('cannot be missing', async () => {
|
||||
minimal.spec = undefined
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
describe('steps', () => {
|
||||
it('cannot be missing', async () => {
|
||||
if (minimal.spec === undefined) throw new Error('spec is undefined') // for typescript
|
||||
minimal.spec.steps = undefined
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
it('must have at least one item', async () => {
|
||||
if (minimal.spec === undefined) throw new Error('spec is undefined') // for typescript
|
||||
minimal.spec.steps = []
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
it('items cannot be missing a name', async () => {
|
||||
if (minimal.spec === undefined) throw new Error('spec is undefined') // for typescript
|
||||
if (minimal.spec.steps === undefined) throw new Error('steps is undefined') // for typescript
|
||||
if (minimal.spec.steps[0] === undefined)
|
||||
throw new Error('steps[0] is undefined') // for typescript
|
||||
minimal.spec.steps[0].name = ''
|
||||
expect(() => SpeckleFunctionSchema.parse(minimal)).toThrow(ZodError)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('can be minimal', async () => {
|
||||
expect(SpeckleFunctionSchema.parse(minimal)).toStrictEqual(minimal)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,7 @@ export const SpeckleFunctionSchema = z.object({
|
||||
kind: z.literal(SpeckleFunctionKind),
|
||||
apiVersion: z.enum([SpeckleFunctionApiVersionV1Alpha1]),
|
||||
metadata: z.object({
|
||||
name: z.string(),
|
||||
name: z.string().nonempty(),
|
||||
annotations: z
|
||||
.object({
|
||||
'speckle.systems/v1alpha1/publishing/status': z
|
||||
@@ -47,7 +47,7 @@ export const SpeckleFunctionSchema = z.object({
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().optional()
|
||||
name: z.string().nonempty()
|
||||
//TODO
|
||||
})
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
'use strict'
|
||||
import chai from 'chai'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
chai.config.includeStack = true
|
||||
chai.config.showDiff = true
|
||||
chai.config.truncateThreshold = 0 // disable truncation
|
||||
chai.use(sinonChai)
|
||||
export default chai
|
||||
@@ -1,74 +1,92 @@
|
||||
import chai from './chai-config.js'
|
||||
import sinon from 'sinon'
|
||||
import { describe, it, afterEach, beforeEach } from 'vitest'
|
||||
import {
|
||||
App,
|
||||
eventHandler,
|
||||
createApp,
|
||||
createRouter,
|
||||
H3Event,
|
||||
Router,
|
||||
toNodeListener
|
||||
} from 'h3'
|
||||
import { listen } from 'listhen'
|
||||
import { getRandomPort } from 'get-port-please'
|
||||
import { registerSpeckleFunction } from '../speckleautomate.js'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { rest } from 'msw'
|
||||
import { registerSpeckleFunction } from '../registerspecklefunction.js'
|
||||
import { getLogger } from './logger.js'
|
||||
import { SpeckleFunctionPostResponseBody } from '../client/schema.js'
|
||||
|
||||
const expect = chai.expect
|
||||
import { getMinimalSpeckleFunctionExample } from '../schema/specklefunction.spec.js'
|
||||
import { ValidationError } from 'zod-validation-error'
|
||||
|
||||
describe('integration', () => {
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: 'error' })
|
||||
})
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
describe('Load from ./examples directory', () => {
|
||||
let app: App
|
||||
let router: Router
|
||||
let port: number
|
||||
const hostname = '127.0.0.1'
|
||||
|
||||
beforeEach(() => {
|
||||
app = createApp({ debug: false })
|
||||
})
|
||||
|
||||
describe('registerSpeckleAutomate', () => {
|
||||
it('should respond with Function name and id', async () => {
|
||||
// set up the fake server
|
||||
router = createRouter().post(
|
||||
'/api/v1/functions',
|
||||
eventHandler((event: H3Event): SpeckleFunctionPostResponseBody => {
|
||||
event.node.res.statusCode = 201
|
||||
event.node.res.statusMessage = 'Created'
|
||||
event.node.res.setHeader('Content-Type', 'application/json')
|
||||
return {
|
||||
functionId: 'minimalfunctionid',
|
||||
versionId: 'minimalversionid',
|
||||
imageName: 'speckle/minimalfunctionid:minimalversionid'
|
||||
describe('Load from ./examples directory', async () => {
|
||||
describe('registerSpeckleAutomate', async () => {
|
||||
describe('valid input', async () => {
|
||||
it('should respond with image name, function id, and version id', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://integration1.automate.speckle.example.org/api/v1/functions',
|
||||
async (req, res, ctx) => {
|
||||
expect(req.body).toStrictEqual({
|
||||
functionId: null,
|
||||
url: 'https://github.com/specklesystems/speckle-automate-examples.git',
|
||||
path: 'examples/minimal',
|
||||
ref: 'main',
|
||||
commitSha: '1234567890',
|
||||
manifest: getMinimalSpeckleFunctionExample()
|
||||
})
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer supersecret')
|
||||
const response = await res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
functionId: 'minimalfunctionid',
|
||||
versionId: 'minimalversionid',
|
||||
imageName: 'speckle/minimalfunctionid:minimalversionid'
|
||||
})
|
||||
)
|
||||
return response
|
||||
}
|
||||
)
|
||||
)
|
||||
const result = registerSpeckleFunction({
|
||||
speckleFunctionId: undefined,
|
||||
speckleServerUrl: 'https://integration1.automate.speckle.example.org',
|
||||
speckleToken: 'supersecret',
|
||||
speckleFunctionRepositoryUrl:
|
||||
'https://github.com/specklesystems/speckle-automate-examples.git',
|
||||
speckleFunctionPath: 'examples/minimal',
|
||||
ref: 'main',
|
||||
commitsha: '1234567890',
|
||||
logger: getLogger(),
|
||||
fileSystem: {
|
||||
loadYaml: async () => getMinimalSpeckleFunctionExample()
|
||||
}
|
||||
})
|
||||
)
|
||||
app.use(router)
|
||||
port = await getRandomPort(hostname)
|
||||
listen(toNodeListener(app), {
|
||||
hostname,
|
||||
port
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
functionId: 'minimalfunctionid',
|
||||
versionId: 'minimalversionid',
|
||||
imageName: 'speckle/minimalfunctionid:minimalversionid'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await registerSpeckleFunction({
|
||||
speckleFunctionId: undefined,
|
||||
speckleServerUrl: `http://${hostname}:${port}`,
|
||||
speckleToken: 'token',
|
||||
speckleFunctionPath: 'examples/minimal',
|
||||
ref: 'main',
|
||||
commitsha: '1234567890',
|
||||
logger: getLogger()
|
||||
})
|
||||
describe('invalid input', async () => {
|
||||
it('should throw an error', async () => {
|
||||
expect(async () =>
|
||||
registerSpeckleFunction({
|
||||
speckleFunctionId: undefined,
|
||||
speckleServerUrl: undefined,
|
||||
speckleToken: '',
|
||||
speckleFunctionRepositoryUrl: '',
|
||||
speckleFunctionPath: undefined,
|
||||
ref: undefined,
|
||||
commitsha: undefined,
|
||||
logger: getLogger(),
|
||||
fileSystem: {
|
||||
loadYaml: async () => getMinimalSpeckleFunctionExample()
|
||||
}
|
||||
})
|
||||
).rejects.toThrow(ValidationError)
|
||||
})
|
||||
|
||||
expect(result.functionId).to.equal('minimalfunctionid')
|
||||
expect(result.versionId).to.equal('minimalversionid')
|
||||
expect(result.imageName).to.equal('speckle/minimalfunctionid:minimalversionid')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"lib": ["ESNext"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true //FIXME: remove this
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
+34
-2
@@ -1,5 +1,37 @@
|
||||
export default {
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
mainFields: ['module']
|
||||
},
|
||||
test: {
|
||||
exclude: [...configDefaults.exclude, 'lib/**', 'dist/**'],
|
||||
coverage: {
|
||||
reporter: ['lcov', 'text', 'json', 'html'],
|
||||
provider: 'istanbul',
|
||||
exclude: [
|
||||
'src/tests/**/*',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.spec.tsx',
|
||||
'lib/**/*',
|
||||
'dist/**/*',
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
'**/*.js'
|
||||
],
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 95,
|
||||
statements: 95,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src/')
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': 'undefined'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
exclude: ['node_modules/**/*', 'lib/**/*', 'dist/**/*', '.yarn/**/*']
|
||||
}
|
||||
Reference in New Issue
Block a user