From b7999fcb7df0377aca28347491a8804c99faa4cd Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Sat, 30 May 2026 00:12:13 +0000 Subject: [PATCH] tests: add Linux AFP entry id smoke test Add an optional Linux-side smoke test for the first implemented NetWare AFP endpoint. The WebSDK documents NCP 0x2222/35/12 AFP Get Entry ID From Path Name as taking a NetWare directory handle and path string and returning a 32-bit AFP entry id. MARS-NWE now has a guarded implementation of that probe when the optional Netatalk/libatalk backend is compiled in, but exercising it does not require a real AppleTalk workstation. Use the ncpfs/libncp client library as the test transport. ncpfs is commonly available on Linux mars_nwe test hosts and its NWRequestSimple() helper builds the same length-prefixed subfunction request format used by normal libncp callers. The test accepts standard ncpfs connection options such as -S, -U, -P, and -n, sends NCP 0x23/0x0c, and prints the returned entry id. Keep the test out of normal builds behind MARS_NWE_BUILD_LINUX_TESTS because it depends on host ncpfs development headers/library and on a running server. Add an --allow-invalid-namespace mode so builds without the Netatalk backend can still run a negative smoke test and verify that AFP remains unavailable. This adds test infrastructure only and does not change server protocol behavior. --- CMakeLists.txt | 5 + tests/linux/CMakeLists.txt | 23 +++++ tests/linux/README.md | 40 ++++++++ tests/linux/afp_entry_id_smoke.c | 170 +++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 tests/linux/CMakeLists.txt create mode 100644 tests/linux/README.md create mode 100644 tests/linux/afp_entry_id_smoke.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 069cd84..57e95d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ option(ENABLE_NETATALK_LIBATALK "Enable optional Netatalk/libatalk AFP metadata option(MARS_NWE_INSTALL_DOSUTILS "Install DOS client utilities" ON) option(MARS_NWE_INSTALL_NEW_DOSUTILS "Install the new/experimental DOS client utilities instead of legacy netold.exe" OFF) option(MARS_NWE_BUILD_DOSUTILS "Build DOS client utilities with Open Watcom" OFF) +option(MARS_NWE_BUILD_LINUX_TESTS "Build optional Linux integration tests using ncpfs/libncp" OFF) set(MARS_NWE_SMART_ADMIN_GROUP "root" CACHE STRING "Unix group allowed to log in to the SMArT/nwwebui admin interface") @@ -244,6 +245,10 @@ add_subdirectory(include) add_subdirectory(src) add_subdirectory(opt) add_subdirectory(sys) + +if(MARS_NWE_BUILD_LINUX_TESTS) + add_subdirectory(tests/linux) +endif() add_subdirectory(dosutils) add_subdirectory(mail) add_subdirectory(smart) diff --git a/tests/linux/CMakeLists.txt b/tests/linux/CMakeLists.txt new file mode 100644 index 0000000..01b1a29 --- /dev/null +++ b/tests/linux/CMakeLists.txt @@ -0,0 +1,23 @@ +# Optional Linux-side integration tests. +# +# These helpers are not built by default because they depend on the host +# ncpfs/libncp development files and on a running NetWare-compatible server. + +find_path(NCPFS_INCLUDE_DIR + NAMES ncp/nwcalls.h ncp/ncplib.h +) + +find_library(NCPFS_LIBRARY + NAMES ncp +) + +if(NOT NCPFS_INCLUDE_DIR OR NOT NCPFS_LIBRARY) + message(FATAL_ERROR + "MARS_NWE_BUILD_LINUX_TESTS requires ncpfs/libncp headers and libncp. " + "Install the ncpfs development package or disable MARS_NWE_BUILD_LINUX_TESTS." + ) +endif() + +add_executable(afp_entry_id_smoke afp_entry_id_smoke.c) +target_include_directories(afp_entry_id_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_entry_id_smoke ${NCPFS_LIBRARY}) diff --git a/tests/linux/README.md b/tests/linux/README.md new file mode 100644 index 0000000..eb4b547 --- /dev/null +++ b/tests/linux/README.md @@ -0,0 +1,40 @@ +# Linux NCP smoke tests + +This directory contains optional Linux-side integration tests for endpoints that +are easier to exercise from a Unix host than from the DOS test utilities. + +The tests use the ncpfs/libncp client library. They are not built by default +because they require the host ncpfs development headers/library and a running +NetWare-compatible server. + +Build with: + +```sh +cmake -DMARS_NWE_BUILD_LINUX_TESTS=ON ... +cmake --build . --target afp_entry_id_smoke +``` + +## AFP Entry ID smoke test + +`afp_entry_id_smoke` sends the WebSDK-documented NetWare AFP request: + +```text +NCP 0x2222/35/12 AFP Get Entry ID From Path Name +``` + +It uses libncp's `NWRequestSimple()` path, so it goes through the same client +transport stack as other Linux ncpfs utilities. + +Example: + +```sh +./tests/linux/afp_entry_id_smoke -S MARS -U SUPERVISOR -P secret SYS:LOGIN +``` + +If the server was built without the optional Netatalk/libatalk backend, the +endpoint is expected to return invalid namespace. To treat that as a successful +negative smoke test, use: + +```sh +./tests/linux/afp_entry_id_smoke --allow-invalid-namespace -S MARS SYS:LOGIN +``` diff --git a/tests/linux/afp_entry_id_smoke.c b/tests/linux/afp_entry_id_smoke.c new file mode 100644 index 0000000..b6d5614 --- /dev/null +++ b/tests/linux/afp_entry_id_smoke.c @@ -0,0 +1,170 @@ +/* + * Linux smoke test for NetWare AFP Entry ID path lookup. + * + * This intentionally uses ncpfs/libncp instead of a private socket path so the + * request is sent through the same client stack that is commonly available on + * Linux mars_nwe test hosts. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#ifndef NCPC_SUBFUNCTION +#define NCPC_SUBFUNCTION 0x10000 +#endif +#ifndef NCPC_SFN +#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION) +#endif + +#define AFP_GET_ENTRY_ID_FROM_PATH_NAME 0x0c +#define NWE_INVALID_NAMESPACE 0xbf + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--allow-invalid-namespace] [--dir-handle N] " + "[ncpfs options] PATH\n" + "\n" + "ncpfs options are parsed by ncp_initialize(), for example:\n" + " -S SERVER -U USER -P PASSWORD -n\n" + "\n" + "Examples:\n" + " %s -S MARS -U SUPERVISOR -P secret SYS:LOGIN\n" + " %s --allow-invalid-namespace -S MARS SYS:LOGIN\n", + prog, prog, prog); +} + +static int parse_u8(const char *text, unsigned int *value) +{ + char *end = NULL; + unsigned long v; + + errno = 0; + v = strtoul(text, &end, 0); + if (errno || !end || *end || v > 255) + return -1; + *value = (unsigned int)v; + return 0; +} + +static uint32_t be32_to_cpu(const uint8_t p[4]) +{ + return ((uint32_t)p[0] << 24) | + ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | + ((uint32_t)p[3]); +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + NW_FRAGMENT reply; + long init_err = 0; + const char *path = NULL; + unsigned int dir_handle = 0; + int allow_invalid_namespace = 0; + int i; + size_t path_len; + uint8_t request[1 + 1 + 255]; + uint8_t reply_buf[4]; + NWCCODE err; + + if (NWCallsInit(NULL, NULL)) { + fprintf(stderr, "NWCallsInit failed\n"); + return 2; + } + + conn = ncp_initialize(&argc, argv, 0, &init_err); + if (!conn) { + fprintf(stderr, "ncp_initialize failed: %ld\n", init_err); + usage(argv[0]); + return 2; + } + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--allow-invalid-namespace")) { + allow_invalid_namespace = 1; + } else if (!strcmp(argv[i], "--dir-handle")) { + if (++i >= argc || parse_u8(argv[i], &dir_handle)) { + fprintf(stderr, "invalid --dir-handle value\n"); + ncp_close(conn); + return 2; + } + } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + usage(argv[0]); + ncp_close(conn); + return 0; + } else if (!path) { + path = argv[i]; + } else { + fprintf(stderr, "unexpected argument: %s\n", argv[i]); + usage(argv[0]); + ncp_close(conn); + return 2; + } + } + + if (!path) { + fprintf(stderr, "missing PATH\n"); + usage(argv[0]); + ncp_close(conn); + return 2; + } + + path_len = strlen(path); + if (path_len > 255) { + fprintf(stderr, "PATH is too long for AFP Get Entry ID From Path Name: %zu\n", + path_len); + ncp_close(conn); + return 2; + } + + request[0] = (uint8_t)dir_handle; + request[1] = (uint8_t)path_len; + memcpy(request + 2, path, path_len); + + memset(reply_buf, 0, sizeof(reply_buf)); + reply.fragAddr.rw = reply_buf; + reply.fragSize = sizeof(reply_buf); + + err = NWRequestSimple(conn, + NCPC_SFN(0x23, AFP_GET_ENTRY_ID_FROM_PATH_NAME), + request, + 2 + path_len, + &reply); + + if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) { + printf("AFP Get Entry ID From Path Name returned invalid namespace " + "as expected: path=%s\n", path); + ncp_close(conn); + return 0; + } + + if (err) { + fprintf(stderr, + "AFP Get Entry ID From Path Name failed: completion=0x%02x (%u) path=%s\n", + (unsigned int)err & 0xff, (unsigned int)err, path); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < 4) { + fprintf(stderr, "short AFP reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + printf("AFP Entry ID path=%s dir_handle=%u entry_id=0x%08x (%u)\n", + path, dir_handle, + (unsigned int)be32_to_cpu(reply_buf), + (unsigned int)be32_to_cpu(reply_buf)); + + ncp_close(conn); + return 0; +}