Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci(tests): fuzz testing workflow for REST API #3752

Draft
wants to merge 62 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d65e435
hack(tests): add fuzz testing workflow
iainsproat Dec 30, 2024
e4bbf38
fixes
iainsproat Dec 30, 2024
a026354
Update path of binary
iainsproat Dec 30, 2024
e66d2c2
Update binary path again, and add names to steps
iainsproat Dec 30, 2024
3e18153
More path fixes, too much nesting
iainsproat Dec 30, 2024
c8b96e4
Add debugging output
iainsproat Dec 30, 2024
48b7d2f
it is case-sensitive fml
iainsproat Dec 30, 2024
ea36cd9
Restler documentation is inconsistent on the output dir and there are…
iainsproat Dec 30, 2024
8ea40b3
Now output the file content to allow configuration
iainsproat Dec 30, 2024
968e4e7
Attempt to run speckle-server and test the API
iainsproat Dec 30, 2024
45674f0
fix minio service
iainsproat Dec 30, 2024
bc4ca17
Fix paths for yarn
iainsproat Dec 30, 2024
95fbf4a
set cache dependency path
iainsproat Dec 30, 2024
b2ebd47
Add a yarn start to server package.json
iainsproat Dec 30, 2024
8686c00
Caching
iainsproat Dec 30, 2024
3d20d7a
configure env vars for fuzz test
iainsproat Dec 30, 2024
a030b22
Printing results
iainsproat Dec 30, 2024
272b9fa
caching was too agressive
iainsproat Dec 30, 2024
d0c0c60
overwrite minio entrypoint
iainsproat Dec 30, 2024
e19abdd
More bug fixing
iainsproat Dec 30, 2024
2460fb9
remove network alias
iainsproat Dec 30, 2024
6cba30d
Bump OpenAPI specification version and add servers property
iainsproat Dec 30, 2024
198a8cc
Override host when testing
iainsproat Dec 30, 2024
b76f20e
Swap to docker compose
iainsproat Dec 30, 2024
e68da6a
fix postgres creds
iainsproat Dec 30, 2024
c8002de
Wait for server to start running, and always print restler output
iainsproat Dec 30, 2024
fe446d5
Wait until server is responding
iainsproat Dec 30, 2024
ca46336
Do not fail on decoding of responses being unexpected unicode
iainsproat Dec 30, 2024
f09ce25
Two step creation of grammar
iainsproat Dec 31, 2024
62abdcb
Fix restler config
iainsproat Dec 31, 2024
7691195
Add more detail to OpenAPI specification
iainsproat Dec 31, 2024
1b7cd28
Update openapi specification and skip unimplemented endpoint
iainsproat Dec 31, 2024
ebbf332
Restler should be authenticated; seed the database with data
iainsproat Dec 31, 2024
0d3ac1c
sudo is required
iainsproat Dec 31, 2024
dbf2c07
Token file should match Restler format
iainsproat Dec 31, 2024
d0c8287
Fix seeding of database
iainsproat Dec 31, 2024
beb60ef
Run from docker image, not source
iainsproat Dec 31, 2024
5aab551
remove obsolete step
iainsproat Dec 31, 2024
0d5a1c5
fix migrations in pg dump backup
iainsproat Dec 31, 2024
ea22d5b
wait until server is ready
iainsproat Dec 31, 2024
b13b547
Fix token
iainsproat Dec 31, 2024
3d5a049
Update OpenAPI specification
iainsproat Jan 1, 2025
fb2a22d
Fix indentation of openapi spec
iainsproat Jan 1, 2025
7e2e079
Upload test results as an artifact
iainsproat Jan 2, 2025
2cda8d8
Attempt to improve test coverage
iainsproat Jan 2, 2025
c00c3bb
Print compile logs
iainsproat Jan 2, 2025
5791e5a
Cache the restler binary and do not attempt to save cache if cache hit
iainsproat Jan 2, 2025
1f6bb11
Attempt to fix dictionary
iainsproat Jan 2, 2025
6ff569b
Update OpenAPI specification
iainsproat Jan 2, 2025
5f86467
provide proper path to compiler
iainsproat Jan 2, 2025
080a478
Bust cache to retry building
iainsproat Jan 2, 2025
a3e17b8
feat(server): allow ratelimiting to be explicitly disabled
iainsproat Jan 2, 2025
d2d6210
Disable rate limiter in CI
iainsproat Jan 2, 2025
96bc9fc
Merge branch 'main' into iain/web-511-fuzz-test-speckle-server-rest-api
iainsproat Jan 2, 2025
a5f15ed
Merge branch 'iain/ratelimiter-can-be-explicitly-disabled' into iain/…
iainsproat Jan 2, 2025
c188fa9
Explicitly disable ratelimiter
iainsproat Jan 2, 2025
fef3198
More fixes to openapi specification
iainsproat Jan 2, 2025
ed09a8c
Remove caching
iainsproat Jan 2, 2025
49884b5
fix(server/blobstorage): handles errors with missing content-type header
iainsproat Jan 2, 2025
b56ba47
More openapi specification improvements
iainsproat Jan 2, 2025
29ee4f4
Provide default app Ids
iainsproat Jan 2, 2025
39efe09
Enhance OpenAPI specification
iainsproat Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ jobs:
S3_REGION: '' # optional, defaults to 'us-east-1'
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
FF_BILLING_INTEGRATION_ENABLED: 'true'
RATELIMITER_ENABLED: 'false'
steps:
- checkout
- restore_cache:
Expand Down Expand Up @@ -578,6 +579,7 @@ jobs:
S3_REGION: '' # optional, defaults to 'us-east-1'
ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json'
DISABLE_ALL_FFS: 'true'
RATELIMITER_ENABLED: 'false'

test-server-multiregion:
<<: *test-server-job
Expand Down Expand Up @@ -624,6 +626,7 @@ jobs:
FF_WORKSPACES_MODULE_ENABLED: 'true'
FF_WORKSPACES_MULTI_REGION_ENABLED: 'true'
RUN_TESTS_IN_MULTIREGION_MODE: true
RATELIMITER_ENABLED: 'false'

test-frontend-2:
docker: &docker-node-browsers-image
Expand Down
2 changes: 2 additions & 0 deletions .gitguardian.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ secret:
name: setup/keycloak/speckle-realm.json - secret for dev keycloak
- match: 2e1b3675a4049cd39fe6db081735f747730969071528270800f00fa98720d198
name: setup/keycloak/speckle-realm.json - algorithm name
- match: 164797a0ebc32f504e2dbaf48bec77969456bc301829039b34dc6787a1f5b0f3
name: setup/fuzzer/token.txt - fuzz test token

version: 2
265 changes: 265 additions & 0 deletions .github/workflows/rest-api-fuzzer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
name: REST API Fuzz Test

on:
workflow_dispatch:
# schedule:
# - cron: "15 4 3 * *" # Run at 4:15am on the 3rd of every month
pull_request:
paths:
- '.github/workflows/rest-api-fuzzer.yml'
- 'setup/fuzzer/**/*'

env:
BUILD_CONFIGURATION: Release
BUILD_PLATFORM: 'Any CPU'
RESTLER_VERSION: '9.2.4'
PYTHON_VERSION: '3.8'
DOTNET_VERSION: '6.0.x'

jobs:
build-restler-fuzzer:
name: Fuzz test speckle-server REST API
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4
name: Checkout speckle-server
with:
path: 'speckle-server'

- uses: actions/checkout@v4
name: Checkout RESTler Fuzzer
with:
repository: microsoft/restler-fuzzer
ref: v${{ env.RESTLER_VERSION }}
path: 'restler-fuzzer' # The path to clone the repository within the {{ github.workspace }} directory

- name: Setup .NET ${{ env.DOTNET_VERSION }}
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
cache: true
cache-dependency-path: ${{ github.workspace }}/restler-fuzzer/src/Restler.sln

- name: Restore NuGet packages
run: dotnet restore ${{ github.workspace }}/restler-fuzzer/src/Restler.sln

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: ${{ github.workspace }}/restler-fuzzer/restler/requirements.txt

- name: Install engine (Python) dependencies
run: |
pip install -r ${{ github.workspace }}/restler-fuzzer/restler/requirements.txt

# This doesn't currently work as it cannot find path to compiler
# - name: Restore cached Restler binaries
# id: cache-restler-bin-restore
# uses: actions/cache/restore@v4
# with:
# path: |
# ${{ github.workspace }}/bin/restler
# key: restler-binaries-${{ env.RESTLER_VERSION }}-${{ hashFiles('speckle-server/.github/workflows/rest-api-fuzzer.yml') }}

- name: Build RESTler
# if: steps.cache-restler-bin-restore.outputs.cache-hit != 'true'
run: |
python ${{ github.workspace }}/restler-fuzzer/build-restler.py --dest_dir ${{ github.workspace }}/bin
python -m compileall -b ${{ github.workspace }}/bin/restler

- name: Debug the built output
run: |
ls -la ${{ github.workspace }}/bin/restler
ls -la ${{ github.workspace }}/bin/restler/Restler

# - name: Save Restler binaries to cache
# if: steps.cache-restler-bin-restore.outputs.cache-hit != 'true'
# id: cache-restler-bin-save
# uses: actions/cache/save@v4
# with:
# path: |
# ${{ github.workspace }}/bin/restler
# key: ${{ steps.cache-restler-bin-restore.outputs.cache-primary-key }}

- name: Restore cached Restler configuration
id: cache-config-restore
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/restlerConfig
key: restler-config-${{ hashFiles('speckle-server/setup/fuzzer/speckle-server.openapi.json') }}

- name: Generate RESTler config from OpenAPI specification
if: steps.cache-config-restore.outputs.cache-hit != 'true'
run: |
${{ github.workspace }}/bin/restler/Restler generate_config --specs ${{ github.workspace }}/speckle-server/setup/fuzzer/speckle-server.openapi.json

- name: Print the Restler configuration
run: |
ls -la ${{ github.workspace }}
ls -la ${{ github.workspace }}/restlerConfig
echo ""
echo "############################################"
echo "# Engine settings #"
echo "# To customize, copy and save this file to #"
echo "# setup/fuzzer/settings.restler.json #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/restlerConfig/engine_settings.json
echo ""
echo "############################################"
echo "# Config #"
echo "# To customize, copy and save this file to #"
echo "# setup/fuzzer/config.restler.json #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/restlerConfig/config.json
echo ""
echo "############################################"
echo "# Dictionary #"
echo "# To customize, copy and save this file to #"
echo "# setup/fuzzer/dictionary.restler.json #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/restlerConfig/dict.json
echo ""
echo "############################################"
echo "# Annotations #"
echo "# To customize, copy and save this file to #"
echo "# setup/fuzzer/annotations.restler.json #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/restlerConfig/annotations.json

- name: Save Restler Config
if: steps.cache-config-restore.outputs.cache-hit != 'true'
id: cache-config-save
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/restlerConfig
key: ${{ steps.cache-config-restore.outputs.cache-primary-key }}

- name: Restore cached Restler grammar
id: cache-grammar-restore
uses: actions/cache/restore@v4
with:
path: |
${{ github.workspace }}/Compile
key: restler-grammar-${{ hashFiles('speckle-server/setup/fuzzer/*.json') }}

- name: Generate RESTler grammar from Restler config
if: steps.cache-grammar-restore.outputs.cache-hit != 'true'
run: |
${{ github.workspace }}/bin/restler/Restler compile ${{ github.workspace }}/speckle-server/setup/fuzzer/config.restler.json

- name: Print the contents of the Restler compile directory
if: always()
run: |
ls -la ${{ github.workspace }} || true
ls -la ${{ github.workspace }}/Compile || true
cat $(find ${{ github.workspace }}/Compile -type f -name "*.log") || true

- name: Save Grammar
if: steps.cache-grammar-restore.outputs.cache-hit != 'true'
id: cache-grammar-save
uses: actions/cache/save@v4
with:
path: |
${{ github.workspace }}/Compile
key: ${{ steps.cache-grammar-restore.outputs.cache-primary-key }}

- name: Deploy dependencies (docker compose)
run: |
docker compose --file ${{ github.workspace }}/speckle-server/docker-compose-deps.yml up --detach

- name: Seed the database
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends postgresql-client
PGPASSWORD=speckle psql -h 127.0.0.1 -U speckle -d speckle -p 5432 -w < ${{ github.workspace }}/speckle-server/setup/fuzzer/speckle.backup.sql

- name: Deploy speckle-server (docker compose)
working-directory: ${{ github.workspace }}
timeout-minutes: 1
run: |
docker compose --file ${{ github.workspace }}/speckle-server/setup/fuzzer/docker-compose-speckle.yml up --detach

- name: Run RESTler coverage test
run: |
until curl --output /dev/null --silent --head --fail http://127.0.0.1:3000/readiness; do
echo "Waiting a further 3 seconds for speckle-server to start..."
sleep 3
done

${{ github.workspace }}/bin/restler/Restler test \
--grammar_file "${{ github.workspace }}/Compile/grammar.py" \
--dictionary_file "${{ github.workspace }}/speckle-server/setup/fuzzer/dictionary.restler.json" \
--settings "${{ github.workspace }}/speckle-server/setup/fuzzer/settings.restler.json" \
--no_ssl \
--target_ip "127.0.0.1" \
--target_port "3000"

- name: Print the results
if: always()
run: |
ls -la ${{ github.workspace }}/Test
ls -la ${{ github.workspace }}/Test/RestlerResults || true
ls -la ${{ github.workspace }}/Test/ResponseBuckets || true
echo ""
echo "############################################"
echo "# Engine stderr #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/EngineStdErr.txt || true
echo ""
echo "############################################"
echo "# Engine stdout #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/EngineStdOut.txt || true
echo ""
echo "############################################"
echo "# Results analyzer stderr #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/ResultsAnalyzerStdErr.txt || true
echo ""
echo "############################################"
echo "# Results analyzer stdout #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/ResultsAnalyzerStdOut.txt || true
echo ""
echo "############################################"
echo "# Coverage failures to investigate #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/coverage_failures_to_investigate.txt || true
echo ""
echo "############################################"
echo "# Restler logs #"
echo "############################################"
echo ""
cat ${{ github.workspace }}/Test/restler-*.log || true

- uses: actions/upload-artifact@v4
name: Store the Test output
if: always()
with:
name: fuzz-test-rest-api-output
path: ${{ github.workspace }}/Test
if-no-files-found: error
retention-days: 5
overwrite: true

- name: Print Docker Compose logs
if: always()
run: |
docker compose --file ${{ github.workspace }}/speckle-server/docker-compose-deps.yml logs
docker compose --file ${{ github.workspace }}/speckle-server/setup/fuzzer/docker-compose-speckle.yml logs
3 changes: 2 additions & 1 deletion packages/server/.env.test-example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ PORT=0
POSTGRES_URL=postgres://speckle:[email protected]/speckle2_test
POSTGRES_USER=''
MULTI_REGION_CONFIG_PATH="multiregion.test.json"
#RUN_TESTS_IN_MULTIREGION_MODE=true
#RUN_TESTS_IN_MULTIREGION_MODE=true
RATELIMITER_ENABLED='false'
17 changes: 13 additions & 4 deletions packages/server/modules/blobstorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,19 @@ export const init: SpeckleModule['init'] = async (app) => {
uploadError?: Error | null | string
formKey: string
}>[] = []
const busboy = Busboy({
headers: req.headers,
limits: { fileSize: getFileSizeLimit() }
})
let busboy: Busboy.Busboy
try {
// Busboy does some validation of user input (headers) on creation
busboy = Busboy({
headers: req.headers,
limits: { fileSize: getFileSizeLimit() }
})
} catch (err) {
throw new BadRequestError(
err instanceof Error ? err.message : 'Error while uploading blob',
ensureError(err, 'Unknown error while uploading blob')
)
}

const [projectDb, projectStorage] = await Promise.all([
getProjectDbClient({ projectId: streamId }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,15 @@ describe('Blobs integration @blobstorage', () => {

expect(response.status).to.equal(400)
})

it('Returns 400 for missing content-type', async () => {
const streamId = await createStreamForTest()
const response = await request(app)
.post(`/api/stream/${streamId}/blob`)
.set('Authorization', `Bearer ${token}`)
// .set('Content-type', 'multipart/form-data; boundary=XXX') // purposefully missing content-type header

console.log(response.text)
expect(response.status).to.equal(400)
})
})
8 changes: 6 additions & 2 deletions packages/server/modules/core/services/ratelimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express from 'express'
import {
getRedisUrl,
getIntFromEnv,
isTestEnv
getBooleanFromEnv
} from '@/modules/shared/helpers/envHelper'
import {
BurstyRateLimiter,
Expand Down Expand Up @@ -55,6 +55,10 @@ export type RateLimiterMapping = {

export type RateLimitAction = keyof typeof LIMITS

export const isRateLimiterEnabled = (): boolean => {
return getBooleanFromEnv('RATELIMITER_ENABLED', true)
}

export const LIMITS = <const>{
ALL_REQUESTS: {
regularOptions: {
Expand Down Expand Up @@ -307,7 +311,7 @@ export const createRateLimiterMiddleware = (
res: express.Response,
next: express.NextFunction
) => {
if (isTestEnv()) return next()
if (!isRateLimiterEnabled()) return next()
const path = getRequestPath(req) || ''
const action = getActionForPath(path, req.method)
const source = getSourceFromRequest(req)
Expand Down
Loading
Loading