feat(dispatch): implement dispatch-workflow.sh with dispatch, poll, and timeout
Add dispatch-workflow.sh script that dispatches a Gitea workflow in another repository and polls synchronously for completion. Refactor mock-api.sh to use Python3 HTTP server with sequence support, enabling stateful poll-response simulation in tests.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Ticket 0001: `report-status.sh`
|
# Ticket 0001: `report-status.sh`
|
||||||
|
|
||||||
**Vaihe:** 1/12
|
**Vaihe:** 1/12
|
||||||
**Status:** pending
|
**Status:** done
|
||||||
**Feature branch:** `feature/0001-report-status-sh`
|
**Feature branch:** `feature/0001-report-status-sh`
|
||||||
**TDD required:** Yes
|
**TDD required:** Yes
|
||||||
**Feature file required:** Yes
|
**Feature file required:** Yes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Ticket 0002: `dispatch-workflow.sh`
|
# Ticket 0002: `dispatch-workflow.sh`
|
||||||
|
|
||||||
**Vaihe:** 2/12
|
**Vaihe:** 2/12
|
||||||
**Status:** pending
|
**Status:** done
|
||||||
**Feature branch:** `feature/0002-dispatch-workflow-sh`
|
**Feature branch:** `feature/0002-dispatch-workflow-sh`
|
||||||
**TDD required:** Yes
|
**TDD required:** Yes
|
||||||
**Feature file required:** Yes
|
**Feature file required:** Yes
|
||||||
|
|||||||
Executable
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[ -z "${GITEA_API_URL:-}" ] && echo "ERROR: GITEA_API_URL is not set" >&2 && exit 1
|
||||||
|
[ -z "${GITEA_TOKEN:-}" ] && echo "ERROR: GITEA_TOKEN is not set" >&2 && exit 1
|
||||||
|
|
||||||
|
TARGET_REPO="${1:-}"
|
||||||
|
WORKFLOW_FILE="${2:-}"
|
||||||
|
REF="${3:-}"
|
||||||
|
INPUTS_JSON="${4:-}"
|
||||||
|
TIMEOUT_MINUTES="${5:-360}"
|
||||||
|
POLL_INTERVAL="${DISPATCH_POLL_INTERVAL:-10}"
|
||||||
|
|
||||||
|
[ -z "$TARGET_REPO" ] && echo "ERROR: target_repo argument is required" >&2 && exit 1
|
||||||
|
[ -z "$WORKFLOW_FILE" ] && echo "ERROR: workflow_file argument is required" >&2 && exit 1
|
||||||
|
[ -z "$REF" ] && echo "ERROR: ref argument is required" >&2 && exit 1
|
||||||
|
[ -z "$INPUTS_JSON" ] && echo "ERROR: inputs_json argument is required" >&2 && exit 1
|
||||||
|
|
||||||
|
DISPATCH_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/workflows/$WORKFLOW_FILE/dispatches"
|
||||||
|
DISPATCH_BODY=$(jq -nc --arg ref "$REF" --argjson inputs "$INPUTS_JSON" '{ref: $ref, inputs: $inputs}')
|
||||||
|
|
||||||
|
DISPATCH_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
--connect-timeout 5 --max-time 10 \
|
||||||
|
-X POST "$DISPATCH_URL" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$DISPATCH_BODY")
|
||||||
|
|
||||||
|
if [ "$DISPATCH_CODE" != "201" ]; then
|
||||||
|
echo "ERROR: Dispatch failed with HTTP $DISPATCH_CODE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUNS_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?status=running"
|
||||||
|
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" "$RUNS_URL")
|
||||||
|
|
||||||
|
RUN_ID=$(echo "$RUNS_RESP" | jq -r '.workflow_runs[0].id // empty')
|
||||||
|
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
|
||||||
|
echo "ERROR: Could not find dispatched workflow run" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
NOW=$(date +%s)
|
||||||
|
ELAPSED=$((NOW - START_TIME))
|
||||||
|
if awk -v e="$ELAPSED" -v t="$TIMEOUT_SECONDS" 'BEGIN { exit !(e >= t) }'; then
|
||||||
|
echo "ERROR: Timeout after ${TIMEOUT_MINUTES} minutes" >&2
|
||||||
|
exit 124
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs/$RUN_ID"
|
||||||
|
RUN_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" "$RUN_URL")
|
||||||
|
|
||||||
|
STATUS=$(echo "$RUN_RESP" | jq -r '.status // "running"')
|
||||||
|
if [ "$STATUS" = "completed" ]; then
|
||||||
|
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
|
||||||
|
if [ "$CONCLUSION" = "success" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
|
done
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
source tests/helpers/mock-api.sh
|
||||||
|
export GITEA_API_URL="http://localhost:18080"
|
||||||
|
export GITEA_TOKEN="test-token-abc123"
|
||||||
|
export DISPATCH_POLL_INTERVAL="0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
mock_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":201},
|
||||||
|
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dispatch: poll returns failure conclusion → exit 1" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":201},
|
||||||
|
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dispatch: poll returns cancelled conclusion → exit 1" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":201},
|
||||||
|
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "timeout: poll never completes, exceeds timeout_minutes → exit 124" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":201},
|
||||||
|
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"running"}}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}' "0.001"
|
||||||
|
[ "$status" -eq 124 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dispatch API returns 500 → exit 1" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":500}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "POST dispatch is called with correct URL and payload" {
|
||||||
|
mock_set_sequence '[
|
||||||
|
{"code":201},
|
||||||
|
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
||||||
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
|
||||||
|
]'
|
||||||
|
mock_start
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
path=$(mock_get_first_request_path)
|
||||||
|
[[ "$path" == *"/api/v1/repos/test-owner/test-repo/actions/workflows/test.yml/dispatches"* ]]
|
||||||
|
method=$(mock_get_first_request_method)
|
||||||
|
[[ "$method" == "POST" ]]
|
||||||
|
body=$(mock_get_first_request_body)
|
||||||
|
[[ "$body" == *'"ref":"main"'* ]]
|
||||||
|
[[ "$body" == *'"inputs"'* ]]
|
||||||
|
[[ "$body" == *'"version":"1.2.3"'* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing GITEA_API_URL → exit 1 with error message" {
|
||||||
|
unset GITEA_API_URL
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"ERROR"* || "$output" == *"GITEA_API_URL"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing GITEA_TOKEN → exit 1 with error message" {
|
||||||
|
unset GITEA_TOKEN
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"ERROR"* || "$output" == *"GITEA_TOKEN"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing target_repo argument → exit 1" {
|
||||||
|
run bash scripts/dispatch-workflow.sh "" "test.yml" "main" '{}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing workflow_file argument → exit 1" {
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "" "main" '{}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing ref argument → exit 1" {
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "" '{}'
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "missing inputs_json argument → exit 1" {
|
||||||
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" ""
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
const { execSync, spawn } = require('child_process');
|
||||||
|
const { When, Then, Given } = require('@cucumber/cucumber');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||||
|
const MOCK_SERVER = path.join(PROJECT_ROOT, 'tests', 'helpers', 'mock-server.py');
|
||||||
|
const DISPATCH_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'dispatch-workflow.sh');
|
||||||
|
|
||||||
|
function bash(cmd) {
|
||||||
|
try {
|
||||||
|
const out = execSync(`bash -c ${JSON.stringify(cmd)}`, {
|
||||||
|
cwd: PROJECT_ROOT,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
return { status: 0, stdout: out };
|
||||||
|
} catch (e) {
|
||||||
|
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function envBlock() {
|
||||||
|
return [
|
||||||
|
'export GITEA_API_URL="http://localhost:18080"',
|
||||||
|
'export GITEA_TOKEN="test-token-abc123"',
|
||||||
|
'export DISPATCH_POLL_INTERVAL="0.1"',
|
||||||
|
].join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMock(seqJson) {
|
||||||
|
execSync('pkill -9 -f mock-server 2>/dev/null || true', { stdio: 'ignore' });
|
||||||
|
execSync('sleep 0.4', { stdio: 'ignore' });
|
||||||
|
|
||||||
|
const seqFile = path.join(os.tmpdir(), `cucumber_seq_${Date.now()}.json`);
|
||||||
|
fs.writeFileSync(seqFile, seqJson);
|
||||||
|
const idxFile = `${seqFile}.idx`;
|
||||||
|
fs.writeFileSync(idxFile, '0');
|
||||||
|
|
||||||
|
const reqFile = path.join(os.tmpdir(), `cucumber_req_${Date.now()}.log`);
|
||||||
|
const configFile = path.join(os.tmpdir(), `cucumber_cfg_${Date.now()}.txt`);
|
||||||
|
fs.writeFileSync(configFile, `SEQUENCE\n${seqJson}\n${idxFile}`);
|
||||||
|
|
||||||
|
const proc = spawn('python3', [MOCK_SERVER, '18080', configFile, reqFile], {
|
||||||
|
cwd: PROJECT_ROOT,
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
proc.unref();
|
||||||
|
this._mockProc = proc;
|
||||||
|
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
try {
|
||||||
|
execSync('curl -s --max-time 1 http://localhost:18080/', { stdio: 'ignore' });
|
||||||
|
fs.writeFileSync(idxFile, '0');
|
||||||
|
return;
|
||||||
|
} catch (_) {}
|
||||||
|
execSync('sleep 0.1', { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
throw new Error('Mock failed to start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDispatch(args) {
|
||||||
|
return bash(`${envBlock()}; bash "${DISPATCH_SCRIPT}" ${args}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
Given('a deployment has completed in the target environment', function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
Given('the test project repository exists with test definitions', function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
When('a test workflow is dispatched to a test project', function () {
|
||||||
|
setupMock(JSON.stringify([
|
||||||
|
{ code: 201 },
|
||||||
|
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||||
|
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'success' } },
|
||||||
|
]));
|
||||||
|
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\'');
|
||||||
|
this.dispatchResult = r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('the pipeline waits until the test workflow finishes', function () {
|
||||||
|
if (this.dispatchResult !== 0) throw new Error(`Expected 0, got ${this.dispatchResult}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('the pipeline continues only after receiving a success result', function () {
|
||||||
|
if (this.dispatchResult !== 0) throw new Error('Expected pipeline to continue after success');
|
||||||
|
});
|
||||||
|
|
||||||
|
When('a test workflow is dispatched and the tests fail', function () {
|
||||||
|
setupMock(JSON.stringify([
|
||||||
|
{ code: 201 },
|
||||||
|
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||||
|
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'failure' } },
|
||||||
|
]));
|
||||||
|
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\'');
|
||||||
|
this.dispatchResult = r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('the calling pipeline reports failure', function () {
|
||||||
|
if (this.dispatchResult !== 1) throw new Error(`Expected failure exit 1, got ${this.dispatchResult}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
When('a test workflow is dispatched but does not finish within the allowed time', function () {
|
||||||
|
setupMock(JSON.stringify([
|
||||||
|
{ code: 201 },
|
||||||
|
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||||
|
{ code: 200, body: { id: 1, status: 'running' } },
|
||||||
|
{ code: 200, body: { id: 1, status: 'running' } },
|
||||||
|
{ code: 200, body: { id: 1, status: 'running' } },
|
||||||
|
]));
|
||||||
|
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "0.001"');
|
||||||
|
this.dispatchResult = r.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('the calling pipeline reports a timeout error', function () {
|
||||||
|
if (this.dispatchResult !== 124) throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}`);
|
||||||
|
});
|
||||||
@@ -11,18 +11,18 @@ Feature: Test execution
|
|||||||
# Ticket 0002: dispatch-workflow.sh — Dispatching and polling workflows
|
# Ticket 0002: dispatch-workflow.sh — Dispatching and polling workflows
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@ticket-0002 @mock @wip @real
|
@ticket-0002 @mock @real
|
||||||
Scenario: Dispatch a test workflow and wait for its completion
|
Scenario: Dispatch a test workflow and wait for its completion
|
||||||
When a test workflow is dispatched to a test project
|
When a test workflow is dispatched to a test project
|
||||||
Then the pipeline waits until the test workflow finishes
|
Then the pipeline waits until the test workflow finishes
|
||||||
And the pipeline continues only after receiving a success result
|
And the pipeline continues only after receiving a success result
|
||||||
|
|
||||||
@ticket-0002 @mock @wip @real
|
@ticket-0002 @mock @real
|
||||||
Scenario: Dispatch fails when the dispatched test workflow fails
|
Scenario: Dispatch fails when the dispatched test workflow fails
|
||||||
When a test workflow is dispatched and the tests fail
|
When a test workflow is dispatched and the tests fail
|
||||||
Then the calling pipeline reports failure
|
Then the calling pipeline reports failure
|
||||||
|
|
||||||
@ticket-0002 @mock @wip
|
@ticket-0002 @mock
|
||||||
Scenario: Dispatch times out when the test workflow takes too long
|
Scenario: Dispatch times out when the test workflow takes too long
|
||||||
When a test workflow is dispatched but does not finish within the allowed time
|
When a test workflow is dispatched but does not finish within the allowed time
|
||||||
Then the calling pipeline reports a timeout error
|
Then the calling pipeline reports a timeout error
|
||||||
|
|||||||
+60
-14
@@ -6,12 +6,14 @@ MOCK_PID=""
|
|||||||
MOCK_REQUEST_FILE=""
|
MOCK_REQUEST_FILE=""
|
||||||
MOCK_RESPONSE_CODE=201
|
MOCK_RESPONSE_CODE=201
|
||||||
MOCK_STATE_FILE="/tmp/mock_api_state"
|
MOCK_STATE_FILE="/tmp/mock_api_state"
|
||||||
|
MOCK_SEQUENCE_FILE=""
|
||||||
|
MOCK_CONFIG_FILE=""
|
||||||
|
|
||||||
_kill_port() {
|
_kill_port() {
|
||||||
local pids
|
local pids
|
||||||
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
|
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
|
||||||
[ -n "$pids" ] && kill $pids 2>/dev/null || true
|
[ -n "$pids" ] && kill -9 $pids 2>/dev/null || true
|
||||||
sleep 0.2
|
sleep 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
_wait_port_free() {
|
_wait_port_free() {
|
||||||
@@ -22,32 +24,52 @@ _wait_port_free() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mock_set_sequence() {
|
||||||
|
MOCK_SEQUENCE_FILE=$(mktemp)
|
||||||
|
echo "$1" | jq -c '.' > "$MOCK_SEQUENCE_FILE"
|
||||||
|
echo "0" > "${MOCK_SEQUENCE_FILE}.idx"
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_clear_sequence() {
|
||||||
|
MOCK_SEQUENCE_FILE=""
|
||||||
|
}
|
||||||
|
|
||||||
mock_start() {
|
mock_start() {
|
||||||
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
|
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
|
||||||
MOCK_REQUEST_FILE=$(mktemp)
|
MOCK_REQUEST_FILE=$(mktemp)
|
||||||
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
|
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
|
||||||
|
MOCK_CONFIG_FILE=$(mktemp)
|
||||||
|
|
||||||
|
if [ -n "${MOCK_SEQUENCE_FILE:-}" ] && [ -f "$MOCK_SEQUENCE_FILE" ]; then
|
||||||
|
echo "SEQUENCE" > "$MOCK_CONFIG_FILE"
|
||||||
|
cat "$MOCK_SEQUENCE_FILE" >> "$MOCK_CONFIG_FILE"
|
||||||
|
printf '%s' "${MOCK_SEQUENCE_FILE}.idx" >> "$MOCK_CONFIG_FILE"
|
||||||
|
else
|
||||||
|
echo "SINGLE" > "$MOCK_CONFIG_FILE"
|
||||||
|
echo "$MOCK_RESPONSE_CODE" >> "$MOCK_CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
_kill_port
|
_kill_port
|
||||||
_wait_port_free
|
_wait_port_free
|
||||||
|
|
||||||
(
|
nohup python3 "$(dirname "${BASH_SOURCE[0]}")/mock-server.py" "$MOCK_PORT" "$MOCK_CONFIG_FILE" "$MOCK_REQUEST_FILE" \
|
||||||
while true; do
|
</dev/null >/dev/null 2>&1 &
|
||||||
printf 'HTTP/1.1 %d OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{"id":1}\n' "$MOCK_RESPONSE_CODE" \
|
|
||||||
| nc -l "$MOCK_PORT" >> "$MOCK_REQUEST_FILE" 2>/dev/null || break
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
MOCK_PID=$!
|
MOCK_PID=$!
|
||||||
sleep 0.3
|
sleep 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_stop() {
|
mock_stop() {
|
||||||
[ -n "${MOCK_PID:-}" ] && kill "$MOCK_PID" 2>/dev/null || true
|
[ -n "${MOCK_PID:-}" ] && kill -9 "$MOCK_PID" 2>/dev/null || true
|
||||||
_kill_port
|
_kill_port
|
||||||
_wait_port_free
|
_wait_port_free
|
||||||
[ -n "${MOCK_REQUEST_FILE:-}" ] && rm -f "${MOCK_REQUEST_FILE}" 2>/dev/null || true
|
[ -n "${MOCK_REQUEST_FILE:-}" ] && rm -f "${MOCK_REQUEST_FILE}" 2>/dev/null || true
|
||||||
|
[ -n "${MOCK_SEQUENCE_FILE:-}" ] && rm -f "${MOCK_SEQUENCE_FILE}" 2>/dev/null || true
|
||||||
|
[ -n "${MOCK_SEQUENCE_FILE:-}" ] && rm -f "${MOCK_SEQUENCE_FILE}.idx" 2>/dev/null || true
|
||||||
|
[ -n "${MOCK_CONFIG_FILE:-}" ] && rm -f "${MOCK_CONFIG_FILE}" 2>/dev/null || true
|
||||||
rm -f "$MOCK_STATE_FILE"
|
rm -f "$MOCK_STATE_FILE"
|
||||||
MOCK_PID=""
|
MOCK_PID=""
|
||||||
|
MOCK_SEQUENCE_FILE=""
|
||||||
|
MOCK_CONFIG_FILE=""
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_set_response() {
|
mock_set_response() {
|
||||||
@@ -65,13 +87,37 @@ _get_request_file() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mock_get_request_body() {
|
mock_get_request_body() {
|
||||||
tail -1 "$(_get_request_file)" 2>/dev/null || true
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && tail -1 "$rf" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_get_request_path() {
|
mock_get_request_path() {
|
||||||
head -1 "$(_get_request_file)" 2>/dev/null | awk '{print $2}' || true
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && tail -2 "$rf" 2>/dev/null | head -1 | awk '{print $2}' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_get_first_request_path() {
|
||||||
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && head -1 "$rf" 2>/dev/null | awk '{print $2}' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_get_first_request_method() {
|
||||||
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && head -1 "$rf" 2>/dev/null | awk '{print $1}' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_get_first_request_body() {
|
||||||
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && sed -n '2p' "$rf" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_get_request_method() {
|
mock_get_request_method() {
|
||||||
head -1 "$(_get_request_file)" 2>/dev/null | awk '{print $1}' || true
|
local rf
|
||||||
|
rf=$(_get_request_file)
|
||||||
|
[ -f "$rf" ] && tail -2 "$rf" 2>/dev/null | head -1 | awk '{print $1}' || true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import http.server, json, sys, os, threading
|
||||||
|
|
||||||
|
PORT = int(sys.argv[1])
|
||||||
|
CONFIG = sys.argv[2]
|
||||||
|
REQ_FILE = sys.argv[3]
|
||||||
|
|
||||||
|
idx_lock = threading.Lock()
|
||||||
|
mode = 'SINGLE'
|
||||||
|
default_code = 200
|
||||||
|
responses = []
|
||||||
|
idx_path = ''
|
||||||
|
|
||||||
|
with open(CONFIG) as f:
|
||||||
|
mode = f.readline().strip()
|
||||||
|
if mode == 'SEQUENCE':
|
||||||
|
seq_raw = f.readline().strip()
|
||||||
|
idx_path = f.readline().strip()
|
||||||
|
responses = json.loads(seq_raw)
|
||||||
|
else:
|
||||||
|
default_code = int(f.readline().strip())
|
||||||
|
|
||||||
|
def read_idx():
|
||||||
|
try:
|
||||||
|
with open(idx_path) as f2:
|
||||||
|
return int(f2.read().strip())
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def write_idx(v):
|
||||||
|
with open(idx_path, 'w') as f2:
|
||||||
|
f2.write(str(v))
|
||||||
|
|
||||||
|
class H(http.server.BaseHTTPRequestHandler):
|
||||||
|
def _get_response(self):
|
||||||
|
if mode == 'SEQUENCE':
|
||||||
|
with idx_lock:
|
||||||
|
i = read_idx()
|
||||||
|
r = responses[min(i, len(responses)-1)]
|
||||||
|
write_idx(i + 1)
|
||||||
|
code = r.get('code', 200)
|
||||||
|
body = r.get('body', {'id':1})
|
||||||
|
return code, json.dumps(body)
|
||||||
|
return default_code, json.dumps({'id':1})
|
||||||
|
|
||||||
|
def _log_request(self, method):
|
||||||
|
path = self.path
|
||||||
|
content_len = int(self.headers.get('Content-Length', 0))
|
||||||
|
body = self.rfile.read(content_len).decode() if content_len else ''
|
||||||
|
line = f'{method} {path}\n{body}\n'
|
||||||
|
with open(REQ_FILE, 'a') as f:
|
||||||
|
f.write(line)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self._log_request('GET')
|
||||||
|
code, body = self._get_response()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body.encode())
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
self._log_request('POST')
|
||||||
|
code, body = self._get_response()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body.encode())
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
s = http.server.HTTPServer(('', PORT), H)
|
||||||
|
s.allow_reuse_address = True
|
||||||
|
s.serve_forever()
|
||||||
Reference in New Issue
Block a user