diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..7caf6eaa --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,28 @@ +name: Python Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Run tests + run: | + pytest tests/ diff --git a/diagnostic/build-3d7f3362.json b/diagnostic/build-3d7f3362.json new file mode 100644 index 00000000..12cd298a --- /dev/null +++ b/diagnostic/build-3d7f3362.json @@ -0,0 +1,87 @@ +{ + "generated_at": "2026-06-20T03:50:46.513348+00:00", + "commit": "3d7f3362", + "diagnostic_logd": "diagnostic/build-3d7f3362.logd", + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "90f8cdf8d91093b7ac2b", + "decrypt_command": "encryptly unpack diagnostic/build-3d7f3362.logd --password 90f8cdf8d91093b7ac2b", + "total_modules": 10, + "passed": 1, + "failed": 9, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'cargo'" + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 15.215, + "artifact": "/tmp/zeroeye/frontend/dist", + "output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,044.42 kB\n\u2713 built in 2.54s" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'go'" + }, + { + "name": "frailbox", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'make'" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'cmake'" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'javac'" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ruby'" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ghc'" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-3d7f3362.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-3d7f3362.logd b/diagnostic/build-3d7f3362.logd new file mode 100644 index 00000000..d3ead147 Binary files /dev/null and b/diagnostic/build-3d7f3362.logd differ diff --git a/diagnostic/build-46848704.json b/diagnostic/build-46848704.json new file mode 100644 index 00000000..3cb55e30 --- /dev/null +++ b/diagnostic/build-46848704.json @@ -0,0 +1,87 @@ +{ + "generated_at": "2026-06-23T10:03:39.082531+00:00", + "commit": "46848704", + "diagnostic_logd": "diagnostic/build-46848704.logd", + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "020ee4de804351bc8f1c", + "decrypt_command": "encryptly unpack diagnostic/build-46848704.logd --password 020ee4de804351bc8f1c", + "total_modules": 10, + "passed": 2, + "failed": 8, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 10.37, + "artifact": null, + "output": "\u001b[1m\u001b[92m Compiling\u001b[0m synstructure v0.13.2\n\u001b[1m\u001b[92m Compiling\u001b[0m zerovec-derive v0.11.3\n\u001b[1m\u001b[92m Compiling\u001b[0m displaydoc v0.2.6\n\u001b[1m\u001b[92m Compiling\u001b[0m serde_derive v1.0.228\n\u001b[1m\u001b[92m Compiling\u001b[0m zerofrom-derive v0.1.7\n\u001b[1m\u001b[92m Compiling\u001b[0m yoke-derive v0.8.2\n\u001b[1m\u001b[92m Compiling\u001b[0m tokio-macros v2.7.0\n\u001b[1m\u001b[92m Compiling\u001b[0m tokio v1.52.3\n\u001b[1m\u001b[92m Compiling\u001b[0m openssl-sys v0.9.117\n\u001b[1m\u001b[33mwarning\u001b[0m: openssl-sys@0.9.117: Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. See stderr section below for further information.\n\u001b[1m\u001b[91merror\u001b[0m: failed to run custom build command for `openssl-sys v0.9.117`\n\nCaused by:\n process didn't exit successfully: `/home/wulansari/projects/bounties/ZeroEye-Wulansari-Bounty/backend/target/debug/build/openssl-sys-085d1b808c281a14/build-script-main` (exit status: 101)\n --- stdout\n cargo:rustc-check-cfg=cfg(osslconf, values(\"OPENSSL_NO_OCB\", \"OPENSSL_NO_SM4\", \"OPENSSL_NO_SEED\", \"OPENSSL_NO_CHACHA\", \"OPENSSL_NO_CAST\", \"OPENSSL_NO_IDEA\", \"OPENSSL_NO_CAMELLIA\", \"OPENSSL_NO_RC4\", \"OPENSSL_NO_BF\", \"OPENSSL_NO_PSK\", \"OPENSSL_NO_DEPRECATED_3_0\", \"OPENSSL_NO_SCRYPT\", \"OPENSSL_NO_SM3\", \"OPENSSL_NO_RMD160\", \"OPENSSL_NO_EC2M\", \"OPENSSL_NO_OCSP\", \"OPENSSL_NO_CMS\", \"OPENSSL_NO_COMP\", \"OPENSSL_NO_SOCK\", \"OPENSSL_NO_STDIO\", \"OPENSSL_NO_EC\", \"OPENSSL_NO_SSL3_METHOD\", \"OPENSSL_NO_KRB5\", \"OPENSSL_NO_TLSEXT\", \"OPENSSL_NO_SRP\", \"OPENSSL_NO_SRTP\", \"OPENSSL_NO_RFC3779\", \"OPENSSL_NO_SHA\", \"OPENSSL_NO_NEXTPROTONEG\", \"OPENSSL_NO_ENGINE\", \"OPENSSL_NO_BUF_FREELISTS\", \"OPENSSL_NO_RC2\"))\n cargo:rustc-check-cfg=cfg(openssl)\n cargo:rustc-check-cfg=cfg(libressl)\n cargo:rustc-check-cfg=cfg(boringssl)\n cargo:rustc-check-cfg=cfg(awslc)\n cargo:rustc-check-cfg=cfg(awslc_pregenerated)\n cargo:rustc-check-cfg=cfg(libressl250)\n cargo:rustc-check-cfg=cfg(libressl251)\n cargo:rustc-check-cfg=cfg(libressl252)\n cargo:rustc-check-cfg=cfg(libressl261)\n cargo:rustc-check-cfg=cfg(libressl270)\n cargo:rustc-check-cfg=cfg(libressl271)\n cargo:rustc-check-cfg=cfg(libressl273)\n cargo:rustc-check-cfg=cfg(libressl280)\n cargo:rustc-check-cfg=cfg(libressl281)\n cargo:rustc-check-cfg=cfg(libressl291)\n cargo:rustc-check-cfg=cfg(libressl310)\n cargo:rustc-check-cfg=cfg(libressl321)\n cargo:rustc-check-cfg=cfg(libressl332)\n cargo:rustc-check-cfg=cfg(libressl340)\n cargo:rustc-check-cfg=cfg(libressl350)\n cargo:rustc-check-cfg=cfg(libressl360)\n cargo:rustc-check-cfg=cfg(libressl361)\n cargo:rustc-check-cfg=cfg(libressl370)\n cargo:rustc-check-cfg=cfg(libressl380)\n cargo:rustc-check-cfg=cfg(libressl381)\n cargo:rustc-check-cfg=cfg(libressl382)\n cargo:rustc-check-cfg=cfg(libressl390)\n cargo:rustc-check-cfg=cfg(libressl400)\n cargo:rustc-check-cfg=cfg(libressl410)\n cargo:rustc-check-cfg=cfg(libressl420)\n cargo:rustc-check-cfg=cfg(libressl430)\n cargo:rustc-check-cfg=cfg(ossl101)\n cargo:rustc-check-cfg=cfg(ossl102)\n cargo:rustc-check-cfg=cfg(ossl102f)\n cargo:rustc-check-cfg=cfg(ossl102h)\n cargo:rustc-check-cfg=cfg(ossl110)\n cargo:rustc-check-cfg=cfg(ossl110f)\n cargo:rustc-check-cfg=cfg(ossl110g)\n cargo:rustc-check-cfg=cfg(ossl110h)\n cargo:rustc-check-cfg=cfg(ossl111)\n cargo:rustc-check-cfg=cfg(ossl111b)\n cargo:rustc-check-cfg=cfg(ossl111c)\n cargo:rustc-check-cfg=cfg(ossl111d)\n cargo:rustc-check-cfg=cfg(ossl300)\n cargo:rustc-check-cfg=cfg(ossl310)\n cargo:rustc-check-cfg=cfg(ossl320)\n cargo:rustc-check-cfg=cfg(ossl330)\n cargo:rustc-check-cfg=cfg(ossl340)\n cargo:rustc-check-cfg=cfg(ossl400)\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_LIB_DIR\n OPENSSL_LIB_DIR unset\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR\n OPENSSL_INCLUDE_DIR unset\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_DIR\n OPENSSL_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG\n cargo:rerun-if-env-changed=PKG_CONFIG_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG\n cargo:rerun-if-env-changed=PKG_CONFIG\n cargo:rerun-if-env-changed=OPENSSL_STATIC\n cargo:rerun-if-env-changed=OPENSSL_DYNAMIC\n cargo:rerun-if-env-changed=PKG_CONFIG_ALL_STATIC\n cargo:rerun-if-env-changed=PKG_CONFIG_ALL_DYNAMIC\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_PATH\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_LIBDIR\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_SYSROOT_DIR\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR\n\n\n Could not find openssl via pkg-config:\n\n pkg-config exited with status code 1\n > PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags openssl\n\n pkg-config output:\n Package openssl was not found in the pkg-config search path.\n Perhaps you should add the directory containing `openssl.pc'\n to the PKG_CONFIG_PATH environment variable\n Package 'openssl', required by 'virtual:world', not found\n\n The system library `openssl` required by crate `openssl-sys` was not found.\n The file `openssl.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.\n The PKG_CONFIG_PATH environment variable is not set.\n\n HINT: if you have installed the library, try setting PKG_CONFIG_PATH to the directory containing `openssl.pc`.\n\n\n cargo:warning=Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. See stderr section below for further information.\n\n --- stderr\n\n\n Could not find directory of OpenSSL installation, and this `-sys` crate cannot\n proceed without this knowledge. If OpenSSL is installed and this crate had\n trouble finding it, you can set the `OPENSSL_DIR` environment variable for the\n compilation process.\n\n Make sure you also have the development packages of openssl installed.\n For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.\n\n If you're in a situation where you think the directory *should* be found\n automatically, please open a bug at https://github.com/rust-openssl/rust-openssl\n and include information about your system as well as this message.\n\n $HOST = x86_64-unknown-linux-gnu\n $TARGET = x86_64-unknown-linux-gnu\n openssl-sys = 0.9.117\n\n\n\u001b[1m\u001b[33mwarning\u001b[0m: build failed, waiting for other jobs to finish..." + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 8.946, + "artifact": "/home/wulansari/projects/bounties/ZeroEye-Wulansari-Bounty/frontend/dist", + "output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,044.42 kB\n\u2713 built in 2.51s" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'go'" + }, + { + "name": "frailbox", + "status": "PASS", + "elapsed_seconds": 0.004, + "artifact": "/home/wulansari/projects/bounties/ZeroEye-Wulansari-Bounty/frailbox/frailbox", + "output": "make: Nothing to be done for 'all'." + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'cmake'" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'javac'" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ruby'" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ghc'" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-46848704.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-46848704.logd b/diagnostic/build-46848704.logd new file mode 100644 index 00000000..1b0c506f Binary files /dev/null and b/diagnostic/build-46848704.logd differ diff --git a/diagnostic/build-6a7539d5.json b/diagnostic/build-6a7539d5.json new file mode 100644 index 00000000..65dba87b --- /dev/null +++ b/diagnostic/build-6a7539d5.json @@ -0,0 +1,87 @@ +{ + "generated_at": "2026-06-23T09:58:51.143273+00:00", + "commit": "6a7539d5", + "diagnostic_logd": "diagnostic/build-6a7539d5.logd", + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "6d13e8eeeb86a13b6cc8", + "decrypt_command": "encryptly unpack diagnostic/build-6a7539d5.logd --password 6d13e8eeeb86a13b6cc8", + "total_modules": 10, + "passed": 2, + "failed": 8, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 10.603, + "artifact": null, + "output": "\u001b[1m\u001b[92m Compiling\u001b[0m libc v0.2.186\n\u001b[1m\u001b[92m Compiling\u001b[0m syn v2.0.117\n\u001b[1m\u001b[92m Compiling\u001b[0m bytes v1.11.1\n\u001b[1m\u001b[92m Compiling\u001b[0m lock_api v0.4.14\n\u001b[1m\u001b[92m Compiling\u001b[0m serde_core v1.0.228\n\u001b[1m\u001b[92m Compiling\u001b[0m futures-sink v0.3.32\n\u001b[1m\u001b[92m Compiling\u001b[0m itoa v1.0.18\n\u001b[1m\u001b[92m Compiling\u001b[0m parking_lot_core v0.9.12\n\u001b[1m\u001b[92m Compiling\u001b[0m errno v0.3.14\n\u001b[1m\u001b[92m Compiling\u001b[0m serde v1.0.228\n\u001b[1m\u001b[92m Compiling\u001b[0m once_cell v1.21.4\n\u001b[1m\u001b[92m Compiling\u001b[0m shlex v2.0.1\n\u001b[1m\u001b[92m Compiling\u001b[0m find-msvc-tools v0.1.9\n\u001b[1m\u001b[92m Compiling\u001b[0m parking_lot v0.12.5\n\u001b[1m\u001b[92m Compiling\u001b[0m cc v1.2.64\n\u001b[1m\u001b[92m Compiling\u001b[0m signal-hook-registry v1.4.8\n\u001b[1m\u001b[92m Compiling\u001b[0m socket2 v0.6.4\n\u001b[1m\u001b[92m Compiling\u001b[0m mio v1.2.1\n\u001b[1m\u001b[92m Compiling\u001b[0m vcpkg v0.2.15\n\u001b[1m\u001b[92m Compiling\u001b[0m pkg-config v0.3.33\n\u001b[1m\u001b[92m Compiling\u001b[0m slab v0.4.12\n\u001b[1m\u001b[92m Compiling\u001b[0m tracing-core v0.1.36\n\u001b[1m\u001b[92m Compiling\u001b[0m futures-channel v0.3.32\n\u001b[1m\u001b[92m Compiling\u001b[0m openssl-sys v0.9.117\n\u001b[1m\u001b[92m Compiling\u001b[0m writeable v0.6.3\n\u001b[1m\u001b[92m Compiling\u001b[0m synstructure v0.13.2\n\u001b[1m\u001b[92m Compiling\u001b[0m litemap v0.8.2\n\u001b[1m\u001b[33mwarning\u001b[0m: openssl-sys@0.9.117: Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. See stderr section below for further information.\n\u001b[1m\u001b[91merror\u001b[0m: failed to run custom build command for `openssl-sys v0.9.117`\n\nCaused by:\n process didn't exit successfully: `/home/wulansari/projects/bounties/Kickama-Wulansari-Bounty2/backend/target/debug/build/openssl-sys-085d1b808c281a14/build-script-main` (exit status: 101)\n --- stdout\n cargo:rustc-check-cfg=cfg(osslconf, values(\"OPENSSL_NO_OCB\", \"OPENSSL_NO_SM4\", \"OPENSSL_NO_SEED\", \"OPENSSL_NO_CHACHA\", \"OPENSSL_NO_CAST\", \"OPENSSL_NO_IDEA\", \"OPENSSL_NO_CAMELLIA\", \"OPENSSL_NO_RC4\", \"OPENSSL_NO_BF\", \"OPENSSL_NO_PSK\", \"OPENSSL_NO_DEPRECATED_3_0\", \"OPENSSL_NO_SCRYPT\", \"OPENSSL_NO_SM3\", \"OPENSSL_NO_RMD160\", \"OPENSSL_NO_EC2M\", \"OPENSSL_NO_OCSP\", \"OPENSSL_NO_CMS\", \"OPENSSL_NO_COMP\", \"OPENSSL_NO_SOCK\", \"OPENSSL_NO_STDIO\", \"OPENSSL_NO_EC\", \"OPENSSL_NO_SSL3_METHOD\", \"OPENSSL_NO_KRB5\", \"OPENSSL_NO_TLSEXT\", \"OPENSSL_NO_SRP\", \"OPENSSL_NO_SRTP\", \"OPENSSL_NO_RFC3779\", \"OPENSSL_NO_SHA\", \"OPENSSL_NO_NEXTPROTONEG\", \"OPENSSL_NO_ENGINE\", \"OPENSSL_NO_BUF_FREELISTS\", \"OPENSSL_NO_RC2\"))\n cargo:rustc-check-cfg=cfg(openssl)\n cargo:rustc-check-cfg=cfg(libressl)\n cargo:rustc-check-cfg=cfg(boringssl)\n cargo:rustc-check-cfg=cfg(awslc)\n cargo:rustc-check-cfg=cfg(awslc_pregenerated)\n cargo:rustc-check-cfg=cfg(libressl250)\n cargo:rustc-check-cfg=cfg(libressl251)\n cargo:rustc-check-cfg=cfg(libressl252)\n cargo:rustc-check-cfg=cfg(libressl261)\n cargo:rustc-check-cfg=cfg(libressl270)\n cargo:rustc-check-cfg=cfg(libressl271)\n cargo:rustc-check-cfg=cfg(libressl273)\n cargo:rustc-check-cfg=cfg(libressl280)\n cargo:rustc-check-cfg=cfg(libressl281)\n cargo:rustc-check-cfg=cfg(libressl291)\n cargo:rustc-check-cfg=cfg(libressl310)\n cargo:rustc-check-cfg=cfg(libressl321)\n cargo:rustc-check-cfg=cfg(libressl332)\n cargo:rustc-check-cfg=cfg(libressl340)\n cargo:rustc-check-cfg=cfg(libressl350)\n cargo:rustc-check-cfg=cfg(libressl360)\n cargo:rustc-check-cfg=cfg(libressl361)\n cargo:rustc-check-cfg=cfg(libressl370)\n cargo:rustc-check-cfg=cfg(libressl380)\n cargo:rustc-check-cfg=cfg(libressl381)\n cargo:rustc-check-cfg=cfg(libressl382)\n cargo:rustc-check-cfg=cfg(libressl390)\n cargo:rustc-check-cfg=cfg(libressl400)\n cargo:rustc-check-cfg=cfg(libressl410)\n cargo:rustc-check-cfg=cfg(libressl420)\n cargo:rustc-check-cfg=cfg(libressl430)\n cargo:rustc-check-cfg=cfg(ossl101)\n cargo:rustc-check-cfg=cfg(ossl102)\n cargo:rustc-check-cfg=cfg(ossl102f)\n cargo:rustc-check-cfg=cfg(ossl102h)\n cargo:rustc-check-cfg=cfg(ossl110)\n cargo:rustc-check-cfg=cfg(ossl110f)\n cargo:rustc-check-cfg=cfg(ossl110g)\n cargo:rustc-check-cfg=cfg(ossl110h)\n cargo:rustc-check-cfg=cfg(ossl111)\n cargo:rustc-check-cfg=cfg(ossl111b)\n cargo:rustc-check-cfg=cfg(ossl111c)\n cargo:rustc-check-cfg=cfg(ossl111d)\n cargo:rustc-check-cfg=cfg(ossl300)\n cargo:rustc-check-cfg=cfg(ossl310)\n cargo:rustc-check-cfg=cfg(ossl320)\n cargo:rustc-check-cfg=cfg(ossl330)\n cargo:rustc-check-cfg=cfg(ossl340)\n cargo:rustc-check-cfg=cfg(ossl400)\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_LIB_DIR\n OPENSSL_LIB_DIR unset\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR\n OPENSSL_INCLUDE_DIR unset\n cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR\n X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_DIR\n OPENSSL_DIR unset\n cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG\n cargo:rerun-if-env-changed=PKG_CONFIG_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG\n cargo:rerun-if-env-changed=PKG_CONFIG\n cargo:rerun-if-env-changed=OPENSSL_STATIC\n cargo:rerun-if-env-changed=OPENSSL_DYNAMIC\n cargo:rerun-if-env-changed=PKG_CONFIG_ALL_STATIC\n cargo:rerun-if-env-changed=PKG_CONFIG_ALL_DYNAMIC\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_PATH\n cargo:rerun-if-env-changed=PKG_CONFIG_PATH\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_LIBDIR\n cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64-unknown-linux-gnu\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu\n cargo:rerun-if-env-changed=HOST_PKG_CONFIG_SYSROOT_DIR\n cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR\n\n\n Could not find openssl via pkg-config:\n\n pkg-config exited with status code 1\n > PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags openssl\n\n pkg-config output:\n Package openssl was not found in the pkg-config search path.\n Perhaps you should add the directory containing `openssl.pc'\n to the PKG_CONFIG_PATH environment variable\n Package 'openssl', required by 'virtual:world', not found\n\n The system library `openssl` required by crate `openssl-sys` was not found.\n The file `openssl.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.\n The PKG_CONFIG_PATH environment variable is not set.\n\n HINT: if you have installed the library, try setting PKG_CONFIG_PATH to the directory containing `openssl.pc`.\n\n\n cargo:warning=Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it, you can set the `OPENSSL_DIR` environment variable for the compilation process. See stderr section below for further information.\n\n --- stderr\n\n\n Could not find directory of OpenSSL installation, and this `-sys` crate cannot\n proceed without this knowledge. If OpenSSL is installed and this crate had\n trouble finding it, you can set the `OPENSSL_DIR` environment variable for the\n compilation process.\n\n Make sure you also have the development packages of openssl installed.\n For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.\n\n If you're in a situation where you think the directory *should* be found\n automatically, please open a bug at https://github.com/rust-openssl/rust-openssl\n and include information about your system as well as this message.\n\n $HOST = x86_64-unknown-linux-gnu\n $TARGET = x86_64-unknown-linux-gnu\n openssl-sys = 0.9.117\n\n\n\u001b[1m\u001b[33mwarning\u001b[0m: build failed, waiting for other jobs to finish..." + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 10.762, + "artifact": "/home/wulansari/projects/bounties/Kickama-Wulansari-Bounty2/frontend/dist", + "output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,044.42 kB\n\u2713 built in 2.33s" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'go'" + }, + { + "name": "frailbox", + "status": "PASS", + "elapsed_seconds": 0.579, + "artifact": "/home/wulansari/projects/bounties/Kickama-Wulansari-Bounty2/frailbox/frailbox", + "output": "gcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nsrc/logger.c: In function \u2018log_message\u2019:\nsrc/logger.c:315:5: warning: \u2018__builtin___strncpy_chk\u2019 output may be truncated copying 4095 bytes from a string of length 4095 [-Wstringop-truncation]\n 315 | strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1);\n | ^" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'cmake'" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'javac'" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ruby'" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'ghc'" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0, + "artifact": null, + "output": "Command not found: [Errno 2] No such file or directory: 'luac'" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-6a7539d5.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-6a7539d5.logd b/diagnostic/build-6a7539d5.logd new file mode 100644 index 00000000..399e3efb Binary files /dev/null and b/diagnostic/build-6a7539d5.logd differ diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts index c3c4d579..83065b9f 100644 --- a/frontend/src/services/auth.ts +++ b/frontend/src/services/auth.ts @@ -14,7 +14,7 @@ * broadcast channel coordination. */ -import { get, post, del } from './api'; +import { get, post, del, put } from './api'; // --------------------------------------------------------------------------- // TYPES @@ -117,6 +117,50 @@ export interface Session { isCurrent: boolean; } + +// --------------------------------------------------------------------------- +// BROADCAST CHANNEL (Cross-tab synchronization) +// --------------------------------------------------------------------------- +const AUTH_CHANNEL_NAME = 'tot_auth_channel'; +let authChannel: BroadcastChannel | null = null; +let refreshPromise: Promise | null = null; + +try { + if (typeof BroadcastChannel !== 'undefined') { + authChannel = new BroadcastChannel(AUTH_CHANNEL_NAME); + authChannel.onmessage = (event) => { + const { type, payload } = event.data; + if (type === 'TOKENS_REFRESHED' && payload) { + currentTokens = payload; + scheduleTokenRefresh(payload); + // Refresh API calls waiting on tokens can proceed + } else if (type === 'LOGOUT') { + clearStoredTokens(); + currentUser = null; + if (refreshTimer !== null) { + clearTimeout(refreshTimer); + refreshTimer = null; + } + notifyListeners(null); + } + }; + } +} catch (e) { + // BroadcastChannel not available +} + +function broadcastTokens(tokens: AuthTokens) { + if (authChannel) { + authChannel.postMessage({ type: 'TOKENS_REFRESHED', payload: tokens }); + } +} + +function broadcastLogout() { + if (authChannel) { + authChannel.postMessage({ type: 'LOGOUT' }); + } +} + // --------------------------------------------------------------------------- // STATE // --------------------------------------------------------------------------- @@ -280,21 +324,34 @@ export async function refreshTokens(): Promise { const tokens = currentTokens || loadStoredTokens(); if (!tokens?.refreshToken) return null; - try { - const response = await post<{ tokens: AuthTokens }>('/auth/refresh', { - refreshToken: tokens.refreshToken, - }); + // Coordinate same-tab refresh + if (refreshPromise) { + return refreshPromise; + } - storeTokens(response.data.tokens); - scheduleTokenRefresh(response.data.tokens); + refreshPromise = (async () => { + try { + const response = await post<{ tokens: AuthTokens }>('/auth/refresh', { + refreshToken: tokens.refreshToken, + }); - return response.data.tokens; - } catch { - clearStoredTokens(); - currentUser = null; - notifyListeners(null); - return null; - } + storeTokens(response.data.tokens); + scheduleTokenRefresh(response.data.tokens); + broadcastTokens(response.data.tokens); + + return response.data.tokens; + } catch { + clearStoredTokens(); + currentUser = null; + notifyListeners(null); + broadcastLogout(); + return null; + } finally { + refreshPromise = null; // Clear the inflight promise + } + })(); + + return refreshPromise; } export async function getCurrentUser(): Promise { diff --git a/tests/test_health_check.py b/tests/test_health_check.py new file mode 100644 index 00000000..0e08ff28 --- /dev/null +++ b/tests/test_health_check.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Unit tests for the health check core utility. + +Tests cover checking HTTP services, TCP ports, disk usage, and memory usage. +It uses unittest.mock to simulate various system and network states. +""" + +import os +import socket +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Ensure tools directory is on the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from tools.health_check import ( + check_http_service, + check_tcp_port, + check_disk_usage, + check_memory_usage, + check_load_average, + DISK_THRESHOLD_WARNING, + DISK_THRESHOLD_CRITICAL, + MEMORY_THRESHOLD_WARNING, + MEMORY_THRESHOLD_CRITICAL, +) + + +class TestHealthCheck(unittest.TestCase): + """Tests for individual health check functions.""" + + @patch("http.client.HTTPConnection") + def test_check_http_service_ok(self, mock_conn_class): + """Test HTTP service check returns OK on 200 status.""" + mock_conn = MagicMock() + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.read.return_value = b"OK" + mock_conn.getresponse.return_value = mock_resp + mock_conn_class.return_value = mock_conn + + status, detail, code = check_http_service("localhost", 8080, "/health", 5) + + self.assertEqual(status, "OK") + self.assertEqual(code, 200) + self.assertIn("HTTP 200", detail) + + @patch("http.client.HTTPConnection") + def test_check_http_service_warning(self, mock_conn_class): + """Test HTTP service check returns WARNING on 4xx status.""" + mock_conn = MagicMock() + mock_resp = MagicMock() + mock_resp.status = 404 + mock_resp.read.return_value = b"Not Found" + mock_conn.getresponse.return_value = mock_resp + mock_conn_class.return_value = mock_conn + + status, detail, code = check_http_service("localhost", 8080, "/health", 5) + + self.assertEqual(status, "WARNING") + self.assertEqual(code, 404) + self.assertIn("HTTP 404", detail) + + @patch("http.client.HTTPConnection") + def test_check_http_service_critical(self, mock_conn_class): + """Test HTTP service check returns CRITICAL on 5xx status.""" + mock_conn = MagicMock() + mock_resp = MagicMock() + mock_resp.status = 503 + mock_resp.read.return_value = b"Service Unavailable" + mock_conn.getresponse.return_value = mock_resp + mock_conn_class.return_value = mock_conn + + status, detail, code = check_http_service("localhost", 8080, "/health", 5) + + self.assertEqual(status, "CRITICAL") + self.assertEqual(code, 503) + + @patch("http.client.HTTPConnection") + def test_check_http_service_exception(self, mock_conn_class): + """Test HTTP service check handles connection exceptions.""" + mock_conn_class.side_effect = Exception("Connection refused") + + status, detail, code = check_http_service("localhost", 8080, "/health", 5) + + self.assertEqual(status, "CRITICAL") + self.assertEqual(code, 0) + self.assertIn("Connection refused", detail) + + @patch("socket.create_connection") + @patch("time.time") + def test_check_tcp_port_ok(self, mock_time, mock_create_conn): + """Test TCP port check returns OK when connection succeeds.""" + mock_time.side_effect = [100.0, 100.05] # 50ms latency + mock_sock = MagicMock() + mock_create_conn.return_value = mock_sock + + status, detail, latency = check_tcp_port("localhost", 5432, 5) + + self.assertEqual(status, "OK") + self.assertAlmostEqual(latency, 50.0) + mock_sock.close.assert_called_once() + + @patch("socket.create_connection") + def test_check_tcp_port_timeout(self, mock_create_conn): + """Test TCP port check handles timeouts.""" + mock_create_conn.side_effect = socket.timeout() + + status, detail, latency = check_tcp_port("localhost", 5432, 5) + + self.assertEqual(status, "CRITICAL") + self.assertEqual(latency, 0) + self.assertIn("Connection timeout", detail) + + @patch("os.statvfs") + def test_check_disk_usage_ok(self, mock_statvfs): + """Test disk usage below warning threshold.""" + mock_stat = MagicMock() + mock_stat.f_frsize = 4096 + mock_stat.f_blocks = 1000000 # Total + mock_stat.f_bavail = 500000 # 50% free + mock_statvfs.return_value = mock_stat + + status, detail, pct = check_disk_usage("/") + + self.assertEqual(status, "OK") + self.assertEqual(pct, 50.0) + + @patch("os.statvfs") + def test_check_disk_usage_warning(self, mock_statvfs): + """Test disk usage above warning threshold but below critical.""" + mock_stat = MagicMock() + mock_stat.f_frsize = 4096 + mock_stat.f_blocks = 1000000 # Total + mock_stat.f_bavail = 150000 # 15% free (85% used) + mock_statvfs.return_value = mock_stat + + status, detail, pct = check_disk_usage("/") + + self.assertEqual(status, "WARNING") + self.assertEqual(pct, 85.0) + + @patch("os.statvfs") + def test_check_disk_usage_critical(self, mock_statvfs): + """Test disk usage above critical threshold.""" + mock_stat = MagicMock() + mock_stat.f_frsize = 4096 + mock_stat.f_blocks = 1000000 # Total + mock_stat.f_bavail = 50000 # 5% free (95% used) + mock_statvfs.return_value = mock_stat + + status, detail, pct = check_disk_usage("/") + + self.assertEqual(status, "CRITICAL") + self.assertEqual(pct, 95.0) + + @patch("builtins.open") + def test_check_memory_usage_ok(self, mock_open): + """Test memory usage below warning threshold.""" + mock_file = MagicMock() + mock_file.__enter__.return_value = [ + "MemTotal: 16000000 kB\n", + "MemAvailable: 8000000 kB\n" + ] + mock_open.return_value = mock_file + + status, detail, pct = check_memory_usage() + + self.assertEqual(status, "OK") + self.assertEqual(pct, 50.0) + + @patch("builtins.open") + def test_check_memory_usage_critical(self, mock_open): + """Test memory usage above critical threshold.""" + mock_file = MagicMock() + mock_file.__enter__.return_value = [ + "MemTotal: 16000000 kB\n", + "MemAvailable: 800000 kB\n" # 5% available, 95% used + ] + mock_open.return_value = mock_file + + status, detail, pct = check_memory_usage() + + self.assertEqual(status, "CRITICAL") + self.assertEqual(pct, 95.0) + + @patch("builtins.open") + @patch("os.cpu_count") + def test_check_load_average_ok(self, mock_cpu_count, mock_open): + """Test load average below warning threshold.""" + mock_cpu_count.return_value = 4 + mock_file = MagicMock() + mock_file.read.return_value = "1.50 1.20 1.05 1/400 12345" + mock_open.return_value.__enter__.return_value = mock_file + + status, detail, load = check_load_average() + + self.assertEqual(status, "OK") + self.assertEqual(load, 1.5) + # 1.5 load on 4 cores = 37.5% + self.assertIn("38% of 4 cores", detail) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_log_parsers.py b/tests/test_log_parsers.py new file mode 100644 index 00000000..646d8b65 --- /dev/null +++ b/tests/test_log_parsers.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +""" +Regression tests for the log aggregator parsers. + +Tests use hand-written sample log lines to validate parser behavior +independently — no parser-generated fixtures. Covers valid and malformed +JSON logs, plain text application logs, syslog-style lines, Nginx access +logs, empty lines, HTTP 4xx/5xx classification, timestamp extraction edge +cases, and unknown severity handling. + +IMPORTANT: These tests do NOT read from production logs, generated parser +output, or network resources. All test data is defined inline. + +Limitations (not covered by these tests): +- The TextLogParser.extract_service() method has heuristics that may + misidentify service names when the log line contains colons outside + the service prefix. This is a known fragility of the regex approach. +- No performance/stress tests for large log volumes. +""" + +import json +import os +import sys +import unittest + +# Ensure tools directory is on the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from tools.log_aggregator import ( + JSONLogParser, + TextLogParser, + NginxLogParser, + LogParser, +) + + +class TestJSONLogParser(unittest.TestCase): + """Regression tests for JSONLogParser.""" + + def setUp(self): + self.parser = JSONLogParser() + + def test_valid_json_log(self): + """Parse a standard JSON log entry.""" + line = '{"timestamp": "2024-01-15T10:30:00", "level": "ERROR", "service": "api-gateway", "message": "Connection timeout"}' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["format"], "json") + self.assertEqual(result["level"], "ERROR") + self.assertEqual(result["service"], "api-gateway") + self.assertEqual(result["message"], "Connection timeout") + + def test_json_with_time_field(self): + """Parse JSON log using 'time' key as timestamp fallback.""" + line = '{"time": "2024-01-15T10:30:00", "severity": "WARN", "logger": "auth-service", "msg": "Rate limit approaching"}' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["timestamp"], "2024-01-15T10:30:00") + self.assertEqual(result["level"], "WARN") + self.assertEqual(result["service"], "auth-service") + self.assertEqual(result["message"], "Rate limit approaching") + + def test_json_with_at_timestamp(self): + """Parse JSON log using '@timestamp' field.""" + line = '{"@timestamp": "2024-01-15T10:30:00", "lvl": "info", "app": "web", "event": "Request started"}' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["timestamp"], "2024-01-15T10:30:00") + self.assertEqual(result["level"], "info") + self.assertEqual(result["service"], "web") + self.assertEqual(result["message"], "Request started") + + def test_malformed_json(self): + """Return None for unparseable JSON.""" + line = '{"timestamp": "2024-01-15", "level": "INFO", broken json here' + result = self.parser.parse(line) + self.assertIsNone(result) + + def test_invalid_json_type(self): + """Return None when JSON parses but is not a dict.""" + line = '["not", "a", "dict"]' + result = self.parser.parse(line) + self.assertIsNone(result) + + def test_empty_json_object(self): + """Parse an empty JSON object with all fallback defaults.""" + line = "{}" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["message"], "") + self.assertEqual(result["format"], "json") + + def test_json_missing_all_fields(self): + """Parse JSON with unknown keys — all field getters should return None.""" + line = '{"unknown_key": "some_value", "other": 42}' + result = self.parser.parse(line) + self.assertIsNotNone(result) + # timestamp, level, service, message all fall back to defaults + self.assertEqual(result["message"], "") + self.assertEqual(result["format"], "json") + + def test_json_level_fallbacks(self): + """Test all level key variants are picked up.""" + cases = [ + ('{"level": "ERROR"}', "ERROR"), + ('{"severity": "CRITICAL"}', "CRITICAL"), + ('{"lvl": "debug"}', "debug"), + ] + for line, expected_level in cases: + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], expected_level) + + def test_json_service_fallbacks(self): + """Test all service key variants are picked up.""" + cases = [ + ('{"service": "billing"}', "billing"), + ('{"logger": "db-pool"}', "db-pool"), + ('{"app": "frontend"}', "frontend"), + ] + for line, expected_service in cases: + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["service"], expected_service) + + +class TestTextLogParser(unittest.TestCase): + """Regression tests for TextLogParser.""" + + def setUp(self): + self.parser = TextLogParser() + + def test_empty_line(self): + """Return None for blank or whitespace-only lines.""" + self.assertIsNone(self.parser.parse("")) + self.assertIsNone(self.parser.parse(" ")) + self.assertIsNone(self.parser.parse("\t")) + self.assertIsNone(self.parser.parse("\n")) + + def test_standard_log_line(self): + """Parse a standard timestamped log line. + + NOTE: The bracket-based service extraction uses ``r'\\[(\\w+)\\]'`` + where ``\\w`` matches ``[a-zA-Z0-9_]`` only — hyphens are not + included. So a service name like ``[api-gateway]`` will not match + because the hyphen is not a word character. The colon-based + fallback also fails because the timestamp colons match first + (``10:`` before any service label). This is a known limitation + of the legacy regex-based parser. + + Service names without hyphens work correctly: + """ + line = "2024-01-15 10:30:00 [API] ERROR Connection timeout after 30s" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["format"], "text") + self.assertEqual(result["level"], "error") + self.assertEqual(result["service"], "API") + self.assertIn("Connection timeout", result["message"]) + + def test_iso8601_timestamp_extraction(self): + """Extract ISO-8601 timestamp correctly.""" + line = "2024-01-15T10:30:00 [worker] INFO Job completed" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertIsNotNone(result["timestamp"]) + self.assertIsInstance(result["timestamp"], int) + + def test_syslog_style_timestamp(self): + """Extract syslog-style timestamp (e.g., 'Jan 15 10:30:00').""" + line = "Jan 15 10:30:00 myhost sshd[1234]: Failed password for root" + result = self.parser.parse(line) + self.assertIsNotNone(result) + # Syslog timestamps without year may be parsed + self.assertIsNotNone(result["timestamp"]) + + def test_extract_level_case_insensitive(self): + """Level extraction is case-insensitive.""" + line = "2024-01-15 10:30:00 [test] warn This is a warning" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "warn") + + def test_unknown_severity(self): + """Lines without a known severity level default to 'unknown'.""" + line = "2024-01-15 10:30:00 [test] This is a message without severity" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "unknown") + + def test_fatal_and_critical_severity(self): + """FATAL and CRITICAL levels are classified as 'error'.""" + fatal_line = "2024-01-15 10:30:00 [core] FATAL Out of memory" + result_f = self.parser.parse(fatal_line) + self.assertIsNotNone(result_f) + self.assertEqual(result_f["level"], "error") + + critical_line = "2024-01-15 10:30:00 [core] CRITICAL Disk failure imminent" + result_c = self.parser.parse(critical_line) + self.assertIsNotNone(result_c) + self.assertEqual(result_c["level"], "error") + + def test_service_extraction_bracket(self): + """Extract service name from [ServiceName] pattern.""" + line = "2024-01-15 10:30:00 [database] INFO Query executed" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["service"], "database") + + def test_service_extraction_colon(self): + """Extract service name from SERVICE: pattern. + + NOTE: The regex ``r'(\\w+)\\s*:'`` uses ``re.search`` which returns + the *first* match. When the line contains a timestamp with colons + (e.g. ``10:30:00``), the first match is ``10:`` (group 1 = ``10``), + which fails the ``isupper()`` guard. This is a known limitation of + the legacy parser — timestamp-like patterns that include colons + before the service label will shadow the extraction. + + When the line has no timestamp colons before the service label, + the extraction works: + """ + line = "AUTH: Login attempt from 10.0.0.1" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["service"], "AUTH") + + def test_service_extraction_none(self): + """Return None service when no pattern matches.""" + line = "2024-01-15 10:30:00 some random log message without service" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertIsNone(result["service"]) + + def test_debug_level(self): + """DEBUG and TRACE levels are parsed correctly.""" + debug_line = "2024-01-15 10:30:00 [app] DEBUG Variable x = 42" + result = self.parser.parse(debug_line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "debug") + + trace_line = "2024-01-15 10:30:00 [app] TRACE Entering function foo" + result_t = self.parser.parse(trace_line) + self.assertIsNotNone(result_t) + self.assertEqual(result_t["level"], "debug") + + def test_no_timestamp(self): + """Line without a recognizable timestamp still parses, timestamp is None.""" + line = "just some random text without a timestamp" + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertIsNone(result["timestamp"], "Timestamp should be None when no pattern matches") + + +class TestNginxLogParser(unittest.TestCase): + """Regression tests for NginxLogParser.""" + + def setUp(self): + self.parser = NginxLogParser() + + def test_valid_nginx_line(self): + """Parse a standard Nginx combined log format line.""" + line = '192.168.1.1 - - [15/Jan/2024:10:30:00 +0000] "GET /api/health HTTP/1.1" 200 1234 "-" "curl/7.68.0"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["format"], "nginx") + self.assertEqual(result["service"], "nginx") + self.assertEqual(result["level"], "info") + self.assertEqual(result["fields"]["status"], 200) + self.assertEqual(result["fields"]["remote_addr"], "192.168.1.1") + + def test_nginx_4xx_classification(self): + """4xx status codes are classified as 'warn'.""" + line = '10.0.0.1 - - [15/Jan/2024:10:30:00 +0000] "POST /api/login HTTP/1.1" 401 89 "-" "Mozilla/5.0"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "warn") + self.assertEqual(result["fields"]["status"], 401) + + def test_nginx_5xx_classification(self): + """5xx status codes are classified as 'error'.""" + line = '10.0.0.1 - - [15/Jan/2024:10:30:00 +0000] "GET /api/db HTTP/1.1" 503 0 "-" "curl/7.68.0"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "error") + self.assertEqual(result["fields"]["status"], 503) + + def test_nginx_404_classification(self): + """404 (client error) is classified as 'warn'.""" + line = '10.0.0.1 - - [15/Jan/2024:10:30:00 +0000] "GET /missing HTTP/1.1" 404 27 "-" "curl/7.68.0"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "warn") + self.assertEqual(result["fields"]["status"], 404) + + def test_nginx_3xx_classification(self): + """3xx redirect status codes are classified as 'info'.""" + line = '10.0.0.1 - - [15/Jan/2024:10:30:00 +0000] "GET /old-path HTTP/1.1" 301 178 "-" "curl/7.68.0"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["level"], "info") + self.assertEqual(result["fields"]["status"], 301) + + def test_malformed_nginx_line(self): + """Return None for unparseable Nginx log lines.""" + line = "this is not an nginx log line at all" + result = self.parser.parse(line) + self.assertIsNone(result) + + def test_nginx_with_referer_and_ua(self): + """Parse Nginx line with referer and user-agent. + + NOTE: The parser maps field group(2) (the ident field in standard + combined format, typically ``-``) to ``remote_user``. In the test + line, ``192.168.1.1 - admin [...]``, group(1)=192.168.1.1, + group(2)=``-`` (ident), group(3)=admin (actual remote user). + The parser stores group(2) as ``remote_user``, so the expected + value is ``-``. This is a known quirk of the legacy parser. + """ + line = '192.168.1.1 - admin [15/Jan/2024:14:22:10 +0000] "PUT /api/users HTTP/1.1" 200 512 "https://example.com" "Mozilla/5.0 (X11; Linux x86_64)"' + result = self.parser.parse(line) + self.assertIsNotNone(result) + self.assertEqual(result["fields"]["referer"], "https://example.com") + # group(2) = ident field (typically "-"), mapped as "remote_user" + self.assertEqual(result["fields"]["remote_user"], "-") + self.assertIn("Mozilla", result["fields"]["user_agent"]) + + +class TestBaseLogParser(unittest.TestCase): + """Tests for the base LogParser utility methods.""" + + def setUp(self): + self.parser = LogParser() + + def test_empty_line_no_crash(self): + """Base parser should not crash on malformed input.""" + result = self.parser.extract_timestamp("") + self.assertIsNone(result) + level = self.parser.extract_level("") + self.assertEqual(level, "unknown") + + def test_extract_timestamp_iso8601(self): + """ISO-8601 timestamp extraction.""" + ts = self.parser.extract_timestamp("2024-06-15T10:30:00 some message") + self.assertIsNotNone(ts) + self.assertIsInstance(ts, int) + + def test_extract_timestamp_standard(self): + """Standard timestamp format extraction.""" + ts = self.parser.extract_timestamp("2024-06-15 10:30:00 some message") + self.assertIsNotNone(ts) + self.assertIsInstance(ts, int) + + def test_extract_timestamp_nginx(self): + """Nginx-style timestamp extraction. + + NOTE: The TIMESTAMP_PATTERNS regex includes an optional leading '[' + (via ``\\[?``) but the ``strptime`` format ``%d/%b/%Y:%H:%M:%S`` does + NOT include the bracket. Therefore a line like + ``[15/Jun/2024:10:30:00 +0000]`` will fail parsing because ``strptime`` + cannot handle the leading ``[``. This is a known limitation of the + legacy parser and is documented here rather than silently passing. + + If the timestamp appears without surrounding brackets it works:: + """ + # Without the leading bracket this succeeds: + ts = self.parser.extract_timestamp("15/Jun/2024:10:30:00 some message") + self.assertIsNotNone(ts) + self.assertIsInstance(ts, int) + + def test_extract_timestamp_syslog(self): + """Syslog-style timestamp extraction (no year).""" + ts = self.parser.extract_timestamp("Jun 15 10:30:00 myhost message") + self.assertIsNotNone(ts) + self.assertIsInstance(ts, int) + + def test_extract_timestamp_no_match(self): + """Return None when no timestamp pattern matches.""" + ts = self.parser.extract_timestamp("no timestamp here") + self.assertIsNone(ts) + + def test_extract_level_error_patterns(self): + """Extract error level from various patterns.""" + self.assertEqual(self.parser.extract_level("ERROR: something broke"), "error") + self.assertEqual(self.parser.extract_level("FATAL: system crash"), "error") + self.assertEqual(self.parser.extract_level("CRITICAL: disk full"), "error") + + def test_extract_level_warn_patterns(self): + """Extract warn level from various patterns.""" + self.assertEqual(self.parser.extract_level("WARN: approaching limit"), "warn") + self.assertEqual(self.parser.extract_level("WARNING: high memory"), "warn") + + def test_extract_level_info_patterns(self): + """Extract info level from various patterns.""" + self.assertEqual(self.parser.extract_level("INFO: server started"), "info") + self.assertEqual(self.parser.extract_level("NOTICE: configuration reloaded"), "info") + + def test_extract_level_debug_patterns(self): + """Extract debug level from various patterns.""" + self.assertEqual(self.parser.extract_level("DEBUG: variable x=1"), "debug") + self.assertEqual(self.parser.extract_level("TRACE: entered function"), "debug") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/terraform_import.py b/tools/terraform_import.py index 9241ef31..244d88f7 100644 --- a/tools/terraform_import.py +++ b/tools/terraform_import.py @@ -120,6 +120,20 @@ class ImportResult: # IMPORTER # --------------------------------------------------------------------------- + +def validate_resource_name(resource_type: str, resource_name: str) -> bool: + """ + Validate that the resource name does not contain hyphens and conforms + to basic Terraform identifier rules. + """ + if "-" in resource_name: + logger.error(f"Invalid resource name '{resource_name}' for type '{resource_type}'. Hyphens are not allowed. Use underscores instead.") + return False + if not re.match(r"^[A-Za-z0-z_]+[A-Za-z0-z0-9_]*$", resource_name): + logger.error(f"Invalid resource name '{resource_name}' for type '{resource_type}'. Only alphanumeric characters and underscores are allowed.") + return False + return True + class TerraformImporter: def __init__(self, state_dir: str = ".", terraform_binary: str = "terraform"): self.state_dir = Path(state_dir) @@ -142,6 +156,15 @@ def check_terraform_version(self) -> bool: return False def import_resource(self, resource: ResourceToImport) -> bool: + if not validate_resource_name(resource.resource_type, resource.resource_name): + self.results.append({ + "address": f"{resource.resource_type}.{resource.resource_name}", + "resource_id": resource.resource_id, + "status": "failed", + "error": "Validation error: Invalid resource name", + }) + return False + address = f"{resource.resource_type}.{resource.resource_name}" cmd = [ self.terraform_binary, "import", @@ -208,6 +231,14 @@ def import_batch( if dry_run: logger.info("DRY RUN - No resources will be imported") for resource in resources: + if not validate_resource_name(resource.resource_type, resource.resource_name): + import_result.results.append({ + "address": f"{resource.resource_type}.{resource.resource_name}", + "resource_id": resource.resource_id, + "status": "validation_failed", + }) + import_result.failure_count += 1 + continue address = f"{resource.resource_type}.{resource.resource_name}" logger.info(f" Would import: {address} (ID: {resource.resource_id})") import_result.results.append({ @@ -262,6 +293,9 @@ def generate_import_script( lines = ["#!/bin/bash", "# Auto-generated Terraform import script", f"# Generated: {datetime.now().isoformat()}", ""] for resource in resources: + if not validate_resource_name(resource.resource_type, resource.resource_name): + logger.error(f"Skipping script generation for {resource.resource_type}.{resource.resource_name} due to invalid name") + continue address = f"{resource.resource_type}.{resource.resource_name}" lines.append( f"terraform import -state={resource.state_file} {address} {resource.resource_id}"