From 258749e43dc8c2e842f96f8672823b0fa4e5a147 Mon Sep 17 00:00:00 2001
From: Ed Santiago <santiago@redhat.com>
Date: Mon, 8 Mar 2021 15:32:26 -0700
Subject: apiv2 tests: finally fix POST as originally intended

When I originally wrote this code I had no idea what POST
would look like so I did a sloppy job, deferring making it
usable. Now that we have some real-world examples in place,
I have a better understanding of what params look like and
how to make tests more readable/maintainable. (Deferring isn't
always bad: one of my early ideas was to separate params using
commas; that would've been a disaster because some JSON values,
such as arrays, include commas).

This commit implements a better way of dealing with POST:

  * The main concept is still 'key=value'
    * When value is a JSON object (dictionary, array), it
      can be quoted.
    * Multiple params are simply separated by spaces.
      The 3-digit HTTP code is a prominent, readable separator
      between POST params and expected results. The parsing
      code is a little uglier, but test developers need
      never see that. The important thing is that writing
      tests is now easier.
  * POST params can be empty (this removes the need for a
    useless '')

I snuck in one unrelated change: one of the newly-added
tests, .NetworkSettings, was failing when run rootless
(which is how I test on my setup). I made it conditional.

Signed-off-by: Ed Santiago <santiago@redhat.com>
---
 test/apiv2/01-basic.at          | 10 ++++----
 test/apiv2/10-images.at         |  8 +++----
 test/apiv2/12-imagesMore.at     | 10 ++++----
 test/apiv2/20-containers.at     | 53 ++++++++++++++++++++++++++---------------
 test/apiv2/22-stop.at           | 16 ++++++-------
 test/apiv2/25-containersMore.at | 10 ++++----
 test/apiv2/26-containersWait.at | 10 ++++----
 test/apiv2/30-volumes.at        | 47 +++++++++++++++++++++---------------
 test/apiv2/35-networks.at       | 13 ++++++----
 test/apiv2/40-pods.at           | 40 +++++++++++++++----------------
 test/apiv2/44-mounts.at         | 11 ++++++---
 test/apiv2/45-system.at         | 34 +++++++++++++++-----------
 test/apiv2/50-secrets.at        |  8 +++----
 test/apiv2/README.md            | 11 ++++++---
 test/apiv2/test-apiv2           | 36 ++++++++++++++++++----------
 15 files changed, 186 insertions(+), 131 deletions(-)

(limited to 'test')

diff --git a/test/apiv2/01-basic.at b/test/apiv2/01-basic.at
index 1357e0ca6..788007069 100644
--- a/test/apiv2/01-basic.at
+++ b/test/apiv2/01-basic.at
@@ -30,18 +30,18 @@ done
 # Garbage tests - requests that should yield errors
 #
 t GET  /nonesuch                       404
-t POST /nonesuch ''                    404
+t POST /nonesuch                       404
 t GET  container/nonesuch/json         404
 t GET  libpod/containers/nonesuch/json 404
 
 #### FIXME: maybe someday: t GET 'libpod/containers/json?a=b'     400
 
 # Method not allowed
-t POST   /_ping                 '' 405
+t POST   /_ping                    405
 t DELETE /_ping                    405
-t POST   libpod/containers/json '' 405
-t POST   libpod/pods/abc        '' 405
-t POST   info                   '' 405
+t POST   libpod/containers/json    405
+t POST   libpod/pods/abc           405
+t POST   info                      405
 t GET    libpod/containers/create  405
 
 #
diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at
index f866422e2..4ebaeff45 100644
--- a/test/apiv2/10-images.at
+++ b/test/apiv2/10-images.at
@@ -41,18 +41,18 @@ t GET images/$iid/json 200 \
   .Id=sha256:$iid \
   .RepoTags[0]=$IMAGE
 
-t POST "images/create?fromImage=alpine" '' 200 .error~null .status~".*Download complete.*"
+t POST "images/create?fromImage=alpine" 200 .error~null .status~".*Download complete.*"
 
-t POST "images/create?fromImage=alpine&tag=latest" '' 200
+t POST "images/create?fromImage=alpine&tag=latest" 200
 
 # Make sure that new images are pulled
 old_iid=$(podman image inspect --format "{{.ID}}" docker.io/library/alpine:latest)
 podman rmi -f docker.io/library/alpine:latest
 podman tag $IMAGE docker.io/library/alpine:latest
-t POST "images/create?fromImage=alpine" '' 200 .error~null .status~".*$old_iid.*"
+t POST "images/create?fromImage=alpine" 200 .error~null .status~".*$old_iid.*"
 podman untag $IMAGE docker.io/library/alpine:latest
 
-t POST "images/create?fromImage=quay.io/libpod/alpine&tag=sha256:fa93b01658e3a5a1686dc3ae55f170d8de487006fb53a28efcd12ab0710a2e5f" '' 200
+t POST "images/create?fromImage=quay.io/libpod/alpine&tag=sha256:fa93b01658e3a5a1686dc3ae55f170d8de487006fb53a28efcd12ab0710a2e5f" 200
 
 # Display the image history
 t GET libpod/images/nonesuch/history 404
diff --git a/test/apiv2/12-imagesMore.at b/test/apiv2/12-imagesMore.at
index ce3049106..144b83194 100644
--- a/test/apiv2/12-imagesMore.at
+++ b/test/apiv2/12-imagesMore.at
@@ -17,10 +17,10 @@ t GET libpod/images/$IMAGE/tree 200 \
   .Tree~^Image
 
 # Tag nonesuch image
-t POST "libpod/images/nonesuch/tag?repo=myrepo&tag=mytag" '' 404
+t POST "libpod/images/nonesuch/tag?repo=myrepo&tag=mytag" 404
 
 # Tag the image
-t POST "libpod/images/$IMAGE/tag?repo=localhost:5000/myrepo&tag=mytag" '' 201
+t POST "libpod/images/$IMAGE/tag?repo=localhost:5000/myrepo&tag=mytag" 201
 
 t GET libpod/images/$IMAGE/json 200 \
   .RepoTags[1]=localhost:5000/myrepo:mytag
@@ -41,13 +41,13 @@ if [ -z "${GOT_DIGEST}" ] ; then
 fi
 
 # Push to local registry
-t POST "images/localhost:5000/myrepo/push?tlsVerify=false&tag=mytag" '' 200
+t POST "images/localhost:5000/myrepo/push?tlsVerify=false&tag=mytag" 200
 
 # Untag the image
-t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" '' 201
+t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" 201
 
 # Try to push non-existing image
-t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" '' 200
+t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" 200
 jq -re 'select(.errorDetail)' <<<"$output" &>/dev/null || echo -e "${red}not ok: error message not found in output${nc}" 1>&2
 
 t GET libpod/images/$IMAGE/json 200 \
diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at
index f73d03123..834ff5160 100644
--- a/test/apiv2/20-containers.at
+++ b/test/apiv2/20-containers.at
@@ -31,12 +31,16 @@ t GET libpod/containers/json?all=true 200 \
   .[0].ExitCode=0 \
   .[0].IsInfra=false
 
-# Test compat API for Network Settings
+# Test compat API for Network Settings (.Network is N/A when rootless)
+network_expect=
+if root; then
+    network_expect='.[0].NetworkSettings.Networks.podman.NetworkID=podman'
+fi
 t GET /containers/json?all=true 200 \
   length=1 \
   .[0].Id~[0-9a-f]\\{64\\} \
   .[0].Image=$IMAGE \
-  .[0].NetworkSettings.Networks.podman.NetworkID=podman
+  $network_expect
 
 # Make sure `limit` works.
 t GET libpod/containers/json?limit=1 200 \
@@ -68,9 +72,9 @@ t POST libpod/containers/create?name=test_noargs Image=${IMAGE} 201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 # Prior to the fix in #6835, this would fail 500 "args must not be empty"
-t POST   libpod/containers/${cid}/start '' 204
+t POST   libpod/containers/${cid}/start 204
 # Container should exit almost immediately. Wait for it, confirm successful run
-t POST   "libpod/containers/${cid}/wait?condition=stopped&condition=exited"  '' 200 '0'
+t POST   "libpod/containers/${cid}/wait?condition=stopped&condition=exited"  200 '0'
 t GET    libpod/containers/${cid}/json 200 \
   .Id=$cid \
   .State.Status~\\\(exited\\\|stopped\\\) \
@@ -85,15 +89,15 @@ t GET libpod/containers/json?all=true 200 \
 cid=$(jq -r '.[0].Id' <<<"$output")
 
 # No such container
-t POST "libpod/commit?container=nonesuch" '' 404
+t POST "libpod/commit?container=nonesuch" 404
 
 # Comment can only be used with docker format, not OCI
 cparam="repo=newrepo&comment=foo&author=bob"
-t POST "libpod/commit?container=$CNAME&$cparam"  '' 500 \
+t POST "libpod/commit?container=$CNAME&$cparam"  500 \
   .cause="messages are only compatible with the docker image format (-f docker)"
 
 # Commit a new image from the container
-t POST "libpod/commit?container=$CNAME" '' 200 \
+t POST "libpod/commit?container=$CNAME" 200 \
   .Id~[0-9a-f]\\{64\\}
 iid=$(jq -r '.Id' <<<"$output")
 t GET libpod/images/$iid/json 200 \
@@ -103,7 +107,7 @@ t GET libpod/images/$iid/json 200 \
 
 # Commit a new image w/o tag
 cparam="repo=newrepo&comment=foo&author=bob&format=docker"
-t POST "libpod/commit?container=$CNAME&$cparam" '' 200
+t POST "libpod/commit?container=$CNAME&$cparam" 200
 t GET libpod/images/newrepo:latest/json 200 \
   .RepoTags[0]=localhost/newrepo:latest	\
   .Author=bob \
@@ -111,7 +115,7 @@ t GET libpod/images/newrepo:latest/json 200 \
 
 # Commit a new image w/ specified tag and author
 cparam="repo=newrepo&tag=v1&author=alice"
-t POST "libpod/commit?container=$cid&$cparam&pause=false" '' 200
+t POST "libpod/commit?container=$cid&$cparam&pause=false" 200
 t GET libpod/images/newrepo:v1/json 200 \
   .RepoTags[0]=localhost/newrepo:v1     \
   .Author=alice
@@ -119,7 +123,7 @@ t GET libpod/images/newrepo:v1/json 200 \
 # Commit a new image w/ full parameters
 cparam="repo=newrepo&tag=v2&comment=bar&author=eric"
 cparam="$cparam&format=docker&changes=CMD=/bin/foo"
-t POST "libpod/commit?container=${cid:0:12}&$cparam&pause=true" '' 200
+t POST "libpod/commit?container=${cid:0:12}&$cparam&pause=true" 200
 t GET libpod/images/newrepo:v2/json 200 \
   .RepoTags[0]=localhost/newrepo:v2	\
   .Author=eric \
@@ -143,7 +147,7 @@ cpid_file=$(jq -r '.ConmonPidFile' <<<"$output")
 userdata_path=$(dirname $cpid_file)
 
 # Initializing the container
-t POST libpod/containers/myctr/init '' 204
+t POST libpod/containers/myctr/init 204
 
 # Check configuration after initializing
 t GET libpod/containers/myctr/json 200 \
@@ -166,7 +170,11 @@ t DELETE libpod/containers/myctr 204
 
 # test apiv2 create container with correct entrypoint and cmd
 # --data '{"Image":"quay.io/libpod/alpine_labels:latest","Entrypoint":["echo"],"Cmd":["param1","param2"]}'
-t POST containers/create '"Image":"'$IMAGE'","Entrypoint":["echo"],"Cmd":["param1","param2"]' 201 \
+t POST containers/create \
+  Image=$IMAGE \
+  Entrypoint='["echo"]' \
+  Cmd='["param1","param2"]' \
+  201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 t GET containers/$cid/json 200 \
@@ -179,7 +187,10 @@ t GET containers/$cid/json 200 \
 t DELETE containers/$cid 204
 
 # test only set the entrypoint, Cmd should be []
-t POST containers/create '"Image":"'$IMAGE'","Entrypoint":["echo","param1"]' 201 \
+t POST containers/create \
+  Image=$IMAGE \
+  Entrypoint='["echo","param1"]' \
+  201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 t GET containers/$cid/json 200 \
@@ -190,14 +201,14 @@ t GET containers/$cid/json 200 \
   .Args[0]="param1"
 
 # create a running container for after
-t POST containers/create '"Image":"'$IMAGE'","Entrypoint":["top"]' 201 \
+t POST containers/create Image=$IMAGE Entrypoint='["top"]' 201 \
   .Id~[0-9a-f]\\{64\\}
 cid_top=$(jq -r '.Id' <<<"$output")
 t GET containers/${cid_top}/json 200 \
   .Config.Entrypoint[0]="top" \
   .Config.Cmd='[]' \
   .Path="top"
-t POST  containers/${cid_top}/start '' 204
+t POST  containers/${cid_top}/start 204
 # make sure the container is running
 t GET containers/${cid_top}/json 200 \
   .State.Status="running"
@@ -219,13 +230,17 @@ t GET containers/json?filters='{"id":["'${cid}'","'${cid_top}'"],"status":["runn
   length=1 \
   .[0].Id=${cid_top}
 
-t POST containers/${cid_top}/stop "" 204
+t POST containers/${cid_top}/stop 204
 
 t DELETE containers/$cid 204
 t DELETE containers/$cid_top 204
 
 # test the WORKDIR and StopSignal
-t POST containers/create '"Image":"'$ENV_WORKDIR_IMG'","WorkingDir":"/dataDir","StopSignal":"9"' 201 \
+t POST containers/create \
+  Image=$ENV_WORKDIR_IMG \
+  WorkingDir=/dataDir \
+  StopSignal=9 \
+  201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 t GET containers/$cid/json 200 \
@@ -246,7 +261,7 @@ t DELETE images/${MultiTagName}?force=true 200
 # vim: filetype=sh
 
 # Test Volumes field adds an anonymous volume
-t POST containers/create '"Image":"'$IMAGE'","Volumes":{"/test":{}}' 201 \
+t POST containers/create Image=$IMAGE Volumes='{"/test":{}}' 201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 t GET containers/$cid/json 200 \
@@ -265,7 +280,7 @@ t GET containers/json 200 \
 podman stop bar
 
 # Test CPU limit (NanoCPUs)
-t POST containers/create '"Image":"'$IMAGE'","HostConfig":{"NanoCpus":500000}' 201 \
+t POST containers/create Image=$IMAGE HostConfig='{"NanoCpus":500000}' 201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 t GET containers/$cid/json 200 \
diff --git a/test/apiv2/22-stop.at b/test/apiv2/22-stop.at
index 11318ca81..91bc9937d 100644
--- a/test/apiv2/22-stop.at
+++ b/test/apiv2/22-stop.at
@@ -8,17 +8,17 @@ podman pull $IMAGE &>/dev/null
 # stop, by name
 podman run -dt --name mytop $IMAGE top &>/dev/null
 
-t GET  libpod/containers/mytop/json    200 .State.Status=running
-t POST libpod/containers/mytop/stop "" 204
-t GET  libpod/containers/mytop/json    200 .State.Status~\\\(exited\\\|stopped\\\)
-t DELETE libpod/containers/mytop       204
+t GET  libpod/containers/mytop/json 200 .State.Status=running
+t POST libpod/containers/mytop/stop 204
+t GET  libpod/containers/mytop/json 200 .State.Status~\\\(exited\\\|stopped\\\)
+t DELETE libpod/containers/mytop    204
 
 # stop, by ID
 # Remember that podman() hides all output; we need to get our CID via inspect
 podman run -dt --name mytop $IMAGE top
 
-t GET  libpod/containers/mytop/json   200 .State.Status=running
+t GET  libpod/containers/mytop/json 200 .State.Status=running
 cid=$(jq -r .Id <<<"$output")
-t POST libpod/containers/$cid/stop "" 204
-t GET  libpod/containers/mytop/json   200 .State.Status~\\\(exited\\\|stopped\\\)
-t DELETE libpod/containers/mytop      204
+t POST libpod/containers/$cid/stop  204
+t GET  libpod/containers/mytop/json 200 .State.Status~\\\(exited\\\|stopped\\\)
+t DELETE libpod/containers/mytop    204
diff --git a/test/apiv2/25-containersMore.at b/test/apiv2/25-containersMore.at
index b88c798eb..39bfa2e32 100644
--- a/test/apiv2/25-containersMore.at
+++ b/test/apiv2/25-containersMore.at
@@ -17,7 +17,7 @@ t GET libpod/containers/nonesuch/exists 404
 t GET libpod/containers/foo/exists 204
 
 # Pause the container
-t POST libpod/containers/foo/pause '' 204
+t POST libpod/containers/foo/pause 204
 
 t GET libpod/containers/foo/json 200 \
   .Id~[0-9a-f]\\{64\\} \
@@ -27,7 +27,7 @@ t GET libpod/containers/foo/json 200 \
   .Name=foo
 
 # Unpause the container
-t POST libpod/containers/foo/unpause '' 204
+t POST libpod/containers/foo/unpause 204
 
 t GET libpod/containers/foo/json 200 \
   .Id~[0-9a-f]\\{64\\} \
@@ -44,11 +44,11 @@ t GET libpod/containers/foo/top 200 \
 t GET libpod/containers/nonesuch/top 404
 
 # Mount the container to host filesystem
-t POST libpod/containers/foo/mount '' 200
+t POST libpod/containers/foo/mount 200
 like "$output" ".*merged" "Check container mount"
 
 # Unmount the container
-t POST libpod/containers/foo/unmount '' 204
+t POST libpod/containers/foo/unmount 204
 
 t DELETE libpod/containers/foo?force=true 204
 
@@ -85,7 +85,7 @@ podman run $IMAGE true
 podman run $IMAGE true
 podman run $IMAGE true
 
-t POST libpod/containers/prune '' 200
+t POST libpod/containers/prune 200
 t GET libpod/containers/json 200 \
   length=0
 # vim: filetype=sh
diff --git a/test/apiv2/26-containersWait.at b/test/apiv2/26-containersWait.at
index 3f530c3f0..6a628e55a 100644
--- a/test/apiv2/26-containersWait.at
+++ b/test/apiv2/26-containersWait.at
@@ -13,15 +13,15 @@ podman rm -a -f &>/dev/null
 
 CTR="WaitTestingCtr"
 
-t POST "containers/nonExistent/wait?condition=next-exit" '' 404
+t POST "containers/nonExistent/wait?condition=next-exit" 404
 
 podman create --name "${CTR}" --entrypoint '["sleep", "0.5"]' "${IMAGE}"
 
-t POST "containers/${CTR}/wait?condition=non-existent-cond" '' 400
+t POST "containers/${CTR}/wait?condition=non-existent-cond" 400
 
-t POST "containers/${CTR}/wait?condition=not-running" '' 200
+t POST "containers/${CTR}/wait?condition=not-running" 200
 
-t POST "containers/${CTR}/wait?condition=next-exit" '' 200 &
+t POST "containers/${CTR}/wait?condition=next-exit" 200 &
 child_pid=$!
 podman start "${CTR}"
 wait "${child_pid}"
@@ -37,7 +37,7 @@ if kill -2 "${child_pid}" 2> "/dev/null"; then
   WAIT_TEST_ERROR="1"
 fi
 
-t POST "containers/${CTR}/wait?condition=removed" '' 200 &
+t POST "containers/${CTR}/wait?condition=removed" 200 &
 child_pid=$!
 podman container rm "${CTR}"
 wait "${child_pid}"
diff --git a/test/apiv2/30-volumes.at b/test/apiv2/30-volumes.at
index cf4b3d3ea..c27c638bb 100644
--- a/test/apiv2/30-volumes.at
+++ b/test/apiv2/30-volumes.at
@@ -13,25 +13,34 @@ t POST libpod/volumes/create name=foo1  201 \
     .CreatedAt~[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.* \
     .Labels={} \
     .Options={}
-t POST libpod/volumes/create ''  201
+t POST libpod/volumes/create 201
 t POST libpod/volumes/create \
-    '"Name":"foo2","Label":{"testlabel":"testonly"},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \
-    .Name=foo2 \
-    .Labels.testlabel=testonly \
-    .Options.type=tmpfs \
-    .Options.o=nodev,noexec
+  Name=foo2 \
+  Label='{"testlabel":"testonly"}' \
+  Options='{"type":"tmpfs","o":"nodev,noexec"}}' \
+  201 \
+  .Name=foo2 \
+  .Labels.testlabel=testonly \
+  .Options.type=tmpfs \
+  .Options.o=nodev,noexec
 t POST libpod/volumes/create \
-    '"Name":"foo3","Label":{"testlabel":""},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \
-    .Name=foo3 \
-    .Labels.testlabel="" \
-    .Options.type=tmpfs \
-    .Options.o=nodev,noexec
+  Name=foo3 \
+  Label='{"testlabel":""}' \
+  Options='{"type":"tmpfs","o":"nodev,noexec"}}' \
+  201 \
+  .Name=foo3 \
+  .Labels.testlabel="" \
+  .Options.type=tmpfs \
+  .Options.o=nodev,noexec
 t POST libpod/volumes/create \
-    '"Name":"foo4","Label":{"testlabel1":"testonly"},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \
-    .Name=foo4 \
-    .Labels.testlabel1=testonly \
-    .Options.type=tmpfs \
-    .Options.o=nodev,noexec
+  Name=foo4 \
+  Label='{"testlabel1":"testonly"}' \
+  Options='{"type":"tmpfs","o":"nodev,noexec"}}' \
+  201 \
+  .Name=foo4 \
+  .Labels.testlabel1=testonly \
+  .Options.type=tmpfs \
+  .Options.o=nodev,noexec
 
 # Negative test
 # We have created a volume named "foo1"
@@ -78,15 +87,15 @@ t DELETE libpod/volumes/foo1 404 \
     .response=404
 
 ## Prune volumes with label matching 'testlabel1=testonly'
-t POST libpod/volumes/prune?filters='{"label":["testlabel1=testonly"]}' "" 200
+t POST libpod/volumes/prune?filters='{"label":["testlabel1=testonly"]}' 200
 t GET libpod/volumes/json?filters='{"label":["testlabel1=testonly"]}' 200 length=0
 
 ## Prune volumes with label matching 'testlabel'
-t POST libpod/volumes/prune?filters='{"label":["testlabel"]}' "" 200
+t POST libpod/volumes/prune?filters='{"label":["testlabel"]}' 200
 t GET libpod/volumes/json?filters='{"label":["testlabel"]}' 200 length=0
 
 ## Prune volumes
-t POST libpod/volumes/prune "" 200
+t POST libpod/volumes/prune 200
 #After prune volumes, there should be no volume existing
 t GET libpod/volumes/json 200 length=0
 
diff --git a/test/apiv2/35-networks.at b/test/apiv2/35-networks.at
index d3bbaf32b..98786e914 100644
--- a/test/apiv2/35-networks.at
+++ b/test/apiv2/35-networks.at
@@ -6,18 +6,21 @@
 t GET networks/non-existing-network 404 \
   .cause='network not found'
 
-t POST libpod/networks/create?name=network1 '' 200 \
+t POST libpod/networks/create?name=network1 200 \
   .Filename~.*/network1\\.conflist
 
 # --data '{"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]},"Labels":{"abc":"val"}}'
-t POST libpod/networks/create?name=network2 '"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]},"Labels":{"abc":"val"}' 200 \
+t POST libpod/networks/create?name=network2 \
+  Subnet='{"IP":"10.10.254.0","Mask":[255,255,255,0]}' \
+  Labels='{"abc":"val"}' \
+  200 \
   .Filename~.*/network2\\.conflist
 
 # test for empty mask
-t POST libpod/networks/create '"Subnet":{"IP":"10.10.1.0","Mask":[]}' 500 \
+t POST libpod/networks/create Subnet='{"IP":"10.10.1.0","Mask":[]}' 500 \
   .cause~'.*cannot be empty'
 # test for invalid mask
-t POST libpod/networks/create '"Subnet":{"IP":"10.10.1.0","Mask":[0,255,255,0]}' 500 \
+t POST libpod/networks/create Subnet='{"IP":"10.10.1.0","Mask":[0,255,255,0]}' 500 \
   .cause~'.*mask is invalid'
 
 # network list
@@ -55,7 +58,7 @@ t GET networks/a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1
   .Scope=local
 
 # network create docker
-t POST networks/create '"Name":"net3","IPAM":{"Config":[]}' 201
+t POST networks/create Name=net3\ IPAM='{"Config":[]}' 201
 # network delete docker
 t DELETE networks/net3 204
 
diff --git a/test/apiv2/40-pods.at b/test/apiv2/40-pods.at
index ce65105d2..f3272c41e 100644
--- a/test/apiv2/40-pods.at
+++ b/test/apiv2/40-pods.at
@@ -25,70 +25,70 @@ t POST "libpod/pods/create (dup pod)" name=foo 409 \
 
 #t POST libpod/pods/create a=b 400 .cause='bad parameter'  # FIXME: unimplemented
 
-t POST libpod/pods/foo/start '' 200 \
+t POST libpod/pods/foo/start 200 \
   .Errs=null \
   .Id=$pod_id
 
-t POST libpod/pods/foo/start '' 304 \
+t POST libpod/pods/foo/start 304 \
 
-t POST libpod/pods/fakename/start '' 404 \
+t POST libpod/pods/fakename/start 404 \
   .cause="no such pod" \
   .message="no pod with name or ID fakename found: no such pod"
 
 if root || have_cgroupsv2; then
-    t POST libpod/pods/foo/pause   '' 200
+    t POST libpod/pods/foo/pause 200
 else
     # Rootless cgroupsv1 : unsupported
-    t POST "libpod/pods/foo/pause (rootless cgroups v1)" '' 500 \
+    t POST "libpod/pods/foo/pause (rootless cgroups v1)" 500 \
       .cause="this container does not have a cgroup" \
       .message~".*pause pods containing rootless containers with cgroup V1"
 fi
-t POST  libpod/pods/foo/unpause '' 200
-t POST "libpod/pods/foo/unpause (2nd unpause in a row)" '' 200
-t POST "libpod/pods/fakename/unpause" '' 404\
+t POST  libpod/pods/foo/unpause 200
+t POST "libpod/pods/foo/unpause (2nd unpause in a row)" 200
+t POST "libpod/pods/fakename/unpause" 404\
   .cause="no such pod" \
   .message="no pod with name or ID fakename found: no such pod"
 
 
-t POST libpod/pods/foo/stop '' 200 \
+t POST libpod/pods/foo/stop 200 \
   .Errs=null \
   .Id=$pod_id
 
-t POST "libpod/pods/foo/stop (pod is already stopped)"  '' 304
-t POST "libpod/pods/fakename/stop" '' 404\
+t POST "libpod/pods/foo/stop (pod is already stopped)" 304
+t POST "libpod/pods/fakename/stop" 404\
   .cause="no such pod" \
   .message="no pod with name or ID fakename found: no such pod"
 
-t POST  libpod/pods/foo/restart '' 200 \
+t POST  libpod/pods/foo/restart 200 \
   .Errs=null \
   .Id=$pod_id
 
-t POST  "libpod/pods/bar/restart (restart on nonexistent pod)" '' 404
+t POST  "libpod/pods/bar/restart (restart on nonexistent pod)" 404
 t POST libpod/pods/create name=bar 201 .Id~[0-9a-f]\\{64\\}
 pod_bar_id=$(jq -r .Id <<<"$output")
 
-t POST  libpod/pods/bar/restart '' 200 \
+t POST  libpod/pods/bar/restart 200 \
   .Errs=null \
   .Id=$pod_bar_id
 
-t GET  libpod/pods/bar/json        200 \
+t GET  libpod/pods/bar/json     200 \
   .State=Running
 
-t POST  libpod/pods/bar/restart '' 200 \
+t POST  libpod/pods/bar/restart 200 \
   .Errs=null \
   .Id=$pod_bar_id
 
-t POST "libpod/pods/bar/stop?t=invalid" '' 400 \
+t POST "libpod/pods/bar/stop?t=invalid" 400 \
   .cause="schema: error converting value for \"t\"" \
   .message~"failed to parse parameters for"
 
 podman run -d --pod bar busybox sleep 999
 
-t POST libpod/pods/bar/stop?t=1 '' 200 \
+t POST libpod/pods/bar/stop?t=1 200 \
   .Errs=null \
   .Id=$pod_bar_id
 
-t POST libpod/pods/bar/start '' 200
+t POST libpod/pods/bar/start 200
 
 t GET libpod/pods/stats?all=true 200
 is $(jq '. | length' <<<"$output") 3 "stats?all=true: number of records found"
@@ -118,7 +118,7 @@ t GET libpod/pods/foo/top?ps_args=args,pid 200 \
 
 # FIXME: I'm not sure what 'prune' is supposed to do; as of 20200224 it
 # just returns 200 (ok) with empty result list.
-#t POST libpod/pods/prune ''    200     # FIXME: 2020-02-24 returns 200 {}
+#t POST libpod/pods/prune       200     # FIXME: 2020-02-24 returns 200 {}
 #t POST libpod/pods/prune 'a=b' 400     # FIXME: 2020-02-24 returns 200
 
 # Clean up; and try twice, making sure that the second time fails
diff --git a/test/apiv2/44-mounts.at b/test/apiv2/44-mounts.at
index 5dc560852..901245da6 100644
--- a/test/apiv2/44-mounts.at
+++ b/test/apiv2/44-mounts.at
@@ -4,7 +4,12 @@ podman pull $IMAGE &>/dev/null
 
 # Test various HostConfig options
 tmpfs_name="/mytmpfs"
-t POST containers/create?name=hostconfig_test '"Image":"'$IMAGE'","Cmd":["df"],"HostConfig":{"Binds":["/tmp/doesnotexist:/test1"],"TmpFs":{"'$tmpfs_name'":"rw"}}' 201 \
+t POST containers/create?name=hostconfig_test \
+  Image=$IMAGE \
+  Cmd='["df"]' \
+  HostConfig='{"Binds":["/tmp/doesnotexist:/test1"]' \
+  TmpFs="{\"$tmpfs_name\":\"rw\"}}" \
+  201 \
   .Id~[0-9a-f]\\{64\\}
 cid=$(jq -r '.Id' <<<"$output")
 
@@ -13,8 +18,8 @@ t GET containers/${cid}/json 200 \
   .HostConfig.Tmpfs[\"${tmpfs_name}\"]~rw,
 
 # Run the container, verify output
-t POST containers/${cid}/start '' 204
-t POST containers/${cid}/wait  '' 200
+t POST containers/${cid}/start 204
+t POST containers/${cid}/wait  200
 t GET  containers/${cid}/logs?stdout=true  200
 
 like "$(<$WORKDIR/curl.result.out)" ".* ${tmpfs_name}" \
diff --git a/test/apiv2/45-system.at b/test/apiv2/45-system.at
index ad4bdf4f7..364b87c56 100644
--- a/test/apiv2/45-system.at
+++ b/test/apiv2/45-system.at
@@ -27,22 +27,28 @@ t GET libpod/system/df 200 '.Volumes[0].VolumeName=foo1'
 
 # Create two more volumes to test pruneing
 t POST libpod/volumes/create \
-    '"Name":"foo2","Label":{"testlabel1":""},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \
-    .Name=foo2 \
-    .Driver=local \
-    .Mountpoint=$volumepath/foo2/_data \
-    .CreatedAt~[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.* \
-    .Labels.testlabel1="" \
-    .Options.o=nodev,noexec
+  Name=foo2 \
+  Label='{"testlabel1":""}' \
+  Options='{"type":"tmpfs","o":"nodev,noexec"}}' \
+  201 \
+  .Name=foo2 \
+  .Driver=local \
+  .Mountpoint=$volumepath/foo2/_data \
+  .CreatedAt~[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.* \
+  .Labels.testlabel1="" \
+  .Options.o=nodev,noexec
 
 t POST libpod/volumes/create \
-    '"Name":"foo3","Label":{"testlabel1":"testonly"},"Options":{"type":"tmpfs","o":"nodev,noexec"}}' 201 \
-    .Name=foo3 \
-    .Driver=local \
-    .Mountpoint=$volumepath/foo3/_data \
-    .CreatedAt~[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.* \
-    .Labels.testlabel1=testonly \
-    .Options.o=nodev,noexec
+  Name=foo3 \
+  Label='{"testlabel1":"testonly"}' \
+  Options='{"type":"tmpfs","o":"nodev,noexec"}}' \
+  201 \
+  .Name=foo3 \
+  .Driver=local \
+  .Mountpoint=$volumepath/foo3/_data \
+  .CreatedAt~[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.* \
+  .Labels.testlabel1=testonly \
+  .Options.o=nodev,noexec
 
 t GET system/df 200 '.Volumes | length=3'
 t GET libpod/system/df 200 '.Volumes | length=3'
diff --git a/test/apiv2/50-secrets.at b/test/apiv2/50-secrets.at
index c4ffb5883..034ec080a 100644
--- a/test/apiv2/50-secrets.at
+++ b/test/apiv2/50-secrets.at
@@ -4,14 +4,14 @@
 #
 
 # secret create
-t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0"' 200\
+t POST secrets/create Name=mysecret Data=c2VjcmV0 200\
     .ID~.* \
 
 # secret create unsupported labels
-t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0","Labels":{"fail":"fail"}' 400
+t POST secrets/create Name=mysecret Data=c2VjcmV0 Labels='{"fail":"fail"}' 400
 
 # secret create name already in use
-t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0"' 409
+t POST secrets/create Name=mysecret Data=c2VjcmV0 409
 
 # secret inspect
 t GET secrets/mysecret 200 \
@@ -36,4 +36,4 @@ t DELETE secrets/mysecret 204
 t DELETE secrets/bogus 404
 
 # secret update not implemented
-t POST secrets/mysecret/update "" 501
+t POST secrets/mysecret/update 501
diff --git a/test/apiv2/README.md b/test/apiv2/README.md
index 252d6454e..19727cec7 100644
--- a/test/apiv2/README.md
+++ b/test/apiv2/README.md
@@ -52,9 +52,14 @@ Notes:
 If there's no leading slash, `t` prepends `/v1.40`. This is a simple
 convenience for simplicity of writing tests.
 
-* When method is POST, the argument after the endpoint must be a series
-of POST arguments in the form 'key=value', separated by commas. `t` will
-convert those to JSON form for passing to the server.
+* When method is POST, the argument(s) after the endpoint may be a series
+of POST parameters in the form 'key=value', separated by spaces:
+     t POST myentrypoint 200                                 ! no params
+     t POST myentrypoint id=$id 200                          ! just one
+     t POST myentrypoint id=$id filter='{"foo":"bar"}' 200   ! two, with json
+     t POST myentrypoint name=$name badparam='["foo","bar"]' 500  ! etc...
+`t` will convert the param list to JSON form for passing to the server.
+A numeric status code terminates processing of POST parameters.
 
 * The final arguments are one or more expected string results. If an
 argument starts with a dot, `t` will invoke `jq` on the output to
diff --git a/test/apiv2/test-apiv2 b/test/apiv2/test-apiv2
index e32d6bc62..9f6bf257f 100755
--- a/test/apiv2/test-apiv2
+++ b/test/apiv2/test-apiv2
@@ -156,19 +156,23 @@ function _bump() {
 #  jsonify  #  convert 'foo=bar,x=y' to json {"foo":"bar","x":"y"}
 #############
 function jsonify() {
-    # split by comma
-    local -a settings_in
-    read -ra settings_in <<<"$1"
-
     # convert each to double-quoted form
     local -a settings_out
-    for i in ${settings_in[*]}; do
-        settings_out+=$(sed -e 's/\(.*\)=\(.*\)/"\1":"\2"/' <<<$i)
+    for i in "$@"; do
+        # Each argument is of the form foo=bar. Separate into left and right.
+        local lhs
+        local rhs
+        IFS='=' read lhs rhs <<<"$i"
+
+        # If right-hand side already includes double quotes, do nothing
+        if [[ ! $rhs =~ \" ]]; then
+            rhs="\"${rhs}\""
+        fi
+        settings_out+=("\"${lhs}\":${rhs}")
     done
 
-    # ...and wrap inside braces.
-    # FIXME: handle commas
-    echo "{${settings_out[*]}}"
+    # ...and wrap inside braces, with comma separator if multiple fields
+    (IFS=','; echo "{${settings_out[*]}}")
 }
 
 #######
@@ -180,11 +184,19 @@ function t() {
     local curl_args
 
     local testname="$method $path"
-    # POST requests require an extra params arg
+    # POST requests may be followed by one or more key=value pairs.
+    # Slurp the command line until we see a 3-digit status code.
     if [[ $method = "POST" ]]; then
-        curl_args="-d $(jsonify $1)"
+        local -a post_args
+        for arg; do
+            case "$arg" in
+                *=*)              post_args+=("$arg"); shift ;;
+                [1-9][0-9][0-9])  break;;
+                *)                die "Internal error: invalid POST arg '$arg'" ;;
+            esac
+        done
+        curl_args="-d $(jsonify ${post_args[@]})"
         testname="$testname [$curl_args]"
-        shift
     fi
 
     # entrypoint path can include a descriptive comment; strip it off
-- 
cgit v1.2.3-54-g00ecf