Files
Stefan-Sanger 9be6241a55 test: cover remaining AUDIT gaps — presence exclusion, hidden override, store_user SQL, module load detection
- test_02_muc.py: add test_muc_mam_presence_not_archived (negative assertion
  that presence stanzas are excluded from MUC MAM) and
  test_hidden_lib_rejects_public_override (config form absence + raw override
  submission with xfail for admin accounts)
- test_06_postgres.py: add TestPostgresStoreUserInversion which injects via
  REST and queries the archive table directly to assert the 'user' column is
  the sender; add TestPostgresKickStore and TestPostgresActivityStore for
  mod_vnc_track_kicks and mod_vnc_lastactivity store tables
- test_07_module_load.py: new file with smoke tests for the five previously
  zero-coverage modules (lastactivity IQ, delfile message hook, remotemucstore
  message hook, remotemucinvite event, track_kicks event)
- MANUAL_TESTS.md: expand mod_auth_http_async section with mock-HTTP-server
  procedure; add container build verification steps
- AUDIT.md: revise verdict to 'adequate for upgrade safety'; update all
  coverage tables to reflect 55 tests across 7 files

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
2026-07-15 17:58:02 +02:00

11 KiB

Manual Tests — VNCtalk Prosody

Some behavioral contracts cannot be verified through the XMPP client API or HTTP endpoints. They require server-side inspection, federation, or direct file/database access inside the running container.

This document lists those contracts and provides step-by-step manual verification procedures.


§ 1 — Patch contracts not testable via XMPP

1.1 mod_mam.luastore_user inversion for REST messages

Contract: When a message is injected via POST /rest, the store_user variable is set to the sender (orig_from), not the recipient. This inverts normal C2S message archiving where store_user is the recipient.

Why automated test is hard: The test can verify the message appears in MAM, but it cannot distinguish whether it was archived under the sender's user row or the recipient's user row without direct database inspection.

Manual verification:

  1. Ensure Prosody is configured with default_storage = "sql" and PostgreSQL.
  2. Connect as user1@domain and user2@domain.
  3. POST /rest a message:
    <message id="rest-test-1" from="user1@domain" to="user2@domain" type="chat">
      <body>manual test</body>
    </message>
    
  4. Query the prosodyarchive (or host-prefixed archive) table:
    SELECT "user", "with", value FROM prosodyarchive WHERE key = 'rest-test-1';
    
  5. Expected: The "user" column is user1 (the sender), not user2 (the recipient). The "with" column is user2@domain.
  6. If "user" is user2, the vnc_rest store-user inversion patch is missing.

1.2 moduleapi.luaopen_host_store cross-component store opening

Contract: A module running inside a MUC component can call module:open_host_store(mainHost, "private") and receive a valid store handle. The entire FCM push pipeline (mod_vnc_muc_fcm) depends on this.

Why automated test is hard: slixmpp cannot observe whether a Lua module:open_host_store() call throws or succeeds. The only observable effect is downstream FCM push delivery, which requires an actual FCM proxy and device tokens.

Manual verification:

  1. Enable debug logging in Prosody:
    log = { debug = "/var/log/prosody/prosody.log" }
    
  2. Restart the server with mod_vnc_muc_fcm loaded on the MUC component.
  3. Look for these log lines:
    mod_vnc_muc_fcm:  info    founs storage_host: <mainHost>
    mod_vnc_muc_fcm:  info    private/vcard_store are now <table>/...
    
  4. Expected: The module logs success when opening stores on the main virtual host.
  5. If the log shows private/vcard_store nil/nil or a Lua error traceback, the open_host_store patch is missing.

1.3 muc.lib.lua — Broadcast to offline remote affiliates

Contract: After broadcasting to all online occupants, the broadcast method iterates over _affiliations and sends the stanza to any affiliated JIDs whose domain is not hosted locally.

Why automated test is hard: Requires a federated deployment with at least two Prosody instances and S2S connectivity. The test environment is typically a single server.

Manual verification:

  1. Set up two Prosody servers (local: domainA, remote: domainB) with S2S enabled.
  2. On domainA, create a MUC room and grant user@domainB member affiliation.
  3. Ensure user@domainB is not joined to the room.
  4. From an occupant on domainA, send a groupchat message.
  5. On domainB's server, inspect the logs or use mod_debug to verify the message stanza was received over S2S.
  6. Expected: The remote user receives the MUC message despite not being in the room.
  7. If the message is not received, the offline-affiliate broadcast patch is missing.

1.4 mod_admin_telnet.lua — Bound to all interfaces

Contract: The admin console binds to "*" (all interfaces), not "127.0.0.1".

Why automated test is weak: The test only connects to 127.0.0.1:5582. It cannot verify binding on other interfaces without a second network namespace or host.

Manual verification:

  1. From a host/container with network access to the Prosody container's non-loopback IP:
    telnet <prosody-container-ip> 5582
    
  2. Expected: Connection succeeds and Prosody banner is displayed.
  3. If connection refused, the patch binding to "*" is missing.

1.5 portmanager.luanetwork_default_read_size > 4096

Contract: The socket read buffer size is taken from network_default_read_size config (8192 in VNCtalk), not hardcoded 4096.

Why automated test is indirect: The large-stanza test in test_05_patches.py can fail for other reasons (MTU, BOSH chunking, TLS record limits). A definitive check requires observing the actual buffer size in the running process.

Manual verification:

  1. Inside the Prosody container, run:
    netstat -tunapl | grep prosody
    
    or inspect /proc/<pid>/fdinfo/<fd> for the C2S socket.
  2. Alternatively, verify the config is rendered correctly:
    grep network_default_read_size /etc/prosody/prosody.cfg.lua
    
  3. Expected: Output shows network_default_read_size = 8192.
  4. Send a stanza > 8192 bytes and capture with tcpdump:
    tcpdump -i any -s 0 -w /tmp/large.pcap port 5222
    
  5. Open in Wireshark and verify the full stanza appears in a single TCP segment (or at least is not truncated at 4096 bytes).

§ 2 — Custom module contracts requiring external infrastructure

2.1 mod_auth_http_async — HTTP auth delegation (Priority 4 from AUDIT)

Contract: All client authentication requests are forwarded to http_auth_url with a Basic header containing base64(username@host:password). user_exists always returns true. There is no local password database fallback.

Why automated test is hard: Changing Prosody's http_auth_url at runtime requires a config reload or restart, which the test suite cannot do. Without controlling the auth backend, we cannot verify delegation vs. fallback.

Manual verification:

  1. Start a mock HTTP auth server (e.g. Python http.server or nc) on a free port:
    python3 -c "
    import http.server, base64
    class H(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            auth = self.headers.get('Authorization', '')
            if auth.startswith('Basic '):
                creds = base64.b64decode(auth[6:]).decode()
                print(f'AUTH: {creds}')
                if creds == 'user@domain.com:correctpass':
                    self.send_response(200)
                else:
                    self.send_response(401)
            else:
                self.send_response(401)
            self.end_headers()
    http.server.HTTPServer(('', 9999), H).serve_forever()
    "
    
  2. Point Prosody at the mock server by editing config/prosody.cfg.lua.template:
    http_auth_url = "http://localhost:9999/auth"
    
  3. Rebuild the image and start the container.
  4. Attempt XMPP login with wrong credentials (wrongpass).
  5. Expected: The mock server logs the request with Authorization: Basic ... and returns 401. Prosody rejects the XMPP auth.
  6. Attempt XMPP login with correct credentials (correctpass).
  7. Expected: Backend returns 200. Prosody accepts the XMPP auth.
  8. Stop the mock server (so the auth URL is unreachable).
  9. Attempt XMPP login again.
  10. Expected: Prosody rejects auth (not accept with a local fallback). If Prosody accepts auth when the backend is down, the module has a dangerous fallback.
  11. Try to authenticate with a user that does not exist in the backend.
  12. Expected: Prosody still forwards the request (because user_exists returns true). The backend decides whether to accept or reject. If Prosody rejects before contacting the backend, user_exists is not returning true.

2.2 mod_vnc_fcm / mod_vnc_muc_fcm — FCM push delivery

Contract: When a message is sent and the recipient has no active non-hibernated session, an HTTP POST is made to fcm_api_url with the FCM token.

Manual verification:

  1. Register an FCM token via the vnctalk IQ endpoint:
    <iq type="set" id="reg1">
      <add xmlns="xmpp:vnctalk:fcm">
        <fcm device="test-device" token="test-token" os="android"/>
      </add>
    </iq>
    
  2. Ensure the recipient client is disconnected or hibernated.
  3. Send a message to the recipient.
  4. Inspect Prosody logs or run an HTTP mock server at fcm_api_url.
  5. Expected: Prosody POSTs to the FCM URL with a JSON body containing the token and message payload.
  6. If no HTTP request is made, the FCM module or open_host_store patch is broken.

2.3 mod_vnc_vcard_avatar — Avatar HTTP upload side-effect

Contract: When a vCard with PHOTO is set, the module HTTP-PUTs the decoded image to avatar_upload_url.

Manual verification:

  1. Configure avatar_upload_url to a local HTTP server or use tcpdump.
  2. Send a vCard IQ-set with a PHOTO binval.
  3. Expected: An HTTP PUT request is observed at the upload URL with Content-Type: image/png (or the photo type).
  4. If no request is observed, the avatar upload trigger is broken.

2.4 mod_vnc_remotemucstore — Remote MUC archiving

Contract: Messages from a federated (remote) MUC are archived in the local muc_remote store.

Manual verification:

  1. Join a remote MUC room from a local account.
  2. Ensure messages are exchanged.
  3. Query PostgreSQL:
    SELECT * FROM prosodymuc_remote WHERE host = '<local_domain>';
    
  4. Expected: Rows exist with the remote MUC messages.
  5. If empty, the remote MUC archiving is not working.

§ 3 — Healthcheck script

3.1 healthcheck.sh — Exit code 2 on cert mismatch

Contract: When /etc/tls-update/tls.crt differs from /etc/prosody/certs/prosody-ssl.pem, the script exits 2 (not 1).

Manual verification:

  1. Inside the running container:
    cp /etc/prosody/certs/prosody-ssl.pem /tmp/original.pem
    echo "different-cert-data" > /etc/tls-update/tls.crt
    /vnc/config/healthcheck.sh; echo "Exit code: $?"
    
  2. Expected: Exit code is 2.
  3. Restore the original cert:
    cp /tmp/original.pem /etc/tls-update/tls.crt
    
  4. Run healthcheck again:
    /vnc/config/healthcheck.sh; echo "Exit code: $?"
    
  5. Expected: Exit code is 0 (or 1 if prosody admin telnet is down).

§ 4 — Container build verification

4.1 Patches are actually applied in the image

Contract: The Dockerfile RUN cp block copies each patch over the upstream source. If a patch file is renamed or a new patch is added without updating the Dockerfile, the upstream file remains unpatched.

Manual verification:

  1. Build the image:
    docker build -t vnctalk-prosody:test .
    
  2. Run a shell in the image:
    docker run --rm -it vnctalk-prosody:test sh
    
  3. Verify each patched file contains the VNCtalk-specific code:
    grep "vnc-rest-message" /usr/local/lib/prosody/modules/mod_mam/mod_mam.lua
    grep "open_host_store" /usr/local/lib/prosody/core/moduleapi.lua
    grep "vnc-muc-kick" /usr/local/lib/prosody/modules/muc/register.lib.lua
    grep "dumpTable" /usr/local/lib/prosody/modules/muc/muc.lib.lua
    
  4. Expected: All greps return matches.
  5. If any grep is empty, that patch was not copied during the build.