Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.envrc.private
/.idea/
.DS_Store
.DS_Store
target
12 changes: 12 additions & 0 deletions external-call-integration-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Daml build artifacts
.daml/

# Logs
log/
*.log

# Temp files
/tmp/

# Generated TLS certificates (contain private keys)
certs/
63 changes: 63 additions & 0 deletions external-call-integration-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# External Call Integration Test

Tests for the external call feature, specifically verifying that observers can replay transactions using stored results without needing access to the external service.

## Quick Start

```bash
./run_test.sh --all # run all tests
./run_test.sh # just happy path
./run_test.sh --help # see all options
```

## What's Being Tested

The key property: participant2 (observer) has NO extension configured, yet it can process transactions with external calls because results are stored in the transaction.

```
participant1 (signatory) participant2 (observer)
- has extension - NO extension
- makes HTTP calls - uses stored results
```

## Test Suites

| Flag | Tests |
|------|-------|
| `--happy` | Basic call + observer replay |
| `--errors` | HTTP error codes (400-504) |
| `--retry` | Retry logic |
| `--auth` | JWT authentication |
| `--tls` | TLS/HTTPS |
| `--timeout` | Request timeouts |
| `--edge` | Input/output edge cases |
| `--multi` | Multi-participant scenarios |
| `--config` | Config edge cases |
| `--echo` | Echo mode (no HTTP) |

## Manual Testing

```bash
# Terminal 1: mock service
python3 scripts/mock_service.py 8080

# Terminal 2: Canton
export JAVA_OPTS="-Ddar.path=.daml/dist/external-call-integration-test-1.0.0.dar"
../../sdk/bazel-bin/canton/community_app run -c canton.conf scripts/full_test.canton
```

## Mock Service

Responds based on function_id: `echo`, `error-{code}`, `delay-{ms}`, `retry-once`, etc.
Logs to stdout and writes stats to `/tmp/external_call_test_counts.json`.

## Config Files

- `canton.conf` - participant1 has extension, participant2 doesn't
- `canton-auth.conf` - includes JWT token
- `canton-tls.conf` - TLS enabled
- `canton-echo.conf` - echo mode (no HTTP calls)
- `canton-both-extensions.conf` - both participants have extension
- `canton-no-extensions.conf` - neither has extension

See [TEST_PLAN.md](TEST_PLAN.md) for full test coverage.
126 changes: 126 additions & 0 deletions external-call-integration-test/TEST_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# External Call Integration Tests

Integration tests for the external call feature. Run with `./run_test.sh --all`.

## Test Coverage

### Happy Path
- [x] Basic external call with response (`full_test.canton`)
- [x] Observer replay without HTTP call - verifies stored results work
- [x] Multiple calls in single choice (`test_edge_cases.canton`)
- [x] Different function IDs

### HTTP Errors (`test_errors.canton`)
All error codes tested: 400, 401, 403, 404, 500, 502, 503, 504

### Timeouts (`test_timeout.canton`)
- [x] Request timeout (delay > timeout)
- [ ] Connection timeout (needs network-level mock)
- [ ] Max total timeout

### Retry Logic (`test_retry.canton`)
- [x] Retry on 503 then success
- [x] Max retries exceeded (always 503)
- [x] Rate limit 429
- [ ] Retry-After header timing
- [ ] Exponential backoff verification

### JWT Auth (`test_auth.canton`)
- [x] Valid token sent and accepted
- [x] Header format verification (jwt-echo)
- [x] Server rejects token (401)
- [x] Missing token (`test_edge_cases.canton`)
- [ ] Token from file

### TLS (`test_tls.canton`)
- [x] HTTPS with self-signed cert (insecure mode)
- [x] TLS connection works
- [ ] Cert validation failure
- [ ] HTTP/HTTPS mismatch

### Input/Output Edge Cases (`test_edge_cases.canton`)
- [x] Empty input rejected
- [x] Large input (10KB)
- [x] Large output (100KB)
- [x] Unicode bytes (hex-encoded)
- [x] All byte values 00-FF
- [x] Invalid hex rejected

### Config Edge Cases (`test_config.canton`)
- [x] Unknown extension ID
- [x] Unknown function ID (allowed by default)
- [x] Config hash mismatch (allowed by default)
- [ ] Startup validation (needs different test approach)

### Multi-Participant (`test_multi_*.canton`)
- [x] Signatory has extension, observer doesn't (covered by happy path)
- [x] Both participants have extension
- [x] Neither has extension - proper error

### Echo Mode (`test_echo.canton`)
- [x] Returns input as output, no HTTP calls

## Running

```bash
./run_test.sh # happy path only
./run_test.sh --all # all 39 tests
./run_test.sh --errors # just error handling
./run_test.sh --retry # just retry logic
./run_test.sh --auth # JWT tests
./run_test.sh --tls # TLS tests
./run_test.sh --edge # edge cases
./run_test.sh --multi # multi-participant
./run_test.sh --config # config edge cases
./run_test.sh --echo # echo mode
./run_test.sh --rebuild-dar --all # rebuild DAR first
```

## Files

```
canton.conf # main config (participant1 has extension, participant2 doesn't)
canton-auth.conf # adds JWT token
canton-tls.conf # TLS enabled
canton-echo.conf # echo mode
canton-both-extensions.conf # both participants have extension
canton-no-extensions.conf # neither has extension

scripts/
mock_service.py # configurable mock (errors, delays, JWT, TLS)
full_test.canton # happy path
test_errors.canton # HTTP error codes
test_retry.canton # retry logic
test_auth.canton # JWT
test_tls.canton # TLS
test_timeout.canton # timeouts
test_edge_cases.canton # input/output edge cases
test_config.canton # config edge cases
test_multi_both.canton # both have extension
test_multi_none.canton # neither has extension
test_echo.canton # echo mode
generate_certs.sh # creates self-signed certs for TLS tests
```

## Mock Service

The mock (`scripts/mock_service.py`) responds based on function_id:
- `echo` - returns input
- `error-{code}` - returns that HTTP status
- `delay-{ms}` - delays response
- `retry-once` - 503 first, then 200
- `retry-always` - always 503
- `rate-limit` - 429 with Retry-After
- `jwt-required` - checks for Bearer token
- `jwt-echo` - echoes auth header
- `jwt-invalid` - always 401
- `large-output` - returns 100KB
- `tls-test` - for TLS verification

## Not Implemented

These are tricky to test reliably:
- Retry-After timing verification
- Exponential backoff timing
- Startup validation (needs Canton restart mid-test)
- Connection-level timeouts (need network simulation)
84 changes: 84 additions & 0 deletions external-call-integration-test/canton-auth.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# JWT authentication test config
# participant1 has JWT, participant2 doesn't

canton {
parameters {
non-standard-config = yes
alpha-version-support = yes
}

sequencers {
sequencer1 {
storage.type = memory
public-api.port = 5008
admin-api.port = 5009
sequencer.type = BFT
}
}

mediators {
mediator1 {
storage.type = memory
admin-api.port = 5007
}
}

participants {
participant1 {
storage.type = memory
admin-api.port = 5012
ledger-api.port = 5011
http-ledger-api.port = 5013

parameters {
alpha-version-support = yes

engine {
extensions {
test-oracle {
name = "Test Oracle (with JWT)"
host = "127.0.0.1"
port = 8080
use-tls = false
jwt = "test-token"
connect-timeout = 2s
request-timeout = 5s
max-total-timeout = 10s
max-retries = 1

declared-functions = [
{ function-id = "echo", config-hash = "00000000" }
{ function-id = "jwt-required", config-hash = "00000000" }
{ function-id = "jwt-echo", config-hash = "00000000" }
{ function-id = "jwt-invalid", config-hash = "00000000" }
]
}
}

extension-settings {
validate-extensions-on-startup = false
fail-on-extension-validation-error = false
echo-mode = false
}
}
}
}

participant2 {
storage.type = memory
admin-api.port = 5022
ledger-api.port = 5021
http-ledger-api.port = 5023

parameters {
alpha-version-support = yes
engine {
extension-settings {
validate-extensions-on-startup = false
echo-mode = false
}
}
}
}
}
}
99 changes: 99 additions & 0 deletions external-call-integration-test/canton-both-extensions.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Multi-participant test config (T9.2)
# Both participants have extension - tests validation mode behavior

canton {
parameters {
non-standard-config = yes
alpha-version-support = yes
}

sequencers {
sequencer1 {
storage.type = memory
public-api.port = 5008
admin-api.port = 5009
sequencer.type = BFT
}
}

mediators {
mediator1 {
storage.type = memory
admin-api.port = 5007
}
}

participants {
participant1 {
storage.type = memory
admin-api.port = 5012
ledger-api.port = 5011
http-ledger-api.port = 5013

parameters {
alpha-version-support = yes

engine {
extensions {
test-oracle {
name = "Test Oracle (Participant 1)"
host = "127.0.0.1"
port = 8080
use-tls = false
connect-timeout = 2s
request-timeout = 5s
max-total-timeout = 10s
max-retries = 1

declared-functions = [
{ function-id = "echo", config-hash = "00000000" }
]
}
}

extension-settings {
validate-extensions-on-startup = false
fail-on-extension-validation-error = false
echo-mode = false
}
}
}
}

participant2 {
storage.type = memory
admin-api.port = 5022
ledger-api.port = 5021
http-ledger-api.port = 5023

parameters {
alpha-version-support = yes

engine {
extensions {
test-oracle {
name = "Test Oracle (Participant 2)"
host = "127.0.0.1"
port = 8080
use-tls = false
connect-timeout = 2s
request-timeout = 5s
max-total-timeout = 10s
max-retries = 1

declared-functions = [
{ function-id = "echo", config-hash = "00000000" }
]
}
}

extension-settings {
validate-extensions-on-startup = false
fail-on-extension-validation-error = false
echo-mode = false
}
}
}
}
}
}
Loading