feat: v0 — glpi-go client, mglpi CLI, mglpi-mcp, fake-GLPI test suite [#767]
https://projects.knownelement.com/issues/767#note-4191
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
// Command fakeglpi boots the in-memory fake GLPI on a fixed address for
|
||||
// the smoke run (see smoke/smoke.sh), with a few demo CIs seeded and an
|
||||
// optional agent-profile gate. The real CMDB is never contacted.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.knownelement.com/ukrrs/mopac-glpi-go/internal/fakeglpi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":8602", "listen address")
|
||||
requireProfile := flag.Int("require-change-profile", 0, "reject change creation unless this profile is active (agent-mode gate)")
|
||||
flag.Parse()
|
||||
app := os.Getenv("FAKE_APP_TOKEN")
|
||||
if app == "" {
|
||||
app = "smoke-app-token-0123456789"
|
||||
}
|
||||
user := os.Getenv("FAKE_USER_TOKEN")
|
||||
if user == "" {
|
||||
user = "smoke-user-token-0123456789"
|
||||
}
|
||||
log.Printf("fakeglpi listening on %s", *addr)
|
||||
log.Fatal(fakeglpi.ListenAndServe(*addr, app, user, *requireProfile))
|
||||
}
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
#!/bin/sh
|
||||
# End-to-end smoke for mglpi: builds the CLI + MCP server in the Docker
|
||||
# builder, boots the FAKE GLPI in a container on 127.0.0.1:8602 (with
|
||||
# the agent-profile gate on: change creation needs profile 5), drives
|
||||
# the real binaries from the host through 0600 env files, and asserts
|
||||
# the full command surface (whoami, change create/list/show/transition/
|
||||
# followup, ci search/show, -o json, MCP handshake, exit codes) plus
|
||||
# token redaction. No real CMDB is ever contacted. Only the exact
|
||||
# container ID spawned here is removed.
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
IMAGE="golang@sha256:e8c859f5632dcfde7b32d2012b4351728f6437930887c2f6a91ea242459e5514"
|
||||
PORT=8602
|
||||
APP_TOK="smoke-app-token-0123456789"
|
||||
USER_TOK="smoke-user-token-0123456789"
|
||||
CID=""
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$CID" ]; then
|
||||
docker rm -f "$CID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf .smoke
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
mkdir -p .smoke
|
||||
umask 077
|
||||
|
||||
echo "--- build CLI + MCP (docker builder)"
|
||||
docker run --rm -v "$PWD:/h" -w /h \
|
||||
-u "$(id -u):$(id -g)" -e HOME=/tmp -e GOFLAGS=-buildvcs=false \
|
||||
"$IMAGE" sh -c 'go build -o bin/mglpi ./cmd/mglpi && go build -o bin/mglpi-mcp ./cmd/mglpi-mcp'
|
||||
|
||||
echo "--- boot fake GLPI (container, port $PORT, agent gate: profile 5)"
|
||||
CID=$(docker run -d --rm \
|
||||
-v "$PWD:/h" -w /h \
|
||||
-u "$(id -u):$(id -g)" -e HOME=/tmp -e GOFLAGS=-buildvcs=false \
|
||||
-p 127.0.0.1:$PORT:8602 \
|
||||
-e FAKE_APP_TOKEN="$APP_TOK" -e FAKE_USER_TOKEN="$USER_TOK" \
|
||||
"$IMAGE" go run ./smoke/fakeglpi -addr :8602 -require-change-profile 5)
|
||||
|
||||
# wait for the fake to answer (any HTTP response, even 400, proves it is up)
|
||||
i=0
|
||||
until [ -n "$CID" ] && \
|
||||
[ "$(docker inspect -f '{{.State.Running}}' "$CID" 2>/dev/null)" = "true" ] && \
|
||||
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; do
|
||||
i=$((i+1))
|
||||
if [ "$i" -ge 60 ]; then
|
||||
echo "smoke: fake GLPI did not come up; logs:" >&2
|
||||
docker logs "$CID" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# plain.env has NO profile: the agent gate must reject it.
|
||||
printf 'MGLPI_URL=http://127.0.0.1:%s/apirest.php\nMGLPI_APP_TOKEN=%s\nMGLPI_USER_TOKEN=%s\n' \
|
||||
"$PORT" "$APP_TOK" "$USER_TOK" > .smoke/plain.env
|
||||
# agent.env = agent mode: auto-switch to profile 5 (Hotliner) after init.
|
||||
cp .smoke/plain.env .smoke/agent.env
|
||||
printf 'MGLPI_PROFILE_ID=5\n' >> .smoke/agent.env
|
||||
chmod 600 .smoke/plain.env .smoke/agent.env
|
||||
|
||||
mglpi_plain() { ./bin/mglpi --config .smoke/plain.env "$@"; }
|
||||
mglpi_agent() { ./bin/mglpi --config .smoke/agent.env "$@"; }
|
||||
|
||||
echo "--- whoami (profiles)"
|
||||
mglpi_plain whoami > .smoke/whoami.out
|
||||
grep -q "Hotliner" .smoke/whoami.out
|
||||
grep -q "Super-admin" .smoke/whoami.out
|
||||
grep -q "\[active\]" .smoke/whoami.out
|
||||
|
||||
echo "--- agent gate: create WITHOUT profile is rejected (exit 2, http 403)"
|
||||
if mglpi_plain change create --title "Smoke: gated" --content - > .smoke/gate.out 2> .smoke/gate.err </dev/null; then
|
||||
echo "smoke: create without profile should fail" >&2; exit 1
|
||||
fi
|
||||
grep -q "http 403" .smoke/gate.err || { echo "smoke: gate stderr missing http 403: $(cat .smoke/gate.err)" >&2; exit 1; }
|
||||
[ "$(wc -l < .smoke/gate.err)" -eq 1 ] || { echo "smoke: stderr not one line: $(cat .smoke/gate.err)" >&2; exit 1; }
|
||||
[ ! -s .smoke/gate.out ] || { echo "smoke: stdout not empty on failure" >&2; exit 1; }
|
||||
|
||||
echo "--- change create (agent env auto-switch, stdin body, json out)"
|
||||
mglpi_agent change create --title "Smoke: quota accounting" \
|
||||
--urgency 3 --impact 4 --content - <<'EOF' > .smoke/create.out
|
||||
## Scope
|
||||
- smoke body
|
||||
EOF
|
||||
grep -q "created change #" .smoke/create.out
|
||||
ID1=$(sed -n 's/^created change #\([0-9]*\)$/\1/p' .smoke/create.out)
|
||||
[ -n "$ID1" ] || { echo "smoke: no id in create out: $(cat .smoke/create.out)" >&2; exit 1; }
|
||||
mglpi_agent change create --title "Smoke: json shape" --content - -o json </dev/null > .smoke/create.json.out
|
||||
grep -q '"id"' .smoke/create.json.out
|
||||
ID2=$(jq -r '.change.id' .smoke/create.json.out)
|
||||
[ -n "$ID2" ] && [ "$ID2" != "null" ] || { echo "smoke: no id in create json" >&2; exit 1; }
|
||||
|
||||
echo "--- change list + status filters (text and json)"
|
||||
mglpi_agent change list > .smoke/list.out
|
||||
grep -q "Smoke: quota accounting" .smoke/list.out
|
||||
mglpi_agent change list --status new > .smoke/listn.out
|
||||
grep -q "Smoke: quota accounting" .smoke/listn.out
|
||||
mglpi_agent change list -o json > .smoke/list.json.out
|
||||
jq -e '.changes | length == 2' .smoke/list.json.out >/dev/null
|
||||
|
||||
echo "--- change show + transition + followup"
|
||||
mglpi_agent change show "$ID1" > .smoke/show.out || { echo "smoke: show failed: $(cat .smoke/show.out)" >&2; exit 1; }
|
||||
grep -q "Smoke: quota accounting" .smoke/show.out || { echo "smoke: show missing title: $(cat .smoke/show.out)" >&2; exit 1; }
|
||||
grep -q "smoke body" .smoke/show.out || { echo "smoke: show missing body: $(cat .smoke/show.out)" >&2; exit 1; }
|
||||
mglpi_agent change transition "$ID2" solved > .smoke/trans.out 2> .smoke/trans.err || { echo "smoke: transition failed: $(cat .smoke/trans.err)" >&2; exit 1; }
|
||||
grep -q "updated change #$ID2" .smoke/trans.out || { echo "smoke: transition confirmation wrong: $(cat .smoke/trans.out)" >&2; exit 1; }
|
||||
mglpi_agent change followup "$ID2" --content - <<'EOF' > .smoke/fup.out 2> .smoke/fup.err
|
||||
REPORT delivered: smoke followup
|
||||
EOF
|
||||
grep -q "added followup to change #$ID2" .smoke/fup.out || { echo "smoke: followup failed: $(cat .smoke/fup.err)" >&2; exit 1; }
|
||||
|
||||
echo "--- ci search + show"
|
||||
mglpi_agent ci search Computer smoke > .smoke/ci.out
|
||||
grep -q "smoke-web-01" .smoke/ci.out
|
||||
mglpi_agent ci search Computer smoke -o json > .smoke/ci.json.out
|
||||
CIID=$(jq -r '.results[0].id' .smoke/ci.json.out)
|
||||
[ -n "$CIID" ] && [ "$CIID" != "null" ] || { echo "smoke: no CI in search json" >&2; exit 1; }
|
||||
mglpi_agent ci show Computer "$CIID" -o json > .smoke/cishow.out
|
||||
jq -e '.serial' .smoke/cishow.out >/dev/null
|
||||
|
||||
echo "--- failure path: missing change (exit 2, one-line stderr with http code)"
|
||||
if mglpi_agent change show 999 > .smoke/missing.out 2> .smoke/missing.err; then
|
||||
echo "smoke: missing change should fail" >&2; exit 1
|
||||
fi
|
||||
grep -q "http 404" .smoke/missing.err
|
||||
[ "$(wc -l < .smoke/missing.err)" -eq 1 ] || { echo "smoke: stderr not one line" >&2; exit 1; }
|
||||
[ ! -s .smoke/missing.out ] || { echo "smoke: stdout not empty on failure" >&2; exit 1; }
|
||||
|
||||
echo "--- failure path: wrong user token (exit 2, no token material in stderr)"
|
||||
printf 'MGLPI_URL=http://127.0.0.1:%s/apirest.php\nMGLPI_APP_TOKEN=%s\nMGLPI_USER_TOKEN=wrong-user-token\n' \
|
||||
"$PORT" "$APP_TOK" > .smoke/bad.env
|
||||
chmod 600 .smoke/bad.env
|
||||
if ./bin/mglpi --config .smoke/bad.env change list 2> .smoke/bad.err; then
|
||||
echo "smoke: wrong user token should fail" >&2; exit 1
|
||||
fi
|
||||
grep -q "http 400" .smoke/bad.err
|
||||
|
||||
echo "--- failure path: loose env file (exit 1)"
|
||||
cp .smoke/plain.env .smoke/loose.env
|
||||
chmod 644 .smoke/loose.env
|
||||
if ./bin/mglpi --config .smoke/loose.env change list 2> .smoke/loose.err; then
|
||||
echo "smoke: loose env file should be refused" >&2; exit 1
|
||||
fi
|
||||
grep -q "insecure mode" .smoke/loose.err
|
||||
|
||||
echo "--- MCP handshake (initialize + tools/list + change_list over stdio)"
|
||||
printf '%s\n' \
|
||||
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
|
||||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||||
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
||||
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"change_list","arguments":{}}}' \
|
||||
| ./bin/mglpi-mcp --config .smoke/agent.env > .smoke/mcp.out 2> .smoke/mcp.err
|
||||
|
||||
# Three response lines: the notification is answered with silence.
|
||||
[ "$(wc -l < .smoke/mcp.out)" -eq 3 ] || { echo "smoke: MCP responses = $(wc -l < .smoke/mcp.out), want 3" >&2; exit 1; }
|
||||
sed -n 1p .smoke/mcp.out | jq -e '.result.protocolVersion' >/dev/null
|
||||
sed -n 2p .smoke/mcp.out | jq -e '.result.tools | length == 6' >/dev/null
|
||||
sed -n 3p .smoke/mcp.out | jq -e '.result.content[0].text | fromjson | .changes | length == 2' >/dev/null
|
||||
|
||||
echo "--- redaction: no token material in any captured output"
|
||||
for f in .smoke/*.out .smoke/*.err; do
|
||||
if grep -qF "$APP_TOK" "$f" || grep -qF "$USER_TOK" "$f"; then
|
||||
echo "smoke: token leaked into $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if grep -qF "wrong-user-token" .smoke/bad.err; then
|
||||
echo "smoke: wrong token echoed" >&2; exit 1
|
||||
fi
|
||||
|
||||
echo "smoke: OK"
|
||||
Reference in New Issue
Block a user