From 00b99f38412cd99d58c3652b611913eb78043624 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 8 Jun 2026 16:07:35 +0300 Subject: [PATCH] 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. --- docs/tickets/0001-report-status-sh.md | 2 +- docs/tickets/0002-dispatch-workflow-sh.md | 2 +- scripts/dispatch-workflow.sh | 70 ++++++++++ tests/dispatch-workflow.bats | 130 ++++++++++++++++++ .../step_definitions/test-execution.steps.js | 120 ++++++++++++++++ tests/features/test-execution.feature | 6 +- tests/helpers/mock-api.sh | 74 ++++++++-- tests/helpers/mock-server.py | 75 ++++++++++ 8 files changed, 460 insertions(+), 19 deletions(-) create mode 100755 scripts/dispatch-workflow.sh create mode 100644 tests/dispatch-workflow.bats create mode 100644 tests/features/step_definitions/test-execution.steps.js create mode 100644 tests/helpers/mock-server.py diff --git a/docs/tickets/0001-report-status-sh.md b/docs/tickets/0001-report-status-sh.md index fefd1e9..f3bd46a 100644 --- a/docs/tickets/0001-report-status-sh.md +++ b/docs/tickets/0001-report-status-sh.md @@ -1,7 +1,7 @@ # Ticket 0001: `report-status.sh` **Vaihe:** 1/12 -**Status:** pending +**Status:** done **Feature branch:** `feature/0001-report-status-sh` **TDD required:** Yes **Feature file required:** Yes diff --git a/docs/tickets/0002-dispatch-workflow-sh.md b/docs/tickets/0002-dispatch-workflow-sh.md index 2397586..d4012d0 100644 --- a/docs/tickets/0002-dispatch-workflow-sh.md +++ b/docs/tickets/0002-dispatch-workflow-sh.md @@ -1,7 +1,7 @@ # Ticket 0002: `dispatch-workflow.sh` **Vaihe:** 2/12 -**Status:** pending +**Status:** done **Feature branch:** `feature/0002-dispatch-workflow-sh` **TDD required:** Yes **Feature file required:** Yes diff --git a/scripts/dispatch-workflow.sh b/scripts/dispatch-workflow.sh new file mode 100755 index 0000000..78905b8 --- /dev/null +++ b/scripts/dispatch-workflow.sh @@ -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 diff --git a/tests/dispatch-workflow.bats b/tests/dispatch-workflow.bats new file mode 100644 index 0000000..2b5575b --- /dev/null +++ b/tests/dispatch-workflow.bats @@ -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 ] +} diff --git a/tests/features/step_definitions/test-execution.steps.js b/tests/features/step_definitions/test-execution.steps.js new file mode 100644 index 0000000..19ec879 --- /dev/null +++ b/tests/features/step_definitions/test-execution.steps.js @@ -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}`); +}); diff --git a/tests/features/test-execution.feature b/tests/features/test-execution.feature index 39fb044..7d7b5df 100644 --- a/tests/features/test-execution.feature +++ b/tests/features/test-execution.feature @@ -11,18 +11,18 @@ Feature: Test execution # 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 When a test workflow is dispatched to a test project Then the pipeline waits until the test workflow finishes 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 When a test workflow is dispatched and the tests fail Then the calling pipeline reports failure - @ticket-0002 @mock @wip + @ticket-0002 @mock 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 Then the calling pipeline reports a timeout error diff --git a/tests/helpers/mock-api.sh b/tests/helpers/mock-api.sh index 9cd4013..7c77d69 100644 --- a/tests/helpers/mock-api.sh +++ b/tests/helpers/mock-api.sh @@ -6,12 +6,14 @@ MOCK_PID="" MOCK_REQUEST_FILE="" MOCK_RESPONSE_CODE=201 MOCK_STATE_FILE="/tmp/mock_api_state" +MOCK_SEQUENCE_FILE="" +MOCK_CONFIG_FILE="" _kill_port() { local pids pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true - [ -n "$pids" ] && kill $pids 2>/dev/null || true - sleep 0.2 + [ -n "$pids" ] && kill -9 $pids 2>/dev/null || true + sleep 0.3 } _wait_port_free() { @@ -22,32 +24,52 @@ _wait_port_free() { 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_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}" MOCK_REQUEST_FILE=$(mktemp) 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 _wait_port_free - ( - while true; do - 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 - ) & + nohup python3 "$(dirname "${BASH_SOURCE[0]}")/mock-server.py" "$MOCK_PORT" "$MOCK_CONFIG_FILE" "$MOCK_REQUEST_FILE" \ + /dev/null 2>&1 & MOCK_PID=$! - sleep 0.3 + sleep 0.5 } 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 _wait_port_free [ -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" MOCK_PID="" + MOCK_SEQUENCE_FILE="" + MOCK_CONFIG_FILE="" } mock_set_response() { @@ -65,13 +87,37 @@ _get_request_file() { } 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() { - 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() { - 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 } diff --git a/tests/helpers/mock-server.py b/tests/helpers/mock-server.py new file mode 100644 index 0000000..2b4cbb1 --- /dev/null +++ b/tests/helpers/mock-server.py @@ -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()